I have a React app, page displaying some post containing title, text, few images etc. I'm trying to generate PDF file out of that.
The actual page is quite twisty, dynamic and plicated, so instead of trying to convert HTML to PDF I am using jsPDF package and as a content I insert state variables from Redux store. And it would have been ideal situation for that - I have tools to generate layout, I have bits of content, but... I bumped into a problem. Text of my post can vary from very little to few pages long, so in that case it should be passed to the next page. I have worked out how to get "pageHeight" and "textHeight", so when "textHeight" is bigger than "pageHeight" I can add another page by doc.addPage(), however the text stays on one page only and gets cut off by the end of the page (please see attached screenshot). This is where I am so far with my code:
const pdfGenerator = () => {
// Default export is a4 paper, portrait, using millimeters for units
const doc = new jsPDF();
const pageHeight = doc.internal.pageSize.height;
const wrappedText = doc.splitTextToSize(post.text, 200);
doc.setFontSize(14);
const dim = doc.getTextDimensions(wrappedText);
const textHeight = dim.h;
doc.text(15, 10, wrappedText);
if (textHeight >= pageHeight) {
doc.addPage();
}
// const textPages = Math.ceil(dim.h / pageHeight);
// Don't know what to do with it....
doc.save(`${post.title}.pdf`);
};
One of the possible solutions would be to split whole text into array of chunks that can fit the page and then do a loop or map it through adding doc.addPage() every time. Big problem with that - it will split text in random places, like in the middle of the word.
I tried my best to find solution, lots of time wasted with no result...
Please help!
I have a React app, page displaying some post containing title, text, few images etc. I'm trying to generate PDF file out of that.
The actual page is quite twisty, dynamic and plicated, so instead of trying to convert HTML to PDF I am using jsPDF package and as a content I insert state variables from Redux store. And it would have been ideal situation for that - I have tools to generate layout, I have bits of content, but... I bumped into a problem. Text of my post can vary from very little to few pages long, so in that case it should be passed to the next page. I have worked out how to get "pageHeight" and "textHeight", so when "textHeight" is bigger than "pageHeight" I can add another page by doc.addPage(), however the text stays on one page only and gets cut off by the end of the page (please see attached screenshot). This is where I am so far with my code:
const pdfGenerator = () => {
// Default export is a4 paper, portrait, using millimeters for units
const doc = new jsPDF();
const pageHeight = doc.internal.pageSize.height;
const wrappedText = doc.splitTextToSize(post.text, 200);
doc.setFontSize(14);
const dim = doc.getTextDimensions(wrappedText);
const textHeight = dim.h;
doc.text(15, 10, wrappedText);
if (textHeight >= pageHeight) {
doc.addPage();
}
// const textPages = Math.ceil(dim.h / pageHeight);
// Don't know what to do with it....
doc.save(`${post.title}.pdf`);
};
One of the possible solutions would be to split whole text into array of chunks that can fit the page and then do a loop or map it through adding doc.addPage() every time. Big problem with that - it will split text in random places, like in the middle of the word.
I tried my best to find solution, lots of time wasted with no result...
Please help!
Share Improve this question asked May 5, 2021 at 10:17 wawaywaway 3303 silver badges10 bronze badges2 Answers
Reset to default 5The splitTextToSize function will return an array with the text splitted into lines, we just need to work with it controlling the interactions. The code below should resolve your problem.
const pdfGenerator = () => {
// Default export is a4 paper, portrait, using millimeters for units
const doc = new jsPDF();
const pageHeight = doc.internal.pageSize.height;
const wrappedText = doc.splitTextToSize(post.text, 180);
doc.setFontSize(14);
let iterations = 1; // we need control the iterations of line
const margin = 15; //top and botton margin in mm
const defaultYJump = 5; // default space btw lines
wrappedText.forEach((line) => {
let posY = margin + defaultYJump * iterations++;
if (posY > pageHeight - margin) {
doc.addPage();
iterations = 1;
posY = margin + defaultYJump * iterations++;
}
doc.text(15, posY, line);
});
doc.save(`${post.title}.pdf`);
};
This is a refinement on @Thiago Moura's answer. The only magic numbers, i.e. pletely subjective depending on your preference, are the margins. Everything else is derived. Also no need to keep a count of the number of lines. I'm including the method that makes the header and footer, even though it wasn't asked for, to provide context for how the margins are calculated. You may omit the header/footer logic and decrease or adjust the yMargin value without impacting the overall logic.
if (format === "PDF") {
const doc = new jsPDF("portrait", "px", "a4");
doc.setFontSize(12);
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const xMargin = 25; // left and right margin
const yMargin = 75; // top and botton margin including the header/footer. In px since that's what hte jsPDF is set to use
const lineHeight = doc.getLineHeight(); // default space between lines
const needNewPageAfterThisYPos = pageHeight - yMargin
// the `description` variable here is the pdf content
const wrappedText = doc.splitTextToSize(description, pageWidth - (2 * xMargin)); // account for both the left and right marigns
let yPos;
wrappedText.forEach((line) => {
if (yPos > needNewPageAfterThisYPos) {
doc.addPage();
yPos = undefined;
}
if (yPos === undefined) {
yPos = yMargin;
} else {
yPos = yPos + lineHeight;
}
doc.text(line, xMargin, yPos);
});
this.#addPDFHeaderAndFooter(doc, pageWidth, pageHeight, (yMargin / 2), data.label);
doc.save(file); // file is the file path to save to
} else {
...
}
and the header/footer logic
/**
* Private method to add the header and footer to each page of a generated pdf.
*/
#addPDFHeaderAndFooter = (doc, pageWidth, pageHeight, margin, message) => {
const pageCount = doc.internal.getNumberOfPages();
doc.setFont('Times');
doc.setFontSize(10);
for (var i = 1; i <= pageCount; i++) {
doc.setPage(i);
// add the header to the page
doc.text(message, pageWidth / 2, margin, {
align: 'center'
});
// add the footer to the page
doc.text(message, pageWidth / 2, pageHeight - margin, {
align: 'center'
})
}
}