I was trying to create an API endpoint for rotating images uploaded from client side. I'm sending images as base64 type, converted from blob (from simple <input tag), as follows:
const addImageBase64 = async (fileData) => {
const file = fileData;
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
resolve(event.target.result);
};
reader.onerror = (err) => {
reject(err);
};
reader.readAsDataURL(file);
});
};
Then, on the server side, that's how the endpoint looks like:
app.post("/api/rotate-image", async (req, res) => {
try {
let buffer = Buffer.from(req.body.imageData, "base64"); //not working
let array = new Uint8Array(buffer); //not working
const image = await sharp(buffer)
.rotate(180)
.png({ quality: 100 })
.toBuffer();
console.log("success");
res.status(200).send({
success: true,
result: image,
});
} catch (e) {
console.warn(e);
}
});
And here, every my attempt is ending up with '[Error: Input buffer contains unsupported image format]' - either for Buffer or Uint8Array. Can anyone help me with this issue? What is the right input type for Sharp that acctually works?
Edit:
Error with logged buffer obj:
Related
I'm trying encode an image to base64, (so I can later send it this way to a backend server). Everything seems to work until I use JSON.stringify() on the object that has the encoded image in it.
I think It gets lost in the JSON.stringify() and I can't seem to find a solution. I've been working for weeks on this issue and I couldn't find an answer anywhere. Please help!
const [baseImage, setBaseImage] = useState('');
const [baseImageCorrect, setBaseImageCorrect] = useState('');
const convertBase64 = (file) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = () => {
resolve(fileReader.result);
};
fileReader.onerror = (error) => {
reject(error);
console.log(error);
};
});
};
const uploadImage = async (e) => {
const file = e.target.files[0];
const base64 = await convertBase64(file);
const base64RemovedType = base64.split(',')[1];
setBaseImage(`${base64RemovedType}`);
};
useEffect(() => {
setBaseImageCorrect(baseImage);
console.log('current:' + baseImageCorrect);
//prints out a long string with the RIGHT information
}, [baseImage, baseImageCorrect]);
const EncodedImage = JSON.stringify({
fileBase64: (baseImageCorrect, { encoding: 'base64' }),
});
console.log(EncodedImage)
//PRINTS THIS: "fileBase64":{"encoding":"base64"}} , without the encoded image string
I am assuming u need the key baseImageCorrect and encoding key at the same level.
Use this instead:
const EncodedImage = JSON.stringify({
fileBase64: {baseImageCorrect, encoding: 'base64' },
});
I tried to resize or compress an image before uploading to the google cloud storage.
The upload works fine but the resizing does not seem to work.
Here is my code:
const uploadImage = async (file) => new Promise((resolve, reject) => {
let { originalname, buffer } = file
sharp(buffer)
.resize(1800, 948)
.toFormat("jpeg")
.jpeg({ quality: 80 })
.toBuffer()
const blob = bucket.file(originalname.replace(/ /g, "_"))
const blobStream = blob.createWriteStream({
resumable: false
})
blobStream.on('finish', () => {
const publicUrl = format(
`https://storage.googleapis.com/${bucket.name}/${blob.name}`
)
resolve(publicUrl)
}).on('error', () => {
reject(`Unable to upload image, something went wrong`)
})
.end(buffer)
})
I ran into the same issue with a project I was working on. After lots of trial and error I found the following solution. It might not be the most elegant, but it worked for me.
In my upload route function I created a new thumbnail image object with the original file values and passed it as the file parameter to the uploadFile function for google cloud storage.
Inside my upload image route function:
const file = req.file;
const thumbnail = {
fieldname: file.fieldname,
originalname: `thumbnail_${file.originalname}`,
encoding: file.encoding,
mimetype: file.mimetype,
buffer: await sharp(file.buffer).resize({ width: 150 }).toBuffer()
}
const uploadThumbnail = await uploadFile(thumbnail);
My google cloud storage upload file function:
const uploadFile = async (file) => new Promise((resolve, reject) => {
const gcsname = file.originalname;
const bucketFile = bucket.file(gcsname);
const stream = bucketFile.createWriteStream({
resumable: false,
metadata: {
contentType: file.mimetype
}
});
stream.on('error', (err) => {
reject(err);
});
stream.on('finish', (res) => {
resolve({
name: gcsname
});
});
stream.end(file.buffer);
});
I think the problem is with toFormat(). That function does not exist in the Docs. Can you try to remove it and check if it would work?
sharp(buffer)
.resize(1800, 948)
.jpeg({ quality: 80 })
.toBuffer()
Modify the metadata once you have finished uploading the image.
import * as admin from "firebase-admin";
import * as functions from "firebase-functions";
import { log } from "firebase-functions/logger";
import * as sharp from "sharp";
export const uploadFile = functions.https.onCall(async (data, context) => {
const bytes = data.imageData;
const bucket = admin.storage().bucket();
const buffer = Buffer.from(bytes, "base64");
const bufferSharp = await sharp(buffer)
.png()
.resize({ width: 500 })
.toBuffer();
const nombre = "IMAGE_NAME.png";
const fileName = `img/${nombre}.png`;
const fileUpload = bucket.file(fileName);
const uploadStream = fileUpload.createWriteStream();
uploadStream.on("error", async (err) => {
log("Error uploading image", err);
throw new functions.https.HttpsError("unknown", "Error uploading image");
});
uploadStream.on("finish", async () => {
await fileUpload.setMetadata({ contentType: "image/png" });
log("Upload success");
});
uploadStream.end(bufferSharp);
});
This is my cloud function that is supposed to generate a watermarked image and store it in firebase storage everytime an image is uploaded.
exports.generateWatermark = functions.storage
.object()
.onFinalize(async object => {
try {
const fileBucket = object.bucket; // The Storage bucket that contains the file.
const filePath = object.name; // File path in the bucket.
const contentType = object.contentType; // File content type.
const metageneration = object.metageneration; // Number of times metadata has been generated. New objects have a value of 1.
// Exit if this is triggered on a file that is not an image.
if (!contentType.startsWith('image/')) {
return console.log('This is not an image.');
}
// Get the file name.
const fileName = path.basename(filePath);
// Exit if the image is already a watermarked image.
if (fileName.startsWith('watermark_')) {
return console.log('Already a Watermarked image.');
}
if (!filePath.startsWith('pets')) {
return console.log('Not a pet image: ', filePath);
}
// Download file from bucket.
const bucket = admin.storage().bucket(fileBucket);
const tempFilePath = path.join(os.tmpdir(), fileName);
const tempWatermarkPath = path.join(os.tmpdir(), 'watermark.png');
const metadata = {
contentType: contentType,
};
// Generate a watermarked image using Jimp
await bucket.file(filePath).download({destination: tempFilePath});
await bucket
.file('logo/cbs.png')
.download({destination: tempWatermarkPath});
console.log('Image downloaded locally to', tempFilePath, filePath);
await spawn('convert', [
tempFilePath,
'-gravity',
'NorthWest',
'-draw',
`"image Over 10,10,200,200 ${tempWatermarkPath}"`,
tempFilePath,
]);
console.log('Watermarked image created at', tempFilePath);
// We add a 'watermark_' prefix
const watermarkFileName = `watermark_${fileName}`;
const watermarkFilePath = path.join(
path.dirname(filePath),
watermarkFileName,
);
// Uploading the watermarked image.
await bucket.upload(tempFilePath, {
destination: watermarkFilePath,
metadata: metadata,
});
// Once the watermarked image has been uploaded delete the local file to free up disk space.
fs.unlinkSync(tempFilePath);
return fs.unlinkSync(tempWatermarkPath);
} catch (err) {
console.log('GENERATE WATERMARK ERROR: ', err);
throw err;
}
});
The part of the code that errors out is the imagemagick part:
await spawn('convert', [
tempFilePath,
'-gravity',
'NorthWest',
'-draw',
`"image Over 10,10,200,200 ${tempWatermarkPath}"`,
tempFilePath,
]);
This is the error that I'm getting:
Is there a way I could get more info about the error? The error is not even reaching my catch block..
childprocess.spawn uses the observer pattern.
The return value from invoking childprocess.spawn is a ChildProcess object with stdout and stderr which are EventEmitters.
You'll need an extra step to promisify the existing interface before you can await it. For example,
const spawn = (command, args) => new Promise((resolve, reject) => {
const cp = require('child_process').spawn(command, args);
let err = null, out = null;
cp.stdout.on('data', data => out += data.toString());
cp.stdout.on('error', data => err += data.toString());
cp.on('error', data => err += data.toString());
cp.on('close', code => {
(code === 0) ? resolve(out) : reject(err)
});
})
childprocess.execFile on the other hand uses callbacks. This makes it easily promisifiable uses util.promisify function. For example
const util = require('util');
const execFile = util.promisify(require('child_process').execFile);
exports.generateWatermark = functions.storage
.object()
.onFinalize(async object => {
try {
//...
await execFile('convert', [
tempFilePath,
'-gravity',
'NorthWest',
'-draw',
`"image Over 10,10,200,200 ${tempWatermarkPath}"`,
tempFilePath,
]);
//...
} catch (err) {
console.log('GENERATE WATERMARK ERROR: ', err);
throw err;
}
});
On my NodeJS server I download an image that I need to embed in an email. My bucket is NOT public so just using the link will not work, as is not the solution I want for the requirements of either this question or the project.
I'm using a HTML email for this with something like:
<p>Embedded image: <img src="data:image/jpeg;charset=utf-8;base64,{{url}}" /></p>
So I download from S3
s3.getObject(
{ Bucket: "mybucket", Key: "mykey" },
function (error, data) {
if (error != null) {
console.log("Errror" + error)
} else {
console.log("Loaded " + data.ContentLength + " bytes")
and then I'm trying to convert data.body to UTF-8 base 64
I thought something like
"data:image/png;base64," + new String(encoder.encode(data.body), "UTF-8")
But it doesn't seem to work, and I'm struggling to define encoder to be able to do this.
Make sure you are getting image data in data.Body. I think data.Body already a buffer and you can construct src URL as bellow:
// Convert Body from a Buffer to a String
let base64String= data.Body.toString('base64');
let src = "data:image/jpeg;base64,"+base64String;
if you store s3 bucket object in binary format, to received it and convert it to base64 can be done:
const blobToBase64 = (blob) => {
const reader = new FileReader()
reader.readAsDataURL(blob)
return new Promise((rs, rj) => {
reader.onloadend = () => {
rs(reader.result)
}
reader.onerror = rj
})
}
function useImage({s3Link}) {
const [src, setSrc] = React.useState(null)
React.useEffect(() => {
async function query({link}) {
//link is link to s3 bucket URL link e.g
// const link = s3.getSignedUrl('getObject', {
// Bucket: bucketnamw,
// Key: key,
// Expires: 30,
// })
const r = await fetch(link)
const blob = await r.blob()
const base64 = await blobToBase64(blob)
console.log(`base64!`, base64)
setSrc(base64)
}
if (s3Link) {
query({link: s3Link})
}
}, [s3Link, setSrc])
return [{src}]
}
Been trying various methods I've found via google but none seem to work. I'm working with signature-pad, the javascript/html5 canvas solution for eSignatures. I'm trying to save the canvas data as a png to a temporary folder (TempFolder/username/signature.png). To post the the results of toDataURL I'm using XMLHttpRequest. Everything else is Node.js / Express. I left the download in to make sure the dataURL is not corrupted. Since I wasn't able to use body-parser to grab the data from passing it through XMLHttp I instead populate a hidden field with the data then submit it.
Save to Profile Function
saveToServerButton.addEventListener('click', event => {
if (signaturePad.isEmpty()) {
const warning = document.getElementById('message');
warning.value = 'Please provide a signature first.';
} else {
const image = signaturePad.toDataURL().replace(/^data:image\/png;base64,/, '');
const httpRequest = new XMLHttpRequest();
const hiddenInput = document.getElementById('base64Data');
hiddenInput.value = image;
}
});
And the route in Node
const fs = require('fs');
// POST - ESignature Processing
router.post('/eSig/save/', ensureAuthenticated, (req, res) => {
const trimmedData = req.body.base64Data;
console.log(`The trimmed data is: ${trimmedData}`);
const buffer = Buffer.from(trimmedData);
const usernameForFolder = req.user.username;
const userFolder = `${dir}/${usernameForFolder}`;
if (!fs.existsSync(userFolder)){
fs.mkdirSync(userFolder);
fs.writeFile(`${userFolder}/signature.png`, buffer, 'base64', err => {
if (err) throw err;
console.log('saved');
});
res.redirect('../../profile');
} else {
fs.writeFile(`${userFolder}/signature.png`, buffer, 'base64', err => {
if (err) throw err;
console.log('saved');
});
res.redirect('../../profile');
}
});
Figured it out. Here is the working code (buffer's need to know it's 'base64')
Save to Profile Function
saveToServerButton.addEventListener('click', event => {
if (signaturePad.isEmpty()) {
const warning = document.getElementById('message');
warning.value = 'Please provide a signature first.';
} else {
const image = signaturePad.toDataURL().replace(/\s/g, '+').replace(/^data:image\/png;base64,/, '');
const httpRequest = new XMLHttpRequest();
const hiddenInput = document.getElementById('base64Data');
hiddenInput.value = image;
}
});
Route on Node.js to save
// POST - ESignature Processing
router.post('/eSig/save/', ensureAuthenticated, (req, res) => {
const trimmedData = req.body.base64Data;
const buffer = Buffer.from(trimmedData, 'base64');
const usernameForFolder = req.user.username;
const userFolder = `${dir}/${usernameForFolder}`;
if (!fs.existsSync(userFolder)){
fs.mkdirSync(userFolder);
fs.writeFile(`${userFolder}/signature.png`, buffer, 'base64', err => {
if (err) throw err;
console.log('saved');
});
res.redirect('../../profile');
} else {
fs.writeFile(`${userFolder}/signature.png`, buffer, 'base64', err => {
if (err) throw err;
console.log('saved');
});
res.redirect('../../profile');
}
});