this is my first time testing out Next.js API, I am also quite new to the whole Next.js/React world so bear with me.
The goal for this API route is to trigger an automatic download of a PDF with a custom watermark generated from API URL query parameters like this: /api/PDFWatermark?id=123&firstname=John&lastname=Doe
In order to create the watermark I am using pdf-lib and I am using a modified version of this code to watermark my PDF. To generate a modified and downloadable version of the original PDF pdfDoc I have tried to create a blob using the pdfBytes after the watermarking. After the blob is created I thought I could add it to an anchor attached to the DOM.
When commenting out the blob and anchor code, two errors occur:
ReferenceError: Blob is not defined
ReferenceError: document is not defined (possibility because there is no DOM to attach the anchor link)
At this point I am only able to print the pdfBytes as json, I am not able to create and download the actual watermarked PDF file.
Is there a way to auto download the pdfBytes as a PDF file when the API is called?
UPDATE
Working code below after changing modifyPDF to return a buffer:
const pdfBytes = await pdfDoc.save();
return Buffer.from(pdfBytes.buffer, 'binary');
And:
export default async function handler(req, res) {
const filename = "test.pdf";
const {id, firstname, lastname} = req.query;
const pdfBuffer = await modifyPDF(firstname, lastname, id);
res.status(200);
res.setHeader('Content-Type', 'application/pdf'); // Displsay
res.setHeader('Content-Disposition', 'attachment; filename='+filename);
res.send(pdfBuffer);
}
WORKING:
import {PDFDocument, rgb, StandardFonts } from 'pdf-lib';
export async function modifyPDF(firstname, lastname, id) {
const order_id = id;
const fullname = firstname + " " + lastname;
const existingPdfBytes = await fetch("https://pdf-lib.js.org/assets/us_constitution.pdf").then((res) => res.arrayBuffer());
const pdfDoc = await PDFDocument.load(existingPdfBytes);
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
const watermark = fullname + " (OrderID: " + id + ")";
// Set Document Metadata
pdfDoc.setSubject(watermark);
// Get pages
const pages = pdfDoc.getPages();
// Iterate every page, skip first
//pages.slice(1).forEach(page => {
pages.forEach(page => {
// Get the width and height of the page
const {
width,
height
} = page.getSize()
// Watermark the page
page.drawText(watermark, {
x: 70,
y: 8,
size: 10,
font: helveticaFont,
color: rgb(0.95, 0.1, 0.1),
})
})
const pdfBytes = await pdfDoc.save();
return Buffer.from(pdfBytes.buffer, 'binary');
}
export default async function handler(req, res) {
const filename = "test.pdf";
const {id, firstname, lastname} = req.query;
const pdfBuffer = await modifyPDF(firstname, lastname, id);
res.status(200);
res.setHeader('Content-Type', 'application/pdf'); // Displsay
res.setHeader('Content-Disposition', 'attachment; filename='+filename);
res.send(pdfBuffer);
}
Change your modifyPDF to return a buffer
[...]
const pdfBytes = await pdfDoc.save();
return Buffer.from(pdfBytes.buffer, 'binary');
[...]
Let the API return the PDF to the browser through the handler:
export default async function handler(req, res) {
const {id, firstname, lastname} = req.query;
const pdfBuffer = await modifyPDF(firstname, lastname, id);
res.status(200);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename='+filename);
// Edited as the linked example reports:
// res.type('pdf'); // and might not work
res.send(pdfBuffer);
}
Untested but you should get the gist.
Here's the full example from the library itself
Related
I'm trying to upload files using RN Document Picker.
Once I get those files selected, I need to turn them to base64 string so I can send it to my API.
const handlePickFiles = async () => {
if (await requestExternalStoreageRead()) {
const results = await DocumentPicker.pickMultiple({
type: [
DocumentPicker.types.images,
DocumentPicker.types.pdf,
DocumentPicker.types.docx,
DocumentPicker.types.zip,
],
});
const newUploadedFile: IUploadedFile[] = [];
for (const res of results) {
console.log(JSON.stringify(res, null, 2));
newUploadedFile.push({
name: res.name,
type: res.type as string,
size: res.size as number,
extension: res.type!.split('/')[1],
blob: res.uri, <<-- Must turn this in base64 string
});
}
setUploadedFiles(newUploadedFile);
console.log(newUploadedFile);
}
}
};
The document picker returns content uri (content://...)
They lists this as an example of handling blob data and base64:
let data = new FormData()
data.append('image', {uri: 'content://path/to/content', type: 'image/png', name: 'name'})
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'multipart/form-data',
},
body: data
})
Where they basically say that you don't need to use blob or base64 when using multipart/form-data as content type. However, my graphql endpoint cannot handle multipart data and I don't have time to rewrite the whole API. All I want is to turn it to blob and base64 string, even if other ways are more performant.
Searching for other libraries, all of them are no longer maintained, or has issues with new versions of android. RN Blob Utils is the latest npm that was no longer maintained.
I tried to use RN Blob Utils but I either get errors, wrong data type, or the file uploads but is corrupted.
Some other things I found is that I can use
fetch(res.uri).then(response => {response.blob()})
const response = await ReactNativeBlobUtil.fetch('GET', res.uri);
const data = response.base64();
ReactNativeBlobUtil.wrap(decodeURIComponent(blob))
///----
const blob = ReactNativeBlobUtil.fs.readFile(res.uri, 'base64');
But I can't do anything with that blob file.
What is the simplest way to uplaod files from document picker as base64 format? Is it possible to avoid using external storage permission?
You don't need to the third-party package to fetch BLOB data
const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve(xhr.response);
};
xhr.onerror = function (e) {
reject(new TypeError("Network request failed"));
};
xhr.responseType = "blob";
xhr.open("GET", "[LOCAL_FILE_PATH]", true);
xhr.send(null);
});
// Code to submit blob file to server
// We're done with the blob, close and release it
blob.close();
I ended up using react-native-blob-util
const res = await DocumentPicker.pickSingle({
type: [
DocumentPicker.types.images,
DocumentPicker.types.pdf,
DocumentPicker.types.docx,
DocumentPicker.types.zip,
],
});
const newUploadedFile: IUploadedFile[] = [];
const fileType = res.type;
if (fileType) {
const fileExtension = fileType.substr(fileType.indexOf('/') + 1);
const realURI = Platform.select({
android: res.uri,
ios: decodeURI(res.uri),
});
if (realURI) {
const b64 = await ReactNativeBlobUtil.fs.readFile(
realURI,
'base64',
);
const filename = res.name.replace(/\s/g, '');
const path = uuid.v4();
newUploadedFile.push({
name: filename,
type: fileType,
size: res.size as number,
extension: fileExtension,
blob: b64,
path: Array.isArray(path) ? path.join() : path,
});
} else {
throw new Error('Failed to process file');
}
} else {
throw new Error('Failed to process file');
}
I'm building PDFs using Puppeteer, the resulting PDF looks nice but it is failing PDF Accessibility reports.
The main issues have been the title of the PDF, and the Language of the PDF.
I have tried setting both via EXIF values (Title, Language), the title does display in certain cases but still fails Acrobat Pro's accessibility check report.
I have used another accessibility check report ( http://checkers.eiii.eu/en/pdfcheck/ ) and there the title is set successfully but not the language.
I have used --export-tagged-pdf as a launch parameter which fixed many other issues.
Would anyone have an idea how I could pass the accessibility report please? Mostly the language parameter. I'm using Node.js to generate the PDFs, even if there is another library to edit the PDF after the fact that would be really helpful, I wasn't able to figure that out.
Facing the same problem I managed to get all the required meta data and XMP data except PDF-UA identifier. I used the JS lib "pdf-lib" (https://pdf-lib.js.org/docs/api/classes/pdfdocument) to set the meta data and exiftool-vendored to inject the XMP shema data.
const pdfLib = require('pdf-lib');
const exiftool = require("exiftool-vendored").exiftool
const fs = require('fs');
const distexiftool = require('dist-exiftool');
const pdfData = await fs.readFile('your-pdf-document.pdf');
const pdfDoc = await pdfLib.PDFDocument.load(pdfData);
const nowDate = new Date();
const meta_creator = "The author";
const meta_author = "The author";
const meta_producer = "The producer";
const meta_title = "Your PDF title";
const meta_subject = "Your PDF subject";
const meta_creadate = `${nowDate.getFullYear()}-${nowDate.getMonth()+1}-${nowDate.getDate()}`;
const meta_keywords = ["keyword1", "keyword2", "keyword3", "keyword4"];
// Implement PDF Title
pdfDoc.setSubject(meta_subject);
// Implement required "DisplayDocTitle" pdf var
pdfDoc.setTitle(meta_title, {
showInWindowTitleBar: true,
updateMetadata: true
});
// Implement PDF language
pdfDoc.setLanguage("en-EN");
// Save file in order exiftool can load it
const pdfBytes = await pdfDoc.save();
await fs.promises.writeFile("your-pdf-document.pdf", pdfBytes);
// We use "distexiftool" to get the TAGS from PDF/UA well formed XMP file "pdfUA-ID.xmp" and assign data to "your-pdf-document.pdf"
execFile(distexiftool, ["-j","-xmp<=pdfUA-ID.xmp", "your-pdf-document.pdf"], (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
afterTagsOperation()
});
async function afterTagsOperation(){
// Open the file and write XMP tags with exiftool
await exiftool.write("your-pdf-document.pdf", { 'xmp:Author': meta_author });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Creator': meta_creator });
await exiftool.write("your-pdf-document.pdf", { 'xmp:CreateDate': meta_creadate });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Producer': meta_producer });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Title': meta_title });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Subject': meta_subject });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Keywords': meta_keywords });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Trapped': 'false' });
await exiftool.write("your-pdf-document.pdf", { 'xmp:DocumentID': `uuid:${nowDate.getTime()}` });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Title': meta_title });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Subject': meta_subject });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Keywords': meta_keywords });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Trapped': 'false' });
await exiftool.write("your-pdf-document.pdf", { 'xmp:Identifier': nowDate.getTime() });
await exiftool.write("your-pdf-document.pdf", { 'xmp:PDFVersion': `v${nowDate.getTime()}` });
await exiftool.write("your-pdf-document.pdf", { 'xmp-xmpMM:DocumentID': `uuid:${nowDate.getTime()}` });
await exiftool.write("your-pdf-document.pdf", { 'xmp-dc:format': `application/pdf` });
await exiftool.write("your-pdf-document.pdf", { 'xmp-dc:title': meta_title });
// We save the file
const pdfBytes = await pdfDoc.save();
await fs.promises.writeFile("your-pdf-document.pdf", pdfBytes);
}
Im trying to find an efficient way to get the url of an image in firebase right after ive uploaded it.
Id like to avoid writing a totally separate function to this....and instead Id like to include it in the promise chain. See code below
import storage from '#react-native-firebase/storage';
storage()
.ref('path/to/remote/folder')
.putFile('uri/of/local/image')
.then(() => {
//Id like to use getDownloadUrl() function here;
})
You can create chain of promises using await and then easily get your download url as below :
/**
* Upload the image to the specific firebase path
* #param {String} uri Uri of the image
* #param {String} name Name of the image
* #param {String} firebasePath Firebase image path to store
*/
const uploadImage = async (uri, name, firebasePath) => {
const imageRef = storage().ref(`${firebasePath}/${name}`)
await imageRef.putFile(uri, { contentType: 'image/jpg'}).catch((error) => { throw error })
const url = await imageRef.getDownloadURL().catch((error) => { throw error });
return url
}
Now you can call this function as below :
const uploadedUrl = await uploadImage('uri/of/local/image', 'imageName.jpg', 'path/to/remote/folder');
Now uploadedUrl will contains url of the uploaded image.
Good answer from Kishan but did not work for me....below includes some minor modifications so would work for me.
const localUri = Platform.OS === 'ios' ? imgPickResponse.uri.replace('file://', '') : imgPickResponse.uri;
this.uploadImageAndGetUrl(localUri, '/profilePics/' + this.props.userId)
.then(url => {console.log(url);});
}
uploadImageAndGetUrl = async (localUri, firebasePath) => {
try {
const imageRef = storage().ref(firebasePath);
await imageRef.putFile(localUri, {contentType: 'image/jpg'});
const url = await imageRef.getDownloadURL();
return url;
} catch (err) {
Alert.alert(err);
}
};
In my case, uri threw an error with Android permissions. So following function worked for me. Instead of uri, I passed response.path from the ImagePicker. Tip: you can use uuid to generate random file name.
this.uploadAndReturnFirestoreLink(response.path, 'images/' + 'example.jpg')
uploadAndReturnFirestoreLink = async (loaclPath, folderPath) => {
console.log(loaclPath)
try {
const imageRef = storage().ref(folderPath);
await imageRef.putFile(loaclPath, { contentType: 'image/jpg' });
const url = await imageRef.getDownloadURL();
console.log(url)
alert('Upload Success', url)
} catch (e) {
console.log(e);
}
};
I am trying to using React-Native-Camera to capture an image and upload to the server, the captured response only provide base64 image and relative uri path to the system's cache. I used to turn the image to a blob in websites using packages like blob-util, which doesn't work on React-native.
As I was searching around I see that most people are uploading the base64 strings directly to the server, but I can't find anything about blob, how can I get a blob from base64 image string?
I have a function in my project to convert image to a blob. Here it is. 1st function is to handle the camera. 2nd fuction is to create a blob and a image name.
addPicture = () => {
ImagePicker.showImagePicker({ title: "Pick an Image", maxWidth: 800, maxHeight: 600 }, res => {
if (res.didCancel) {
console.log("User cancelled!");
} else if (res.error) {
console.log("Error", res.error);
} else {
this.updateProfilePicture(res.uri)
}
});
}
This addPicture is used to launch the image picker. In above function, res means the output, that comes from showImagePicker. I had to pass the uri prop of the result(res.uri) to below function, in order to create the blob file
In below function, I wanted to name the image with userId. You can use anything you like.
updateProfilePicture = async (uri) => {
var that = this;
var userId = this.state.user.uid
var re = /(?:\.([^.]+))?$/;
var ext = re.exec(uri)[1];
this.setState({
currentFileType: ext
});
const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve(xhr.response);
};
xhr.onerror = function (e) {
console.log(e);
reject(new TypeError('Network request failed'));
};
xhr.responseType = 'blob';
xhr.open('GET', uri, true);
xhr.send(null);
});
var filePath = userId + '.' + that.state.currentFileType;
}
There are some other codes in above function, which are using to uplad the image to firebase storage. I did not include those codes.
My goal is to retrieve a file from Google Storage and then send it back via response. The problem is that, when I launch this function for the first time it crashes with Error [ERR_STREAM_WRITE_AFTER_END]: write after end. The next executions work fine.
exports = module.exports = region(defaultRegion).https.onRequest(async (req, res): Promise<void> => {
const [authError] = await to(handleAuth(req, res));
if (authError) {
res.status(500).send(authError.message);
return;
}
const { assetId, contentType, path } = req.query;
const file = bloqifyStorage.bucket().file(`assets/${assetId}/${path}`);
const fileExists = (await file.exists())[0];
if (!fileExists) {
res.status(404).send(`${path} was not found`);
return;
}
const fileStream = file.createReadStream();
fileStream.pipe(res).on('end', (): void => {
res.setHeader('content-type', contentType);
});
});
What am I missing here?
Edit: removing the set header doesn't solve it. Ex:
const fileStream = file.createReadStream();
// res.setHeader('content-type', contentType);
fileStream.pipe(res).on('error', (e): void => {
console.log(e);
});
It will print the same error.