I am using html-to-image library to convert div element to image, which is working fine for single div, but if i use it for divs in for loop then it breaks
<div *ngFor="let template of templates; index as i">
<div id="{{template.templateName}}_front_{{i}}" [innerHTML]="(template.templateHtmlFront) | safeHtml">
</div>
<div id="{{template.templateName}}_back_{{i}}"
[innerHTML]="(template.templateHtmlBack) | safeHtml"></div>
</div>
I want to convert each front and back template in an image. so i have written following code in ts file
async createFiles() {
const imageData: any[] = [];
for (let tempIdx = 0; tempIdx < this.templates.length; tempIdx++) {
const containerFront = document.getElementById(`${template.templateName}_front_${tempIdx}`);
//save front template
if (containerFront) {
await htmlToImage.toJpeg(containerFront)
.then((dataUrl) => {
const imageName = `${template.templateId}_front.jpeg`;
const imageBlob: Blob = this.service.dataURItoBlob(dataUrl);
const imageFile = new File([imageBlob], imageName, { type: 'image/jpeg' });
imageData.push(imageFile);
}).catch(function (error) {
console.error('oops, something went wrong!', error);
});
}
//Save back
const containerBack = document.getElementById(`${template.templateName}_back_${tempIdx}`);
if (containerBack) {
await htmlToImage.toJpeg(containerBack)
.then((dataUrl) => {
const imageName = `${template.templateId}_back.jpeg`;
const imageBlob: Blob = this.service.dataURItoBlob(dataUrl);
const imageFile = new File([imageBlob], imageName, { type: 'image/jpeg' });
imageData.push(imgFile);
}).catch(function (error) {
console.error('oops, something went wrong!', error);
});
}
}
}
}
This works fine for max 10 to 15 record, but of templates size is more than 15 then application hangs(or crashes). For 10-15 records also it takes 15mins(which is very slow) Is there any better way to make it work?
Related
Background
Javascript library for Microsoft Office add-ins allows you to get raw content of the DOCX file through getFileAsync() api, which returns a slice of up to 4MB in one go. You keep calling the function using a sliding window approach till you have reed entire content. I need to upload these slices to the server and the join them back to recreate the original DOCX file.
My attempt
I'm using axios on the client-side and busboy-based express-chunked-file-upload middleware on my node server. As I call getFileAsync recursively, I get a raw array of bytes that I then convert to a Blob and append to FormData before posting it to the node server. The entire thing works and I get the slice on the server. However, the chunk that gets written to the disk on the server is much larger than the blob I uploaded, normally of the order of 3 times, so it is obviously not getting what I sent.
My suspicion is that this may have to do with stream encoding, but the node middleware does not expose any options to set encoding.
Here is the current state of code:
Client-side
public sendActiveDocument(uploadAs: string, sliceSize: number): Promise<boolean> {
return new Promise<boolean>((resolve) => {
Office.context.document.getFileAsync(Office.FileType.Compressed,
{ sliceSize: sliceSize },
async (result) => {
if (result.status == Office.AsyncResultStatus.Succeeded) {
// Get the File object from the result.
const myFile = result.value;
const state = {
file: myFile,
filename: uploadAs,
counter: 0,
sliceCount: myFile.sliceCount,
chunkSize: sliceSize
} as getFileState;
console.log("Getting file of " + myFile.size + " bytes");
const hash = makeId(12)
this.getSlice(state, hash).then(resolve(true))
} else {
resolve(false)
}
})
})
}
private async getSlice(state: getFileState, fileHash: string): Promise<boolean> {
const result = await this.getSliceAsyncPromise(state.file, state.counter)
if (result.status == Office.AsyncResultStatus.Succeeded) {
const data = result.value.data;
if (data) {
const formData = new FormData();
formData.append("file", new Blob([data]), state.filename);
const boundary = makeId(12);
const start = state.counter * state.chunkSize
const end = (state.counter + 1) * state.chunkSize
const total = state.file.size
return await Axios.post('/upload', formData, {
headers: {
"Content-Type": `multipart/form-data; boundary=${boundary}`,
"file-chunk-id": fileHash,
"file-chunk-size": state.chunkSize,
"Content-Range": 'bytes ' + start + '-' + end + '/' + total,
},
}).then(async res => {
if (res.status === 200) {
state.counter++;
if (state.counter < state.sliceCount) {
return await this.getSlice(state, fileHash);
}
else {
this.closeFile(state);
return true
}
}
else {
return false
}
}).catch(err => {
console.log(err)
this.closeFile(state)
return false
})
} else {
return false
}
}
else {
console.log(result.status);
return false
}
}
private getSliceAsyncPromise(file: Office.File, sliceNumber: number): Promise<Office.AsyncResult<Office.Slice>> {
return new Promise(function (resolve) {
file.getSliceAsync(sliceNumber, result => resolve(result))
})
}
Server-side
This code is totally from the npm package (link above), so I'm not supposed to change anything in here, but still for reference:
makeMiddleware = () => {
return (req, res, next) => {
const busboy = new Busboy({ headers: req.headers });
busboy.on('file', (fieldName, file, filename, _0, _1) => {
if (this.fileField !== fieldName) { // Current field is not handled.
return next();
}
const chunkSize = req.headers[this.chunkSizeHeader] || 500000; // Default: 500Kb.
const chunkId = req.headers[this.chunkIdHeader] || 'unique-file-id'; // If not specified, will reuse same chunk id.
// NOTE: Using the same chunk id for multiple file uploads in parallel will corrupt the result.
const contentRangeHeader = req.headers['content-range'];
let contentRange;
const errorMessage = util.format(
'Invalid Content-Range header: %s', contentRangeHeader
);
try {
contentRange = parse(contentRangeHeader);
} catch (err) {
return next(new Error(errorMessage));
}
if (!contentRange) {
return next(new Error(errorMessage));
}
const part = contentRange.start / chunkSize;
const partFilename = util.format('%i.part', part);
const tmpDir = util.format('/tmp/%s', chunkId);
this._makeSureDirExists(tmpDir);
const partPath = path.join(tmpDir, partFilename);
const writableStream = fs.createWriteStream(partPath);
file.pipe(writableStream);
file.on('end', () => {
req.filePart = part;
if (this._isLastPart(contentRange)) {
req.isLastPart = true;
this._buildOriginalFile(chunkId, chunkSize, contentRange, filename).then(() => {
next();
}).catch(_ => {
const errorMessage = 'Failed merging parts.';
next(new Error(errorMessage));
});
} else {
req.isLastPart = false;
next();
}
});
});
req.pipe(busboy);
};
}
Update
So it looks like I have found the problem at least. busboy appears to be writing my array of bytes as text in the output file. I get 80,75,3,4,20,0,6,0,8,0,0,0,33,0,44,25 (as text) when I upload the array of bytes [80,75,3,4,20,0,6,0,8,0,0,0,33,0,44,25]. Now need to figure out how to force it to write it as a binary stream.
Figured out. Just in case it helps anyone, there was no problem with busboy or office.js or axios. I just had to convert the incoming chunk of data to Uint8Array before creating a blob from it. So instead of:
formData.append("file", new Blob([data]), state.filename);
like this:
const blob = new Blob([ new Uint8Array(data) ])
formData.append("file", blob, state.filename);
And it worked like a charm.
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:
I am making an API that gets a list of image names, then it has to download them one by one from S3 bucket and then send them all as a response.
The issue is that my images are being uploaded but it seems that when I put them in a list as base64 and then try to send the list then the list just comes up empty.
const getImagesById = async (req, res) => {
const { id } = req.params;
const imagesSet = new Map();
try {
const documentFromDB = await document.findOne({ id });
documentFromDB.devices.forEach((device) => {
const images = new Set();
device.images.forEach(item => images.add(downloadFromS3(item)))
imagesSet.set(device.name, JSON.stringify(mapToObj(images))) // tried adding just images also but neither works
});
res.status(200).json(JSON.stringify(mapToObj(imagesSet)));
} catch (e) {
console.log(`An error occurred : ${e.message}`);
res.status(500)
.send(e.message);
}
};
function mapToObj(inputMap) {
let obj = {};
inputMap.forEach(function(value, key){
obj[key] = value
});
return obj;
}
And this is how I get images from S3:
const downloadFromS3 = async (imageName) => {
try {
const image = await S3Utils.downloadFile(BUCKET_NAME, imageName);
if (image.stack) {
return null;
}
const imageBase64 = image.Body.toString('base64');
return imageBase64;
} catch (e) {
console.log(`An error occurred while downloading : ${e.message}`);
throw e;
}
};
This is the response I am getting at the moment:
"{\"{ name: 'Martin'}\":\"{\\\"[object Promise]\\\":{}}\"}"
What I am trying to do is get a lits of device names, map them in a Map as key with value as the base64 list of images and then send it all in a response to the UI to show the images with the names.
What am I doing wrong here?
You just need to add await before call the downloadFromS3 function, consequently changing all the above functions.
const getImagesById = async (req, res) => {
const { id } = req.params;
const imagesSet = new Map();
try {
const documentFromDB = await document.findOne({ id });
await Promise.all(documentFromDB.devices.map(async (device) => {
const images = new Set();
await Promise.all(device.images.map(async item => images.add(await downloadFromS3(item))))
imagesSet.set(device.name, JSON.stringify(mapToObj(images))) // tried adding just images also but neither works
}));
res.status(200).json(JSON.stringify(mapToObj(imagesSet)));
} catch (e) {
console.log(`An error occurred : ${e.message}`);
res.status(500)
.send(e.message);
}
};
function mapToObj(inputMap) {
let obj = {};
inputMap.forEach(function(value, key){
obj[key] = value
});
return obj;
}
Heyy, i am trying to upload a cropped image to firebase.
I would prefer to use the ionic native "image-picker" and "Crop".
I really dont know how to upload the image after cropping it, because it only returns the path of the new image.
I have already tryed something like this. This worked, but i was not able to crop the image. But as i mentioned, i would prefer using the native tools anyways.
export interface UploadData {
name: string;
filepath: string;
size: number;
}
uploadFile(event: FileList) {
// The File object
const file = event.item(0);
// Validation for Images Only
if (file.type.split('/')[0] !== 'image') {
console.error('unsupported file');
return;
}
// The storage path
const path = `whatever/${new Date().getTime()}_${file.name}`;
// File reference
const fileRef = this.storage.ref(path);
// The main task
this.task = this.storage.upload(path, file, { customMetadata });
this.snapshot = this.task.snapshotChanges().pipe(
finalize(() => {
// Get uploaded file storage path
this.UploadedFileURL = fileRef.getDownloadURL();
this.UploadedFileURL.subscribe(resp => {
this.addImagetoDB({
name: file.name,
filepath: resp,
size: this.fileSize
});
}, error => {
console.error(error);
});
}),
tap(snap => {
this.fileSize = snap.totalBytes;
})
);
}
addImagetoDB(image: UploadData) {
const id = this.db.createId();
// Set document id with value in database
this.imageCollection.doc(id).set(image).then(resp => {
console.log(resp);
}).catch(error => {
console.log('error ' + error);
});
}
}
This is how i would like to do it. But i really have no idea, how to upload it at this point.
pickImage() {
this.imagePicker.getPictures(this.imagePickerOptions).then((results)
=> {
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < results.length; i++) {
this.cropImage(results[i]);
}
}, (err) => {
alert(err);
});
}
cropImage(imgPath) {
this.crop.crop(imgPath, { quality: 50 })
.then(
newPath => {
// ?????
},
error => {
alert('Error cropping image' + error);
}
);
}
Sorry, i am very new to this stuff.
Thanks for your help :)
It seems that you might be able to do this without the crop feature being needed.
These are the options according to the docs:
options = {
// Android only. Max images to be selected, defaults to 15. If this is set to 1, upon
// selection of a single image, the plugin will return it.
maximumImagesCount: int,
// max width and height to allow the images to be. Will keep aspect
// ratio no matter what. So if both are 800, the returned image
// will be at most 800 pixels wide and 800 pixels tall. If the width is
// 800 and height 0 the image will be 800 pixels wide if the source
// is at least that wide.
width: int,
height: int,
// quality of resized image, defaults to 100
quality: int (0-100),
// output type, defaults to FILE_URIs.
// available options are
// window.imagePicker.OutputType.FILE_URI (0) or
// window.imagePicker.OutputType.BASE64_STRING (1)
outputType: int
};
So you could use:
options = {
maximumImagesCount: 3,
width: 800,
height: 600,
quality: 50,
outputType: 1
};
From what I've been researching you could then put the image into Firebase Storage using:
storageRef.putString("Your base64 string substring variable", 'base64');
I'm not sure if this is enough to get you fixed up but I thought I would post what I had found anyway.
I just tried this, but it dosnĀ“t work too. I have no idea why...
constructor(private imagePicker: ImagePicker, private crop: Crop,
private file: File) {
let storageDb = firebase.storage();
this.storageRef = storageDb.ref();
}
pickImage() {
this.imagePicker.getPictures(this.imagePickerOptions).then((results)
=> {
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < results.length; i++) {
this.cropImage(results[i]);
}
}, (err) => {
alert(err);
});
}
cropImage(imgPath) {
this.crop.crop(imgPath, { quality: 50 })
.then(
newPath => {
try {
let n = newPath.lastIndexOf("/");
let x = newPath.lastIndexOf("g");
let nameFile = newPath.substring(n + 1, x + 1);
this.file.readAsArrayBuffer(newPath, nameFile).then((res) => {
let blob = new Blob([res], { type: "image/jpeg" });
var uploadTask = this.storageRef.child('images/' + this.event.id).put(blob);
uploadTask.on('state_changed', (snapshot) => {
let url = uploadTask.snapshot.downloadURL;
this.croppedImagepath = url;
}, (error) => {
alert("error: " + error);
}, () => {
alert("uploaded");
let url = uploadTask.snapshot.downloadURL;
this.croppedImagepath = url;
})
})
}
catch (z) {
alert('error beim erstellen des blobs' + z);
}
},
error => {
alert('Error cropping image' + error);
}
);
}
Problem
I am trying to create an app with react native and firebase. One of the features I would like for this app is the ability to upload images. I am having some trouble uploading the images to firebase storage though. I am using expo's image picker to find the path of the image that the user wants to upload, but once I have the path I don't know how to convert that to something I can upload to firebase.
Can somebody help me convert the path of an image to something I can upload to firebase storage with react native?
What I've tried
I tried using:
_pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
MediaTypeOptions: 'Images',
quality: 0.4,
_uploadAsByteArray = async (pickerResultAsByteArray, progressCallback) => {
try {
var metadata = {
contentType: 'image/jpeg',
};
var storageRef = firebase.storage().ref();
var ref = storageRef.child('images/'+expoID+'/'+this.state.time)
let uploadTask = ref.put(pickerResultAsByteArray, metadata)
uploadTask.on('state_changed', function (snapshot) {
progressCallback && progressCallback(snapshot.bytesTransferred / snapshot.totalBytes)
var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log('Upload is ' + progress + '% done');
}, function (error) {
console.log("in _uploadAsByteArray ", error)
}, function () {
var downloadURL = uploadTask.snapshot.downloadURL;
console.log("_uploadAsByteArray ", uploadTask.snapshot.downloadURL)
this.setState({imageUploaded:true})
});
} catch (ee) {
console.log("when trying to load _uploadAsByteArray ", ee)
}
}
convertToByteArray = (input) => {
var binary_string = this.atob(input);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes
}
atob = (input) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let str = input.replace(/=+$/, '');
let output = '';
if (str.length % 4 == 1) {
throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
}
for (let bc = 0, bs = 0, buffer, i = 0;
buffer = str.charAt(i++);
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
buffer = chars.indexOf(buffer);
}
return output;
}
uploadImage(bsfdata){
this.setState({imageUploaded:false})
this._uploadAsByteArray(this.convertToByteArray(bsfdata), (progress) => {
this.setState({ progress:progress })
})
}
base64:true,
});
/* if (!result.cancelled) {
this.setState({ image: result.uri });
let formData = new FormData();
formData.append('photo', {
uri,
name: `photo.${fileType}`,
type: `image/${fileType}`,
});}*/
this.uploadImage(result.base64);
};
}
I've tried it with the commented code added, which doesn't upload anything, and I've tried it with how the code is now, which gives me the error Can currently only create a Blob from other Blobs, and the uploading progress never gets above 0%.
If you are using expo (>=26), then you can do it easily with the following lines of code.
uploadImage = async(imageUri) => {
const response = await fetch(imageUri);
const blob = await response.blob();
var ref = firebase.storage().ref().child("image.jpg");
return ref.put(blob);
}
Reference: https://youtu.be/KkZckepfm2Q
Refer this link - https://github.com/dailydrip/react-native-firebase-storage/blob/master/src/App.js#L43-L69
Following block of code is working fine.
uploadImage(uri, mime = 'application/octet-stream') {
return new Promise((resolve, reject) => {
const uploadUri = Platform.OS === 'ios' ? uri.replace('file://', '') : uri
let uploadBlob = null
const imageRef = FirebaseClient.storage().ref('images').child('image_001')
fs.readFile(uploadUri, 'base64')
.then((data) => {
return Blob.build(data, { type: `${mime};BASE64` })
})
.then((blob) => {
uploadBlob = blob
return imageRef.put(blob, { contentType: mime })
})
.then(() => {
uploadBlob.close()
return imageRef.getDownloadURL()
})
.then((url) => {
resolve(url)
})
.catch((error) => {
reject(error)
})
})
}
You need to install rn-fetch-blob module:
npm install --save rn-fetch-blob
Then, do the following:
import RNFetchBlob from 'rn-fetch-blob';
const Blob = RNFetchBlob.polyfill.Blob;
const fs = RNFetchBlob.fs;
window.XMLHttpRequest = RNFetchBlob.polyfill.XMLHttpRequest;
window.Blob = Blob;
function uploadImage(path) {
const imageFile = RNFetchBlob.wrap(path);
// 'path/to/image' is where you wish to put your image in
// the database, if you would like to put it in the folder
// 'subfolder' inside 'mainFolder' and name it 'myImage', just
// replace it with 'mainFolder/subfolder/myImage'
const ref = firebase.storage().ref('path/to/image');
var uploadBlob = null;
Blob.build(imageFile, { type: 'image/jpg;' })
.then((imageBlob) => {
uploadBlob = imageBlob;
return ref.put(imageBlob, { contentType: 'image/jpg' });
})
.then(() => {
uploadBlob.close();
return ref.getDownloadURL();
})
.((url) => {
// do something with the url if you wish to
})
.catch(() => {
dispatch({
type: UPDATE_PROFILE_INFO_FAIL,
payload: 'Unable to upload profile picture, please try again'
});
});
}
Please do ask if there's any part of the code that you don't understand. To upload multiple images, simply wrap this code with a for loop. Or if you want to make sure that every image is uploaded without any error, use Promise
Not sure whom this might help, but if you're using MediaLibrary to load images from the gallery, then the uri comes in the format of uri = file:///storage/emulated/0/DCIM/Camera/filename.jpg
In this case, using fetch(uri) didn't help me get the blob.
But if you use fetch(uri.replace("file:///","file:/")) and then follow #sriteja Sugoor's answer, you'll be able to upload the file blob.
const Blob = RNFetchBlob.polyfill.Blob;
const fs = RNFetchBlob.fs;
let uploadBlob;
await fs
.readFile(params?.file.path, 'base64')
.then((data) => {
return Blob.build(data, {type: `BASE64`});
})
.then((blob) => {
uploadBlob = blob;
console.log(uploadBlob, 'uploadBlob');
});