Skip to content

Commit

Permalink
Merge pull request #1 from Bimbalacom/Feature/Web
Browse files Browse the repository at this point in the history
Web UI
  • Loading branch information
l-alexandrov authored Apr 5, 2024
2 parents e6de9e5 + 9160c18 commit 7d065eb
Show file tree
Hide file tree
Showing 11 changed files with 2,148 additions and 16 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ yarn-error.log
**/*.jpeg
**/*.webp
**/*.gif
**/*.png
**/*.png
# Local Netlify folder
.netlify
2 changes: 2 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
publish = "public_web"
126 changes: 126 additions & 0 deletions netlify/functions/process/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const archiver = require('archiver');
const sharp = require('sharp');
const mimetics = require('mimetics')
const {parseMultipartForm} = require('./processMultipartForm.js');

exports.handler = async (event) => {
try {
const file = await parseMultipartForm(event); // Assuming the image file is sent in the request body
// Process the image
if(!('image' in file)){
return {
statusCode: 422,
body: JSON.stringify({
data: {
errors: {
image: "The property is missing."
}
}
})
}
}
const processedImages = await processImage(file.image);

// Create a zip file containing the processed images
const zipData = await createZip(processedImages);

// Return the zip file data
return {
statusCode: 200,
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename="processed-images.zip"',
},
body: zipData.toString('base64'),
isBase64Encoded: true,
};
} catch (error) {
console.error('Error processing images:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Error processing images' }),
};
}
};

async function getMaxWidth(filePath) {
const metadata = await sharp(filePath).metadata();
return metadata.width;
}

function responsiveImages(width, originalFormat) {
const formats = originalFormat === 'png' ? ['png', 'webp'] : ['jpeg', 'webp'];

return formats.flatMap(format => [
{ width: Math.min(320, width), format, rename: { suffix: '-xs' } },
{ width: Math.min(375, width), format, rename: { suffix: '-sm' } },
{ width: Math.min(768, width), format, rename: { suffix: '-md' } },
{ width: Math.min(1024, width), format, rename: { suffix: '-lg' } },
{ width: Math.min(1500, width), format, rename: { suffix: '-xl' } },
{ width: Math.min(2000, width), format, rename: { suffix: '-2xl' } },
]);
}

async function processImage(file) {
// Check if the file has a supported extension
const fileExtension = mimetics(file.content).ext;
if (fileExtension.match(/^(png|jpg|jpeg)$/i) === null) {
throw Error(`Skipping unsupported file format: ${fileExtension}`);
}

const maxWidth = await getMaxWidth(file.content);
const originalFormat = fileExtension === "png" ? 'png' : 'jpeg';
const sizes = responsiveImages(maxWidth, originalFormat);
let outputData = {};

for (const size of sizes) {
// Correctly handle file naming for different formats
let outputFileName;
const suffix = size.format.endsWith(originalFormat) ? size.rename.suffix : size.rename.suffix + '.' + size.format;

if (size.format.endsWith(originalFormat)) {
outputFileName = file.filename.replace(/(\.jpeg|\.jpg|\.png)$/i, suffix) + '.' + originalFormat;
} else {
outputFileName = file.filename.replace(/(\.jpeg|\.jpg|\.png)$/i, '') + suffix;
}
if(outputFileName.endsWith('.jpeg')){
outputFileName = outputFileName.replace('.jpeg', '.jpg');
}

const resizedFile = await sharp(file.content).resize({ width: size.width }).toFormat(size.format).toBuffer();
outputData[outputFileName] = resizedFile;
}


return outputData;
}

async function createZip(processedImages) {
return new Promise((resolve, reject) => {
const zipData = [];
const archive = archiver('zip', {
zlib: { level: 9 } // Set compression level to maximum
});

archive.on('error', (err) => {
console.error('Error creating zip file:', err);
reject(err);
});

archive.on('data', (chunk) => {
zipData.push(chunk);
});

archive.on('end', () => {
const zipBuffer = Buffer.concat(zipData);
resolve(zipBuffer);
});

// Add each processed image to the zip file
for (const fileName in processedImages) {
archive.append(processedImages[fileName], { name: fileName });
};

archive.finalize();
});
}
49 changes: 49 additions & 0 deletions netlify/functions/process/processMultipartForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const busboy = require('busboy');

async function parseMultipartForm(event) {
return new Promise((resolve) => {
// we'll store all form fields inside of this
const fields = {};

// let's instantiate our busboy instance!
const bb = busboy({
// it uses request headers
// to extract the form boundary value (the ----WebKitFormBoundary thing)
headers: event.headers
});

// before parsing anything, we need to set up some handlers.
// whenever busboy comes across a file ...
bb.on(
"file",
(fieldname, filestream, info, transferEncoding, mimeType) => {
// ... we take a look at the file's data ...
filestream.on("data", (data) => {
// ... and write the file's name, type and content into `fields`.
const { filename, encoding, mimeType } = info;
fields[fieldname] = {
filename,
type: mimeType,
content: data,
};
});
}
);

// whenever busboy comes across a normal field ...
bb.on("field", (fieldName, value) => {
// ... we write its value into `fields`.
fields[fieldName] = value;
});

// once busboy is finished, we resolve the promise with the resulted fields.
bb.on("close", () => {
resolve(fields)
});

// now that all handlers are set up, we can finally start processing our request!
bb.end(Buffer.from(event.body, 'base64'));
});
}

module.exports = {parseMultipartForm};
Loading

0 comments on commit 7d065eb

Please sign in to comment.