parallax / jsPDF

Client-side JavaScript PDF generation for everyone.
https://parall.ax/products/jspdf
MIT License
29.03k stars 4.64k forks source link

Can I use html() multiple times in a PDF #3074

Closed CurryPaste closed 3 years ago

CurryPaste commented 3 years ago

I want to export a PDF in a function. At the same time, the content inside is generated by using .html() many times. If only once .html() there is no problem at all. But I found that I used it twice, and the content of the second time would not be generated in PDF. So I want to know,.html() Only once? this is code

async getPdfByJSPDF(){
      // console.log(this.$refs.ProjectDesignReport.changeHeightToA4(),"这里在执行子组件的改变高度函数")
      const { width:A4W, height: A4H } = {width:960,height:1362};
      const { unit, orientation, format } = {unit:"px", orientation:"p", format:[A4H,A4W]} // optionPDF(A4W,A4H);
      // console.log(unit, orientation, format,"unit, orientation, format")

      let thePDF = new JsPDF({ orientation, unit, format });
      thePDF = addSelfFont(thePDF, MyFont, "Alibaba-PuHuiTi-Regular-normal" ); // 增加字体

      const pdfBody = document.querySelector("#export");

      await thePDF.html(pdfBody,{x:0,y:0});
      await thePDF.html(pdfBody,{x:0,y:1362*3});

      const num = thePDF.internal.getNumberOfPages(); // 可以获得页数
      for(let i = 0; i < num; i+=1) {
        thePDF.setPage(i); 
        thePDF.setTextColor("red");
        thePDF.setFontSize(32);
        thePDF.text(`${thePDF.internal.getCurrentPageInfo().pageNumber  }/${  num}`,40,40, );
      }
      thePDF.save();
    }

By the way, we've dealt with this problem in other ways, so now we're just a little curious.

At the same time, if I continue to use html() in html() callback, I can write the content into PDF, but in the way of covering the previous one.

version 2.3.0

HackbrettXXX commented 3 years ago

Can confirm. Calling html a second time has no effect.

CurryPaste commented 3 years ago

Yes, but if you call html again in the callback function, you can override the last time. That's more interesting 😀 So we finally adopted the window.print to deal with pdf

joanc-naia commented 3 years ago

Yeah, I thought I could use .addPage() after .html() and call .html() again to insert anoter DOM element into a new page. Turns out I can't. Is there a differrent way to go about doing this?

everttrollip commented 3 years ago

Interesting. So then I'm stuck and wondering if someone can help: I have a document where I'd like to print html twice. Once at the following coordinates: {x: 15, y: 100} and the second time slightly lower (depending on the size of a table in between, generated with autotable), for example at {x: 15, y: 150}. I achieved this previously with the deprecated function fromHtml

How can I achieve this with the latest version of jspdf (v2.3.0) and the html function?

Another interesting, (maybe related?) issue is the font size of the html - I tried to use inline styling on the div that wraps the text with a fixed width (as described in #3088 ), and tried forcing the font-size. Regardless of what I enter though, I always get the same font size (nothing smaller).

I've attached an illustration of my current struggle. Here is my code snippet to generate the pdf,

document.html(
  `<div style="width: 180px !important; font-size: 14px; font-family: Arial, Helvetica, sans-serif; line-height: 1.2 !important;">${text}</div>`,
  {
    // eslint-disable-next-line object-shorthand, func-names
    callback: function () {
      doc.save();
    },
    x,
    y,
  }
);

CleanShot 2021-03-21 at 17 11 42@2x

ahocevar commented 3 years ago

This is a duplicate of #2899, which contains two more examples to reproduce this bug.

I'd be willing to create a pull request with a fix. Any clue where I should look first would be appreciated. It seems html2canvas is executed correctly for subsequent html() calls, but the result does not end up in the pdf document.

helsonxiao commented 3 years ago

Some workarounds:

ahocevar commented 3 years ago

Using html2canvas directly has the disadvantage that text will be converted to an image, whereas using doc.html() preserves text as text. And html-to-image is not an option because of the Safari limitations.

helsonxiao commented 3 years ago

Using html2canvas directly has the disadvantage that text will be converted to an image, whereas using doc.html() preserves text as text. And html-to-image is not an option because of the Safari limitations.

Yes, that's why they are workarounds.

BTW: If server side rendering is activated, HTML element transformed by html-to-image will be more accurate than html2canvas does(css shadow, position, ... or any other things Canvas can't do). I'm now using both html-to-image & html2canvas with jsPDF to create complicate worksheets for user. And I do hope the text could be copied. :)

everttrollip commented 3 years ago

@helsonxiao that is a very interesting workaround suggestion. The goal of my PDF to create not too complicated reports with a few charts and some minimal text. Copying text is not a "requirement".

So this might be worth looking into...

Would you be able to share some examples how you are using html-to-image with jsPDF to create a pdf?

helsonxiao commented 3 years ago

@helsonxiao that is a very interesting workaround suggestion. The goal of my PDF to create not too complicated reports with a few charts and some minimal text. Copying text is not a "requirement".

So this might be worth looking into...

Would you be able to share some examples how you are using html-to-image with jsPDF to create a pdf?

Sure! Just turn html into image then call addImage API.

import html2canvas from 'html2canvas';
import * as htmlToImage from 'html-to-image';

// using svg
image = await htmlToImage.toPng(element, {
  // cacheBust: true,
  // fontEmbedCss,
});

// or canvas
image = await html2canvas(element, {
  // useCORS: true,
});

doc.addImage(
  image,
  imgX,
  imgY,
  scaledImgW,
  scaledImgH,
  undefined,
  comporession,
);
HackbrettXXX commented 3 years ago

Closing this in favor of #2899.

Sherline1998 commented 1 year ago

Hey hi, Try with Timeout functions with 5 seconds delay. It works great!

const createPDF = async (index) => { let pdf = new jsPDF("portrait", "pt", "a4"); let data = await document.querySelector(#reportCardPdf_${index}); pdf.html(data).then(() => { pdf.save(ExamReportCard_${index + 1}.pdf); } ); };

const handlePdfGeneration = () => {
    const delay = 2; //please add delay minimum of 2 seconds to avoid white pages in pdf
    const limit = state.data.length;
    let i = 0;
    const limitedInterval = setInterval(() => {
        createPDF(i++);
        if (i > limit) {
            clearInterval(limitedInterval);
        }
    }, delay * 1000);
}
kito-arch commented 1 year ago

Didnt work for me :(

prasheel888 commented 1 year ago

I've found a workaround for generating multipage PDF using .html() method,

const doc = new jsPDF("p", "pt", "a4");   
let pageHeight = doc.internal.pageSize.getHeight();
   doc.html(document.getElementById("Page1"), {
      callback: function (pdf) {
        pdf.addPage("a4", "l");
        pdf.html(document.getElementById("Page2"), {
          callback: function (pdf2) {
            pdf2.addPage("a4", "l");
            pdf2.html(document.getElementById("Page3"), {
              callback: function (pdf3) {
                pdf3.save("multipage.pdf")
              },
              x: 0,
              y: 2 * pageHeight,
            });
          },
          x: 0,
          y: pageHeight,
        });
      },
      x: 0,
      y: 0,
    });
adidahl commented 1 year ago

I've tried your solution and it renders only the first element(Page 1). The rest of the pages (with elements Page2 and Page3) are empty. Can you maybe provide a JSFiddle working example? Thanks.

prasheel888 commented 1 year ago

I've tried your solution and it renders only the first element(Page 1). The rest of the pages (with elements Page2 and Page3) are empty. Can you maybe provide a JSFiddle working example? Thanks.

U can try this: https://codesandbox.io/s/a11y-tabs-forked-rr814c?file=/src/App.js

bakuur commented 1 year ago

I found an answer to this question (refrencing)

Just use await and make sure to return doc inside the callback

something like this

var doc = new jspdf.jsPDF({
  orientation: 'p',
  unit: 'pt',
  format: 'letter'
});

var field = "<b>html test </b>";
doc.text(10, 10, "test");
//add first html
await doc.html(field, {
  callback: function (doc) {
    return doc;
  },
  width: 210,
  windowWidth: 210, 
      html2canvas: {
          backgroundColor: 'lightyellow',
          width: 210, 
          height: 150
      },
      backgroundColor: 'lightblue', 
  x: 10,
  y: 50,
  autoPaging: 'text'
});
window.open(doc.output('bloburl'));

now you can call doc.html as many times as you want for that same document

gitalvininfo commented 1 year ago

I've found a workaround for generating multipage PDF using .html() method,

const doc = new jsPDF("p", "pt", "a4");   
let pageHeight = doc.internal.pageSize.getHeight();
   doc.html(document.getElementById("Page1"), {
      callback: function (pdf) {
        pdf.addPage("a4", "l");
        pdf.html(document.getElementById("Page2"), {
          callback: function (pdf2) {
            pdf2.addPage("a4", "l");
            pdf2.html(document.getElementById("Page3"), {
              callback: function (pdf3) {
                pdf3.save("multipage.pdf")
              },
              x: 0,
              y: 2 * pageHeight,
            });
          },
          x: 0,
          y: pageHeight,
        });
      },
      x: 0,
      y: 0,
    });

@prasheel888 Thank you for this. Working as expected.

Samik-Pandit commented 1 year ago

I've found a workaround for generating multipage PDF using .html() method,

const doc = new jsPDF("p", "pt", "a4");   
let pageHeight = doc.internal.pageSize.getHeight();
   doc.html(document.getElementById("Page1"), {
      callback: function (pdf) {
        pdf.addPage("a4", "l");
        pdf.html(document.getElementById("Page2"), {
          callback: function (pdf2) {
            pdf2.addPage("a4", "l");
            pdf2.html(document.getElementById("Page3"), {
              callback: function (pdf3) {
                pdf3.save("multipage.pdf")
              },
              x: 0,
              y: 2 * pageHeight,
            });
          },
          x: 0,
          y: pageHeight,
        });
      },
      x: 0,
      y: 0,
    });

Thank You mate!

tthomaz commented 1 year ago

I've found a workaround for generating multipage PDF using .html() method,

const doc = new jsPDF("p", "pt", "a4");   
let pageHeight = doc.internal.pageSize.getHeight();
   doc.html(document.getElementById("Page1"), {
      callback: function (pdf) {
        pdf.addPage("a4", "l");
        pdf.html(document.getElementById("Page2"), {
          callback: function (pdf2) {
            pdf2.addPage("a4", "l");
            pdf2.html(document.getElementById("Page3"), {
              callback: function (pdf3) {
                pdf3.save("multipage.pdf")
              },
              x: 0,
              y: 2 * pageHeight,
            });
          },
          x: 0,
          y: pageHeight,
        });
      },
      x: 0,
      y: 0,
    });

saved me Thanks

bambang-ap commented 1 year ago

I've found a workaround for generating multipage PDF using .html() method,

const doc = new jsPDF("p", "pt", "a4");   
let pageHeight = doc.internal.pageSize.getHeight();
   doc.html(document.getElementById("Page1"), {
      callback: function (pdf) {
        pdf.addPage("a4", "l");
        pdf.html(document.getElementById("Page2"), {
          callback: function (pdf2) {
            pdf2.addPage("a4", "l");
            pdf2.html(document.getElementById("Page3"), {
              callback: function (pdf3) {
                pdf3.save("multipage.pdf")
              },
              x: 0,
              y: 2 * pageHeight,
            });
          },
          x: 0,
          y: pageHeight,
        });
      },
      x: 0,
      y: 0,
    });
const doc = new jsPDF("l", "pt", "a4");
let pageHeight = doc.internal.pageSize.getHeight();

const pages = [
document.getElementById("Page1")!,
document.getElementById("Page2")!,
document.getElementById("Page3")!,
];

htmlPage(doc, pages);

function htmlPage(document: jsPDF, pages: HTMLElement[], index = 0) {
const hasPages = pages.length > 0;

if (!hasPages) return document.save("multipage.pdf");

return document.html(pages[0]!, {
    y: index * pageHeight,
    callback(pdf) {
        if (index > 0 && pages.length > 1) pdf.addPage("a4", "l");
        const restPages = pages.slice();
        restPages.splice(0, 1);
        htmlPage(pdf, restPages, index + 1);
    },
});
}
braadworst commented 1 year ago

Great solution with the callback logic @prasheel888, here is a recursive one, for arbitrary length documents. This saves me as well as I need portrait and landscape in the same file

const fileName  =  'test.pdf'
addPageToPdf(pages);

function addPageToPdf(pages, pageIndex = 0) {
  if (pages.length === 0) {
    pdf.save(fileName);
    return;
  }
  pdf.html(pages.at(0), {
    y: pdf.internal.pageSize.getHeight() * pageIndex,
    callback(pdf) {
      pdf.addPage();
      addPageToPdf(pages.slice(1), ++pageIndex);
    }
  });
}
martipk commented 10 months ago

@braadworst great solution but it doesn't work if some of the "pages" overflow on a second page. I have an auto-generated table that can span between 1 and 4 pages long depending on user input, and i want my next content to always start on the next page after the end of the table (imagine a page break in word). Is there any way to calculate that?

As I am writing this it dawned on me that I can keep track of how many rows my table has and find the height of the table that way, then do some easy math and stuff to calculate the next page.

Leaving this here in case someone is in a similar situation and might find it helpful.

Edit: Actually found a more reliable solution as my row heights varied so it didnt work out too well. IF YOUR DOCUMENT REQUIRES ONLY ONE PAGE BREAK JUST MAKE THE y value NEGATIVE pageHeight

var page_h = doc.internal.pageSize.getHeight();

doc.html( html_variable_length_content , 
{
    callback: function(doc) {
        doc.addPage("a4");
        doc.html( html_final_page , 
        {
            callback: function(doc) {
                doc.save("test.pdf");
            },
            y : -page_h
        });
    },
    y: 0
});
stev232 commented 9 months ago

I found an easy way to iterate through this and get all of the pages to populate and format. A lot of solutions looked like we were hard coding the pages. This method should allow you to use an array of html pages.

 const doc = new jsPDF();
  let pageHeight = doc.internal.pageSize.getHeight();
  await doc.html(data[0], {
    callback: async function (doc) {
      // Save the PDF
      if(data.length > 0) {
        for(let i = 1; i < data.length; i++) {
          doc.addPage();
          await doc.html(data[i], {
            margin: [10, 10, 10, 10],
            x: 0,
            y: i * pageHeight,
            width: 190, //target width in the PDF document
            windowWidth: 675 //window width in CSS pixels
          });
        }
      }
      doc.save(`${fileName}.pdf`);
    },
    margin: [10, 10, 10, 10],
    x: 0,
    y: 0,
    width: 190, //target width in the PDF document
    windowWidth: 675 //window width in CSS pixels
  });
hashimnobrainer commented 1 month ago

@stev232 - .html is not working for me in Safari. I do not want to use addImage? Any solution?