eKoopmans / html2pdf.js

Client-side HTML-to-PDF rendering using pure JS.
MIT License
4.09k stars 1.39k forks source link

Page Break Issue #200

Open Fahad-AQ opened 5 years ago

Fahad-AQ commented 5 years ago

Hi ekoopmans , kindly give me solution of page break issue ,

image

Fahad-AQ commented 5 years ago

This is my javascript code.

function addScript(url) {
var script = document.createElement('script');
script.type = 'application/javascript';
script.src = url;
document.head.appendChild(script);

} addScript('https://raw.githack.com/eKoopmans/html2pdf/master/dist/html2pdf.bundle.js');

var element = document.getElementById('paginations'); var opt = { filename: 'abc.pdf' };

  // New Promise-based usage:
  html2pdf().set(opt).from(element).save();
chille1987 commented 5 years ago

@Fahad-AQ Are you been able to fix this?

ShyamRaj commented 4 years ago

@eKoopmans I'm running into a similar issue. Any solutions would be of great help. I did use the given page break options but the page-break solution only works for 1st page and not others. So, if I add page-break-inside: avoid It does avoid doing that slicing only in the first page of the PDF and not on subsequent pages

jzsenthy commented 3 years ago

I encountered page break alignment issues as well, and it seemed like I had to change at least two different things to get it to work.

1.

I had a div within a div, and I was trying to use "break-before" on the parent div (newBox) but it was having issues and only breaking after the child div.
Example, with issue :

<div class='newBox'>
   <div class='soBox text-end'>
      SO#  1234
   </div> 
   nextBox  [Box 1 of 5]
</div>

I ended up just adding two divs before it (with some content inside--a non-breaking space--since empty divs don't seem to page break properly either), and changed my break-before target to the second new div (newBoxBefore2). Example (working):

<div class='newBoxBefore1'>
   &nbsp;
</div>
<div class='newBoxBefore2'>
   &nbsp;
</div>

<div class='newBox'>
   <div class='soBox text-end'>
      SO# 1234
   </div> 
   nextBox [Box 1 of 5]
</div>

Note: This may affect the layout of your page, and if you try to reduce the height of the div, the page break location may mess up again. You could try reducing the div height on page load then call a custom function just before saving as pdf to restore the normal div height if you have issues.

 

2.

The second issue I noticed after testing saving a pdf with lots of pages is that the page content started further and further up each page, until it was starting on the previous page. I ended up adding padding-top (via javascript) to each of my "newBox" divs, and made the padding equal in pixels to the page number (for example, page 2 has 2px padding-top, page 3 has 3px padding-top, page 40 has 40px, etc). Example (working):

            elements = document.getElementsByClassName("newBox");
            for (var i = 0; i < elements.length; i++) {
                elements[i].style.paddingTop = elements[i].style.paddingTop = i + "px";
            };

This code gets each element with the class name "newBox", then goes through them one by one, and changes the CSS style on each to essentially padding-top = "##px" where ## = the page number (really, the number of times you've looped through this FOR loop, which is the number of times you've encountered a div with class name "newBox". But those divs are at the top of my pages so it's essentially the same thing as page number).

This is my CSS in the <head> (but I think you could also just use the html2pdf options instead of defining it in CSS):

        .newBoxBefore2 {
            break-before: page;
        }

 

3.

Another thing I did was added a function to run (when clicking the Save button) which will load custom CSS for styling the page for print/save, and running a function afterward to put the CSS back to normal. I actually just duplicated my @media print CSS rules and added another class in front of them so they only run when they see the printCss class in a parent div, so I'm adding the printCSS class to the <html> tag before saving the pdf, and removing it afterward.

Example:

CSS:

        /*  Duplicating PRINT OPTIONS for html2pdf saving*/

        .printCss .newBox {
            padding-left: 20px;
        }

        /* Hiding elements with bootstrap d-print-none class, so it works with html2pdf */
        .printCss .d-print-none {
            display: none;
        }

        /*  Defines colors for printing as: white background, black text */
        /*  HISTORY:  It was printing with gray text as default in dark mode */
        .printCss html,
        .printCss,
        .printCss .newBox, {
            color: black;
            background-color: white;
        }

HTML:

(using some classes from the bootstrap 5 library)

                            <!-- Save pdf button -->
                            <input type="button" class="btn btn-secondary float-end printButton"
                                id="saveButton" value="🖫 Save">

JAVASCRIPT:

    <!-- SAVE PDF. html2pdf library for generating pdf.  https://github.com/eKoopmans/html2pdf.js -->
    <script src="./js/html2pdf.bundle.min.js"></script>

    <script type="text/javascript">
        // RUNNING HTML2PDF
        document.getElementById("saveButton").addEventListener("click", html2pdfAndPrintCss);
        html2pdfAndPrintCss();

        // FUNCTION: Calling the html2pdf function and the add/remove print css functions
        function html2pdfAndPrintCss() {
            // Targeting a parent div containing all my elements I want saved/printed as pdf
            var html2pdfElement = document.getElementById('resultsBox');

            // Html2pdf.js options
            var html2pdfOpt = {
                margin: 0.2,
                filename: 'savedweb.pdf',
                image: { type: 'jpeg', quality: 0.98 },
                html2canvas: { scale: 4 },  // Doubled the scale to 4 to increase print quality on thermal printer
                // pagebreak: { before: '.newBox' },  // Not needed since I'm defining it in CSS
                jsPDF: { unit: 'in', format: [4, 6], orientation: 'portrait' }
            };

            // FUNCTION:  Call the function to add printCss class to body tag, which triggers css rules before saving
            addPrintCss();
            // FUNCTION:  Add extra padding to the top of each page to offset the incorrect positioning that html2pdf applies
            changeCSSAddPadding();

            // Uses .then (promises) to only remove the css after the async saving task is done, 
            // then sets padding-top on each page back to a standard value
            // REFERENCE: https://github.com/eKoopmans/html2pdf.js/issues/79#issuecomment-552126785
            html2pdf().set(html2pdfOpt).from(html2pdfElement).save().then(removePrintCss).then(changeCSSRemovePadding);
            });

        };

        // FUNCTION: Adding @media print css options to html2pdf output
        // by duplicating those rules, and loading them when divs/classes have a parent div with the printCss class
        // USAGE: Call this function before running html2pdf
        function addPrintCss() {
            // SOURCE:  https://stackoverflow.com/a/46980378
            document.documentElement.classList.add("printCss");  // Use this to directly add to html tag
        };

        // USAGE: Call this function after running html2pdf (might need to use .then(function))
        function removePrintCss() {
            // SOURCE:  https://stackoverflow.com/a/46980378
            document.documentElement.classList.remove("printCss");  // Use this to directly affect html tag
        };

        // FUNCTION:  Change CSS dynamically
        // REFERENCE:  https://stackoverflow.com/a/10071316/1263035
        function changeCSSAddPadding() {
            // Add X pixels (X is page number) to the padding-top CSS style per page to correct html2pdf incorrect alignment offset
            elements = document.getElementsByClassName("newBox");
            for (var i = 0; i < elements.length; i++) {
                elements[i].style.paddingTop = elements[i].style.paddingTop = i + "px";
            };
        };

        // FUNCTION:  Change CSS dynamically
        // REFERENCE:  https://stackoverflow.com/a/10071316/1263035
        function changeCSSRemovePadding() {
            // Remove the pixels added to the padding-top CSS style per page for correcting html2pdf alignment offset (set to 20px)
            elements = document.getElementsByClassName("newBox");
            for (var i = 0; i < elements.length; i++) {
                elements[i].style.paddingTop = elements[i].style.paddingTop = "20px";
            };
        };
    </script>

  I'm kinda tired right now so sorry if I made any errors while reducing my code in an attempt to make it a bit easier to understand. (My issue may have not even been the same issue you were having, lol. But I was searching through every "page break" issue I could find on here when I was trying to figure out a solution.)

yurass13 commented 1 year ago

I also faced this problem in my project. Having experimented a little with the input data, it turned out that overriding the width parameter for html2canvas also breaks page breaks. A simplified example of the problem causing code is attached below.

<div class="dragdroppable-row">
    <div class="grid-row background--transparent">
        <div class="dragdroppable dragdroppable-column">
            <div class="resizable-container" style="position: relative; user-select: auto; height: 176px; max-height: 3.40282e+38px; min-width: 135.417px; min-height: 40px; box-sizing: border-box; flex-shrink: 0; max-width: 1801px !important; margin-left: 16px !important;">
                <div class="dashboard-component dashboard-component-chart-holder dashboard-chart-id-2 fade-out">
                    <div class="chart-slice css-v2o9ep">
                        <div class="dashboard-chart">
                            <div class="chart-container css-1wgwgod">
                                <div>
                                    <div id="chart-id-2" class="handlebars">
                                        <div height="144" width="1769" class="css-8i8h0o">
                                            <h1>some h1</h1>
                                            <h3>some h3</h3>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
<div class='newBoxBefore1'>
    &nbsp;
</div>
<div class='newBoxBefore2'>
    &nbsp;
</div>

<div class="dragdroppable-row newBox">
    <div class="grid-row background--transparent">
        <div class="dragdroppable dragdroppable-column">
            <div class="resizable-container" style="position: relative; user-select: auto; height: 176px; max-height: 3.40282e+38px; min-width: 135.417px; min-height: 40px; box-sizing: border-box; flex-shrink: 0; width: 1801px !important; max-width: 1801px !important; margin-left: 16px !important;">
                <div class="dashboard-component dashboard-component-chart-holder dashboard-chart-id-2 fade-out">
                    <div class="chart-slice css-v2o9ep">
                        <div class="dashboard-chart">
                            <div class="chart-container css-1wgwgod">
                                <div>
                                    <div id="chart-id-2" class="handlebars">
                                        <div height="144" width="1769" class="css-8i8h0o">
                                            <h1>some h1</h1>
                                            <h3>
                                                some h3
                                            </h3>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
<div class='newBoxBefore1'>
    &nbsp;
</div>
<div class='newBoxBefore2'>
    &nbsp;
</div>

<div class='newBox'>
    <div class='soBox text-end'>
        SO# 1234
    </div> 
    nextBox [Box 1 of 5]
</div>

<script>
    html2pdfAndPrintCss();
</script>

CSS

* {
    box-sizing: border-box;
}
html, body {
    margin:0;
    padding:0;
    background-color: #cccccc;
    font-size: 14px;
    line-height: 1.4;
}
body {
    font-family: 'Inter', Helvetica, Arial;
    font-size: 14px;
    line-height: 1.4;
    color: #333333;
    background-color: #cccccc;
}
.dragdroppable-row {width: 100%;}
.grid-row {
    position: relative;
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: flex-start;
    width: 100%;
    height: fit-content;
}
.background--transparent {
    background-color: transparent;
}
.dashboard-component-chart-holder.fade-out {
    border-radius: 4px;
    box-shadow: none;
    transition: box-shadow 0.2s ease-in-out, opacity 0.2s ease-in-out, border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.dashboard-component-chart-holder {
    width: 100%;
    height: 100%;
    color: #333333;
    background-color: #ffffff;
    position: relative;
    padding: 16px;
    overflow-y: visible;
    transition: opacity 0.2s, border-color 0.2s, box-shadow 0.2s;
    border: 2px solid transparent;
}
.css-v2o9ep {
    display: -webkit-box;
    display: -webkit-flex;
    display: -ms-flexbox;
    display: flex;
    -webkit-flex-direction: column;
    -ms-flex-direction: column;
    flex-direction: column;
    max-height: 100%;
}
[data-test-chart-id="2"] .chart-header {
    display: none !important;
    visibility: hidden !important;
}
.dashboard-chart {
    overflow: hidden;
    position: relative;
}
.css-1wgwgod {
    min-height: 144px;
    position: relative;
}
#chart-id-2 div h1 {
    font-size: 40px;
}
body h1 {
    font-weight: 600;
    line-height: 1.4;
    font-size: 28px;
    letter-spacing: -0.2px;
    margin-top: 12px;
    margin-bottom: 12px;
}
.css-1wgwgod .slice_container {
    height: 144px;
}
#chart-id-2 div {
    text-align: center;
    position: relative !important;
    width: 100vw !important;
}

JS

function html2pdfAndPrintCss() {
    var html2pdfElement = document.body;

    var html2pdfOpt = {
        margin: 0.2,
        filename: 'savedweb.pdf',
        image: { type: 'jpeg', quality: 0.98 },
        html2canvas: { 
            scale: 3, 
            width: 1850 // broken here
        },
        pagebreak: { before: '.newBox' },
        jsPDF: { unit: 'pt', format: 'a4', orientation: 'l' }
    };

    html2pdf().set(html2pdfOpt).from(html2pdfElement).save();
};

The problem may be due to the timing of page break rules before scaling.

yurass13 commented 1 year ago

I found the following solution, which allows you to consistently convert regardless of the content of the page. It consists of simply adding spacers to create breaks on the page itself. In my case, it was necessary to generate strictly pdf A4 landscape format. If necessary, the page format and its aspect ratio can be moved to a separate input variable, determined based on the main jsPDF parameters.

function addSpacers(target){
    // NOTE a4 fromat in portrait(width=height/Math.sqrt(2)) and in landscape(width=height*Math.sqrt(2))
    var pageH = document.body.clientWidth/ Math.sqrt(2);

    var spacer = document.createElement('div');
    spacer.style.width = '100%';
    spacer.style.backgroundColor = 'transparent';
    spacer.class = 'html2pdf__spacer';

    for (const row of target.children){
        if ($(row).css('display') !== 'none'){
            var rect = row.getBoundingClientRect();
            // IMPORTANT first page always is 1.
            var startBlockPage = Math.ceil(rect.top/pageH) == 0 ? 1 : Math.ceil(rect.top/pageH);
            var endBlockPage = Math.ceil((rect.bottom)/pageH);

            // block bigger than page need recurse for slove it problem and definition for tables
            if (rect.bottom - rect.top >= pageH){
                // TODO need tests. Be careful with css.
                addSpacers(row);
                console.log('Recurcy ', row);
            }
            else{
                if(startBlockPage == endBlockPage){

                    // all OK
                }
                else{
                    // add spacer

                    spacer.style.height = (pageH * startBlockPage) - rect.top + 16 + 'px';
                    row.parentNode.insertBefore(spacer.cloneNode(true), row);
                }
            }
        }
    }
};

Then, after saving you can delete all $('.html2pdf__spacer') elements. But you may run into problems with specific CSS or indivisible content that is larger than the page. In this case, you will have to look for an addition to this solution yourself.