Merge created PDF with existing local PDF in ReactJS - reactjs

I create a PDF in ReactJS using react-pdf/renderer and download it using file-saver.
Here is my code that creates the PDF and downloads it:
const LazyDownloadPDFButton = (number, date, totalHours, formattedHours) => (
<Button
className={classes.download}
onClick={
async () => {
const doc = <InvoicePDF number={number} date={date} totalHours={totalHours} formattedHours={formattedHours} />
const asPdf = pdf()
asPdf.updateContainer(doc)
const blob = await asPdf.toBlob()
saveAs(blob, `PDF${number}.pdf`)
}}>
Download
</Button>
)
where InvoicePDF is a separate component that renders the PDF pages with the necessary arguments, as in react-pdf/renderer documentation page.
Before download the actual PDF I have to merge it with another existing PDF that will be choose from computer drive. To do that I have the next code snippet:
fileRef = useRef()
<Button onClick={() => fileRef.current.click()}>
Upload file
<input
ref={fileRef}
type='file'
style={{ display: 'none' }}
/>
</Button>
Which returns me the details of the file.
I tried to updateContainer with this selected file, but there are errors.
How this new file should be merged with the InvoicePDF that is created?
In the meantime, I tried to create my last blob from arrayBuffers like this:
This is the function that concatenates the created PDF with the selected PDF and it returns the correct sum.
function concatArrayBuffers(buffer1, buffer2) {
if (!buffer1) {
return buffer2;
} else if (!buffer2) {
return buffer1;
}
var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
};
And my method now have a finalBlob that is created with arrayBuffers but the problem is that the resulted PDF will always contain just the content of the second arrayBuffer (which is either the selected pdf or the created pdf)
const LazyDownloadPDFButton = (number, date, totalHours, formattedHours) => (
<Button
className={classes.download}
onClick={
async () => {
const doc = <InvoicePDF number={number} date={date} totalHours={totalHours} formattedHours={formattedHours} />
const asPdf = pdf()
asPdf.updateContainer(doc)
const initialBlob = await new Blob([fileRef.current.files[0]], { type: 'application/pdf' }).arrayBuffer()
const blob = await (await asPdf.toBlob()).arrayBuffer()
const finalArrayBuffer = concatArrayBuffers(initialBlob, blob)
const finalBlob = new Blob([finalArrayBuffer], { type: 'application/pdf' })
saveAs(finalBlob, `PDF${number}.pdf`)
}}
>
Download
</Button>
)

Just A Simple Solution Made By me...
https://github.com/ManasMadan/pdf-actions
https://www.npmjs.com/package/pdf-actions
import { createPDF,pdfArrayToBlob, mergePDF } from "pdf-actions";
// Async Function To Merge PDF Files Uploaded Using The Input Tag in HTML
const mergePDFHandler = async (files) => {
// Converting File Object Array To PDF Document Array
files.forEach((file)=>await createPDF.PDFDocumentFromFile(file))
// Merging The PDF Files to A PDFDocument
const mergedPDFDocument = await mergePDF(files)
// Converting The Merged Document to Unit8Array
const mergedPdfFile = await mergedPDFDocument.save();
// Saving The File To Disk
const pdfBlob = pdfArrayToBlob(mergedPdfFile);
};

Solution
After some research and a lot of failed tries I came to an answer on how to merge the PDFs in the correct order, and bonus add an image (in my case a signature) on every page of the final PDF.
This is the final code:
function base64toBlob(base64Data, contentType) {
contentType = contentType || '';
var sliceSize = 1024;
var byteCharacters = atob(base64Data);
var bytesLength = byteCharacters.length;
var slicesCount = Math.ceil(bytesLength / sliceSize);
var byteArrays = new Array(slicesCount);
for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
var begin = sliceIndex * sliceSize;
var end = Math.min(begin + sliceSize, bytesLength);
var bytes = new Array(end - begin);
for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
bytes[i] = byteCharacters[offset].charCodeAt(0);
}
byteArrays[sliceIndex] = new Uint8Array(bytes);
}
return new Blob(byteArrays, { type: contentType });
}
async function mergeBetweenPDF(pdfFileList, number) {
const doc = await PDFDocument.create()
const getUserSignature = () => {
switch (selectedUser.id) {
case 1:
return FirstImage
case 2:
return SecondImage
default:
return null
}
}
const pngURL = getUserSignature()
const pngImageBytes = pngURL ? await fetch(pngURL).then((res) => res.arrayBuffer()) : null
const pngImage = pngURL ? await doc.embedPng(pngImageBytes) : null
const pngDims = pngURL ? pngImage.scale(0.5) : null
const initialPDF = await PDFDocument.load(pdfFileList[0])
const appendixPDF = await PDFDocument.load(pdfFileList[1])
const initialPDFPages = await doc.copyPages(initialPDF, initialPDF.getPageIndices())
for (const page of initialPDFPages) {
if (pngURL) {
page.drawImage(pngImage, {
x: page.getWidth() / 2 - pngDims.width / 2 + 75,
y: page.getHeight() / 2 - pngDims.height,
width: pngDims.width,
height: pngDims.height,
});
}
doc.addPage(page)
}
const appendixPDFPages = await doc.copyPages(appendixPDF, appendixPDF.getPageIndices())
for (const page of appendixPDFPages) {
if (pngURL) {
page.drawImage(pngImage, {
x: page.getWidth() / 2 - pngDims.width / 2 + 75,
y: page.getHeight() / 2 - pngDims.height,
width: pngDims.width,
height: pngDims.height,
});
}
doc.addPage(page)
}
const base64 = await doc.saveAsBase64()
const bufferArray = base64toBlob(base64, 'application/pdf')
const blob = new Blob([bufferArray], { type: 'application/pdf' })
saveAs(blob, `Appendix${number}.pdf`)
}
const LazyDownloadPDFButton = (number, date, totalHours, formattedHours) => (
<Button
className={classes.download}
onClick={
async () => {
const doc = <InvoicePDF number={number} date={date} totalHours={totalHours} formattedHours={formattedHours} />
const asPdf = pdf()
asPdf.updateContainer(doc)
let initialBlob = await new Blob([fileRef.current.files[0]], { type: 'application/pdf' }).arrayBuffer()
let appendixBlob = await (await asPdf.toBlob()).arrayBuffer()
mergeBetweenPDF([initialBlob, appendixBlob], number)
}}
>
Download
</Button>
)
So the LazyDownloadPDFButton is my button that request the respective parameters to create the final PDF. InvoicePDF is my created PDF with the parameters, and initialBlob is the PDF that I upload on my page, which requires to be the first in merged PDF, and appendixBlob is the created PDF that will be attached to initialBlob.
In mergeBetweenPDF I am using pdf-lib library to create the final document, where I create the image, take the 2 initial PDFs that are send, looping them, add the image on every page, and then add every page to the final doc which will be downloaded.
Hope one day this will help someone.

Related

How can I check if the data_URL is returning an image of video ? - Firebase & Next.js/React

The image is uploaded to firebase and returned as a data_URL that looks like this:
https://firebasestorage.googleapis.com/v0/b/app_name/o/posts%2postId?alt=media&token=token
I am trying to check if the file type is a video or an image, then return a div depending on the "mediaType". Because firebase storage doesn't include the file extension in the url, it is difficult to determine the file type.
First attempt:
const [mediaType, setMediaType] = useState(null);
useEffect(() => {
if (postImage) {
const storageRef = firebase.storage().ref();
storageRef.child(postImage).getDownloadURL().then(url => {
fetch(url)
.then(res => res.blob())
.then(blob => {
let type = blob.type;
if (type.startsWith("image")) {
setMediaType("image");
} else if (type.startsWith("video")) {
setMediaType("video");
} else {
setMediaType("other");
console.log("Unknown file format: " + type);
}
});
});
}
}, [postImage]);
Second attempt:
const handleFile = async (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = async (e) => {
const dataURL = e.target.result;
if (dataURL.startsWith('data:image/')) {
setMediaType('image');
setDataUrl(dataURL);
console.log("Image: " + dataURL);
} else if (dataURL.startsWith('data:video/')) {
setMediaType('video');
setDataUrl(dataURL);
console.log("Video: " + dataURL);
} else {
let response = await fetch(dataURL);
let type = response.headers.get("Content-Type");
if (type.startsWith("image")) {
setMediaType("image");
setDataUrl(dataURL);
} else if (type.startsWith("video")) {
setMediaType("video");
setDataUrl(dataURL);
} else {
setMediaType("other");
console.log("Unknown file format: " + type);
}
}
}
reader.readAsDataURL(file);
}
The div inside my return statement:
<div className="w-full px-3">
{mediaType === 'image' ? <img className="shadow-md w-full" src={postImage || 'default-image.jpg'} alt="" /> : null}
{mediaType === 'video' ? <ReactPlayer layout="fill" url={postImage} config={{file:{attributes:{controlsList:'nodownload'}}}} controls onContextMenu={e => e.preventDefault()}/> : null}
{mediaType === 'other' ? <p>File is not an image or video</p> : null}
</div>
What I would do is add metadata while uploading the file to firebase. You can check the documentation to see how:
//This is a metadata, you can customize these
//as you can see the content type is set to be image/jpeg
var newMetadata = {
cacheControl: 'public,max-age=300',
contentType: 'image/jpeg'
.........
.........
};
You will use this metadata while uploading the file using:
storageRef.updateMetadata(newMetadata).......
And when reading the file read out the metadata that you set to the file to for example detect its type:
storageRef.getMetadata().then((metadata) => {
//use this metadata to know the type here.......
})
Hope this gives you an idea of what to do.

How to upload multiple audio file in react but sending only 3 POST request

I want to select 100 audio file at a time but want to hit only 3 api call at a time. Once these 3 files uploaded (pass or fail) then only other 3 api request will be sent.
Basically I am providing a input field of file type:
<input type="file" multiple name="file" className="myform"
onChange={handleFileChange}
accept="audio/wav"
/>
and I am storing it as array into a state.
Below this I am providing an UPLOAD button.
When user hit on upload, I want to send 3 POST request using axios. once all 3 done either fail or pass then only next 3 should go.
You can do this by iterating the FileList collection in groups of 3 and sending the requests in parallel using Promise.allSettled().
Simply because I cannot recommend Axios, here's a version using the Fetch API
const BATCH_SIZE = 3;
const [fileList, setFileList] = useState([]);
const [uploading, setUploading] = useState(false);
const handleFileChange = (e) => {
setFileList(Array.from(e.target.files)); // just a guess
};
const handleUploadClick = async (e) => {
e.preventDefault();
setUploading(true);
const files = [...fileList]; // avoid mutation during long uploading process
for (let i = 0; i < files.length; i += BATCH_SIZE) {
const result = await Promise.allSettled(
files.slice(i, i + BATCH_SIZE).map(async (file) => {
const body = new FormData();
body.append("file", file);
const res = await fetch(UPLOAD_URL, { method: "POST", body });
return res.ok ? res : Promise.reject(res);
})
);
const passed = result.filter(({ status }) => status === "fulfilled");
console.log(
`Batch ${i + 1}: ${
passed.length
} of ${BATCH_SIZE} requests uploaded successfully`
);
}
setUploading(false);
};
Promise.allSettled() will let you continue after each set of 3 are uploaded, whether they pass or fail.
This method makes 3 separate requests with 1 file each.
With Axios, it would look like this (just replacing the for loop)
for (let i = 0; i < files.length; i += BATCH_SIZE) {
const result = await Promise.allSettled(
files
.slice(i, i + BATCH_SIZE)
.map((file) => axios.postForm(UPLOAD_URL, { file }))
);
const passed = result.filter(({ status }) => status === "fulfilled");
console.log(
`Batch ${i + 1}: ${
passed.length
} of ${BATCH_SIZE} requests uploaded successfully`
);
}
Axios' postForm() method is available from v1.0.0. See https://github.com/axios/axios#files-posting
If you want to send 3 files in a single request, it would look like this for Fetch
for (let i = 0; i < files.length; i += BATCH_SIZE) {
const body = new FormData();
files.slice(i, i + BATCH_SIZE).forEach((file) => {
body.append("file", file); // use "file[]" for the first arg if required
});
try {
const res = await fetch(UPLOAD_URL, { method: "POST", body });
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`);
}
console.log(`Batch ${i + 1} passed`);
} catch (err) {
console.warn(`Batch ${i + 1} failed`, err);
}
}
and this for Axios
for (let i = 0; i < files.length; i += BATCH_SIZE) {
try {
await axios.postForm(
{
file: files.slice(i, i + BATCH_SIZE),
},
{
formSerializer: {
indexes: null, // set to false if you need "[]" added
},
}
);
console.log(`Batch ${i + 1} passed`);
} catch (err) {
console.warn(`Batch ${i + 1} failed`, err.response?.data);
}
}
You can use a combination of JavaScript's for loop and Promise.all functions to achieve this. First, you will need to divide your files array into chunks of 3. You can do this using a for loop and the slice method. Next, you can use Promise.all to send all the requests in parallel, and only move on to the next set of requests once all the promises in the current set have been resolved. Here's some sample code that demonstrates this approach:
const chunkSize = 3;
for (let i = 0; i < files.length; i += chunkSize) {
const fileChunk = files.slice(i, i + chunkSize);
const promises = fileChunk.map(file => {
return axios.post('/api/upload', { file });
});
await Promise.all(promises);
}
This will send 3 post request at a time and will wait until all the request are completed before sending another 3 api request.
You can also use useState hook with useEffect to set the state of files that are uploaded and use a variable to keep track of number of files uploaded.
const [uploadedFiles, setUploadedFiles] = useState([]);
const [uploadCount, setUploadCount] = useState(0);
useEffect(() => {
if (uploadCount === files.length) {
// all files have been uploaded
return;
}
const chunkSize = 3;
const fileChunk = files.slice(uploadCount, uploadCount + chunkSize);
const promises = fileChunk.map(file => {
return axios.post('/api/upload', { file });
});
Promise.all(promises).then(responses => {
setUploadedFiles([...uploadedFiles, ...responses]);
setUploadCount(uploadCount + chunkSize);
});
}, [uploadCount]);
This code will work for you.

Difficulty in displaying image after passing over blob or datauri to image tag

I'm trying to get an image (served to the client from an express server) to display. I've received the data from the server and have parsed this from binary, to blob, to datauri. I, however, am still getting a broken image on my page.
Below is my code:
const [imageUrl, setImageUrl] = useState('');
const [profilePic, setProfilePic] = useState('');
const [errors, setErrors] = useState('');
const binaryToBase64 = () => {
contentType = contentType || '';
var sliceSize = 1024;
var byteCharacters = base64Data.toString("base64");
var bytesLength = byteCharacters.length;
var slicesCount = Math.ceil(bytesLength / sliceSize);
var byteArrays = new Array(slicesCount);
for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
var begin = sliceIndex * sliceSize;
var end = Math.min(begin + sliceSize, bytesLength);
var bytes = new Array(end - begin);
for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
bytes[i] = byteCharacters[offset].charCodeAt(0);
}
byteArrays[sliceIndex] = new Uint8Array(bytes);
}
return new Blob(byteArrays, { type: contentType });
}
useEffect(() => {
// Get all customer media
let tempPhoto = '';
await actions.getMediaReference(customerId)
.then(resp => {
// Set profile pic via reference from DB
tempPhoto = resp.data.find(({ isprofile }) => isprofile);
})
.catch(err => errors.push(err));
await actions.getMediaSource(tempPhoto.key)
.then(resp => {
// console.log("specific data: ", typeof resp.data); // Not sure of the format: ����
const blob = binaryToBase64(resp.request.response, "image/jpg"); // convert to base 64 - I assume I'm getting binary code
const url = URL.createObjectURL(blob);
console.log('blob: ', blob); // prints this which also isn't accepted: blob:http://localhost:3000/1231233234
console.log('url: ', url);
// setImageUrl(url)
// My thinking is that it needed to be further processed (got this from another stackoverflow page)
fileToDataUri(blob)
.then(dataUri => {
setImageUrl(dataUri)
})
setProfilePic({ ...profilePic, image : resp.data });
})
.catch(err => errors.push(err));
}
return(
<>
<img src={imageUrl} />
</>
)
Trying this api out via Postman works - I get the image after a very tiny delay. I can't say that I see the error here but I've tried a few things. After running the base64 code in a converter I can see that something isn't translating correctly. I feel like this shouldn't be that complicated and that I've over-complicated the solution so any help/advice would be much appreciated!
Thanks

React Ant Design multiple files upload doesn't work

I'm in the process of sending multiple files from "React.js" by formData.append() to a backend.
At the backend(Spring boot), I was able to see that multiple files were saved well with postman.
The problem occurred in React.
(I'm using "Ant Design" that is React UI Library.)
Below is the source that append files to formdata with extra data.
const formData = new FormData();
formData.append('webtoonId', this.state.selectedToonId);
formData.append('epiTitle', this.state.epiTitle);
formData.append('eFile', this.state.thumbnail[0].originFileObj);
for( let i = 0; i< this.state.mains.length ; i++){
formData.append('mFiles', this.state.mains[i].originFileObj);
}
uploadEpi(formData)
uploadEpi() is POST API.
Below is about state.
this.state = {
toons: [],
epiTitle :'',
thumbnail : [],
mains : [],
selectedToonID : ''
}
When I submit, Text and single file are stored in the DB normally, but only multiple files cannot be saved.
There was no error. Just multiple files didn't be saved.
The state "mains" is configured as shown below.
I guess it's because I'm using Ant Design incorrectly.
(Ant Design : https://ant.design/components/upload/)
Why I guessed so, because when I add multiple attribute to <Dragger> like below,
<Dragger onChange={this.onChangeMain} beforeUpload={() => false} multiple={true}>
the state "mains" multiple files became undefined.
Below is onChange={this.onChangeMain}
onChangeMain=({ fileList })=> {
this.setState({ mains : fileList }, function(){
console.log(this.state)
});
}
The bottom line is, I want to know how to upload multiple files through <Upload> (or <Dragger>) in "React Ant Design."
I don't know what should I do.
this is my github about this project.
I'd appreciate with your help. thx.
const [loading, setLoading] = useState<boolean>(false);
const [fileList, setFileList] = useState<any[]>([]);
const [/* fileListBase64 */, setFileListBase64] = useState<any[]>([]);
const propsUpload = {
onRemove: (file:any) => {
const index = fileList.indexOf(file);
const newFileList:any = fileList.slice();
newFileList.splice(index, 1);
return setFileList(newFileList)
},
beforeUpload: (file:any) => {
setFileList([...fileList, file]);
return false;
},
onChange(info:any) {
setLoading(true);
const listFiles = info.fileList;
setFileList(listFiles);
const newArrayFiles = listFiles.map((file:any) => file.originFileObj? (file.originFileObj) : file );
const anAsyncFunction = async (item:any) => {
return convertBase64(item)
}
const getData = async () => {
return Promise.all(newArrayFiles.map((item:any) => anAsyncFunction(item)))
}
getData().then(data => {
/* setFileSend(data) */
setFileListBase64(data);
setLoading(false);
// console.log(data);
});
},
directory: true,
fileList: fileList,
};
const convertBase64 = (file:File) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.readAsDataURL(file)
fileReader.onload = () => {
resolve(fileReader?.result);
}
fileReader.onerror = (error) => {
reject(error);
}
})
}
const handleDeleteListFiles = () => {
setFileList([]);
setFileListBase64([]);
}
It seems like you are overriding the value of mFiles.
const formData = new FormData();
formData.append('webtoonId', this.state.selectedToonId);
formData.append('epiTitle', this.state.epiTitle);
formData.append('eFile', this.state.thumbnail[0].originFileObj);
let mFiles = [];
for( let i = 0; i< this.state.mains.length ; i++){
mFiles[i] = this.state.mains[i].originFileObj;
}
formData.append('mFiles', mFiles)
uploadEpi(formData)
Maybe this can work: formData.append('mFiles[]', mFiles)
If you add [] to the string it should not overwrite but add to the array

Firebase upload multiple files and get status

I have a React form where the user can upload multiple files. These are stored in fileList
async function uploadFiles(id) {
try {
const meta = await storageUploadFile(fileList, id);
console.log(meta);
} catch (e) {
console.log(e);
}
}
This calls my helper function that uploads the files to Firebase
export const storageUploadFile = function(files, id) {
const user = firebase.auth().currentUser.uid;
return Promise.all(
files.map((file) => {
return storage.child(`designs/${user}/${id}/${file.name}`).put(file)
})
)
};
What I'd like is on calling uploadFiles, get the total filesize of all items, and then show the overall progress.
At the moment, my code is only returning the file status in an array on completion
[
{bytesTransferred: 485561, totalBytes: 485561, state: "success"},
{bytesTransferred: 656289, totalBytes: 656289, state: "success"}
]
This is the way i do it:
import Deferred from 'es6-deferred';
export const storageUploadFile = function(files, id) {
const user = firebase.auth().currentUser.uid;
// To track the remaining files
let itemsCount = files.length;
// To store our files refs
const thumbRef = [];
// Our main tasks
const tumbUploadTask = [];
// This will store our primses
const thumbCompleter = [];
for (let i = 0; i < files.length; i += 1) {
thumbRef[i] = storage.ref(`designs/${user}/${id}/${file.name}`);
tumbUploadTask[i] = thumbRef[i].put(files[i]);
thumbCompleter[i] = new Deferred();
tumbUploadTask[i].on('state_changed',
(snap) => {
// Here you can check the progress
console.log(i, (snap.bytesTransferred / snap.totalBytes) * 100);
},
(error) => {
thumbCompleter[i].reject(error);
}, () => {
const url = tumbUploadTask[i].snapshot.metadata.downloadURLs[0];
itemsCount -= 1;
console.log(`Items left: ${itemsCount}`)
thumbCompleter[i].resolve(url);
});
}
return Promise.all(thumbCompleter).then((urls) => {
// Here we can see our files urls
console.log(urls);
});
};
Hope it helps.

Resources