yWorks / svg2pdf.js

A javascript-only SVG to PDF conversion utility that runs in the browser. Brought to you by yWorks - the diagramming experts
MIT License
667 stars 104 forks source link

Support for multi-page PDF? #37

Closed kickmyassp closed 6 years ago

kickmyassp commented 6 years ago

Is it possible to convert a single SVG to PDF in multiple pages of Letter or A4 size?
I suppose dividing the original SVG into smaller ones may work but in my case the SVG is created by another javascript and hence not easy to break down (at least I don't know how).

yGuy commented 6 years ago

If you don't know how to break down the SVG, properly, how should the software be able to do that automatically for you?

If you are looking for a simple tiling approach, that of course is easy to achieve: simply render the same svg using different viewports onto the various pages.

scottctr commented 5 years ago

@yGuy Can you point me to a few more details on the approach you mention above?

If you are looking for a simple tiling approach, that of course is easy to achieve: simply render the same svg using different viewports onto the various pages.

HackbrettXXX commented 5 years ago

You can try something like this:

svg:

<svg id="svg-image" xmlns="http://www.w3.org/2000/svg" width="200px" height="100px" viewBox="0 0 200 100">
  <rect x="10" y="10" width="80" height="80" fill="red"></rect>
  <rect x="110" y="10" width="80" height="80" fill="red"></rect>
</svg>

js:

const svgElement = document.getElementById('svg-image')
const doc = new jsPDF('l', 'pt', [100, 100])

svgElement.setAttribute("width", "100px")
svgElement.setAttribute("height", "100px")
svgElement.setAttribute("viewBox", "0 0 100 100")
svg2pdf(svgElement, doc, {})

doc.addPage()
svgElement.setAttribute("viewBox", "100 0 100 100")
svg2pdf(svgElement, doc, {})
scottctr commented 5 years ago

I've almost got the paging to working, but I'm losing my margin on the right and bottom. Am I missing something?

    public convertSvgToPdf(svgElement: Element, size: Size, margins: Insets) {
        svgElement = svgElement.cloneNode(/*deep*/true) as Element;

        const margin = margins ? Math.max(margins.left, margins.right, margins.top, margins.bottom) : Defaults.PdfDocumentMargin;

        const pdfPageWidth = 792;
        const pdfPageHeight = 612;
        svgElement.setAttribute('width', pdfPageWidth + 'px'); 
        svgElement.setAttribute('height', pdfPageHeight + 'px'); 

        const jsPdf = new jsPDF('l', 'pt', [pdfPageWidth, pdfPageHeight]);

        const pagesWideWithMargins = Math.floor((size.width + (pdfPageWidth - 1)) / (pdfPageWidth - (margin * 2)));
        const pagesHighWithMargins = Math.floor((size.height + (pdfPageHeight - 1)) / (pdfPageHeight - (margin * 2)));

        for (let i = 0; i < pagesHighWithMargins; i++) {
            for (let j = 0; j < pagesWideWithMargins; j++) {
                svgElement.setAttribute('viewBox', this.getViewboxAttribute(j, i, pdfPageWidth, pdfPageHeight, margin));
                svg2pdf(svgElement, jsPdf, {
                    xOffset: 0,
                    yOffset: 0,
                    scale: 1
                });

                jsPdf.addPage([pdfPageWidth, pdfPageHeight], 'landscape');
            }
        }

        return jsPdf.output('datauristring');
    }

    private getViewboxAttribute(horizontalPageNumber: number, verticalPageNumber, pdfPageWidth, pdfPageHeight, margin) {
        const value = String((pdfPageWidth - (margin * 2)) * horizontalPageNumber) + ' ' + ((pdfPageHeight - (margin * 2)) * verticalPageNumber) + ' ' + (pdfPageWidth - (margin * 2)) + ' ' + (pdfPageHeight - (margin * 2));

        return value;
    }

image

HackbrettXXX commented 5 years ago

I guess this has something to do with svg2pdf not clipping the svg at the viewport. You could try and add a clipping rect with the size of the current viewport and also update this for each page.

Could you please verify if this is correct? The bottom/right margin portion of each page should be duplicated on the next page.

scottctr commented 5 years ago

No, I don't intend to have any graph content delicates from page to page. I want a small default margin so it can be printed on printers that can't do borderless and I'll probably add a little extra border at the bottom to accommodate row and column numbers to help users reassemble printed pages into the graph.

On Tue, Apr 16, 2019, 9:13 AM Lukas Holländer notifications@github.com wrote:

I guess this has something to do with svg2pdf not clipping the svg at the viewport. You could try and add a clipping rect with the size of the current viewport and also update this for each page.

Could you please verify if this is correct? The bottom/right margin portion of each page should be duplicated on the next page.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/yWorks/svg2pdf.js/issues/37#issuecomment-483677227, or mute the thread https://github.com/notifications/unsubscribe-auth/Abz4wZ__ITyETCXN_HAm45tnIeIyeQ-Eks5vhdqNgaJpZM4Q10Lq .

scottctr commented 5 years ago

I've got this close to what I want after adding the clip rect and the height and width on the svg element. A couple of things I can't figure out:

  1. Every page has a top margin of 10px instead of the expected 5px.
  2. The first page has a left margin of 10px instead of the expected 5px. All subsequent pages have the expected 5px left margin.

Any ideas appreciated.

public convertSvgToPdf(svgElement: Element, size: Size, margins: Insets) {
        svgElement = svgElement.cloneNode(/*deep*/true) as Element;

        const margin = margins ? Math.max(margins.left, margins.right, margins.top, margins.bottom) : Defaults.PdfDocumentMargin;

        const pdfPageWidth = 792;
        const pdfPageHeight = 612;
        const pdfPageWidthContent = pdfPageWidth - (margin * 2);
        const pdfPageHeightContent = pdfPageHeight - (margin * 2);

        svgElement.setAttribute('width', pdfPageWidthContent + 'px');
        svgElement.setAttribute('height', pdfPageHeightContent + 'px');

        const jsPdf = new jsPDF('l', 'pt', [pdfPageWidth, pdfPageHeight]);

        const pagesWideWithMargins = Math.floor((size.width + (pdfPageWidth - 1)) / pdfPageWidthContent);
        const pagesHighWithMargins = Math.floor((size.height + (pdfPageHeight - 1)) / pdfPageHeightContent);

        for (let verticalPageIndex = 0; verticalPageIndex < pagesHighWithMargins; verticalPageIndex++) {
            for (let horizontalPageIndex = 0; horizontalPageIndex < pagesWideWithMargins; horizontalPageIndex++) {
                const svgXOffset = pdfPageWidthContent * horizontalPageIndex;
                const svgYOffset = pdfPageHeightContent * verticalPageIndex;

                this.addClipRectDefinition(svgElement, svgXOffset, svgYOffset, pdfPageWidthContent, pdfPageHeightContent);
                svgElement.setAttribute('viewBox', this.getViewboxAttribute(svgXOffset, svgYOffset, pdfPageHeightContent, pdfPageWidthContent));
                svgElement.setAttribute('clip-path', 'url(#view-clip)');
                svg2pdf(svgElement, jsPdf, {
                    xOffset: margin,
                    yOffset: margin,
                    scale: 1
                });

                jsPdf.addPage();
            }
        }

        return jsPdf.output('datauristring');
    }

    private addClipRectDefinition(element: Element, xOffset, yOffset, width, height) {
        element.removeChild(element.lastChild);

        const definitionElement = document.createElement('defs');
        element.appendChild(definitionElement);

        const clipPathElement = document.createElement('clipPath');
        clipPathElement.setAttribute('id', 'view-clip');
        definitionElement.appendChild(clipPathElement);

        const rectElement = document.createElement('rect');
        rectElement.setAttribute('x', xOffset);
        rectElement.setAttribute('y', yOffset);
        rectElement.setAttribute('width', width);
        rectElement.setAttribute('height', height);
        clipPathElement.appendChild(rectElement);
    }

    private getViewboxAttribute(xOffset, yOffset, captureHeight, captureWidth) {
        const value = xOffset + ' ' + yOffset + ' ' + captureWidth + ' ' + captureHeight;

        return value;
    }

First page: image

Second page: image

HackbrettXXX commented 5 years ago

I found a more elegant version for the clipping: simply add a clipping rect to the pdf directly. I also slightly modified the loop. This code works for me:

  function convertSvgToPdf(svgElement, size, margins) {
    const margin = Math.max(margins.left, margins.right, margins.top, margins.bottom);

    const pdfPageWidth = 210;
    const pdfPageHeight = 210;
    const contentWidth = pdfPageWidth - (margin * 2);
    const contentHeight = pdfPageHeight - (margin * 2);

    svgElement.setAttribute('width', contentWidth + 'px');
    svgElement.setAttribute('height', contentHeight + 'px');

    const jsPdf = new jsPDF('l', 'pt', [pdfPageWidth, pdfPageHeight]);

    for (var xOffset = 0; xOffset < size.width; xOffset += contentWidth) {
      for (var yOffset = 0; yOffset < size.height; yOffset += contentHeight) {
        svgElement.setAttribute('viewBox', this.getViewboxAttribute(xOffset, yOffset, contentHeight, contentWidth));
        jsPdf.advancedAPI()
        jsPdf.rect(margin, margin, contentWidth, contentHeight).clip().discardPath()
        svg2pdf(svgElement, jsPdf, {
          xOffset: margin,
          yOffset: margin,
          scale: 1
        });
        jsPdf.compatAPI()

        if (xOffset + contentWidth < size.width || yOffset + contentHeight < size.height) {
          jsPdf.addPage()
        }
      }
    }

    return jsPdf.output('datauristring');
  }

  function getViewboxAttribute(xOffset, yOffset, captureHeight, captureWidth) {
    return xOffset + ' ' + yOffset + ' ' + captureWidth + ' ' + captureHeight;
  }
scottctr commented 5 years ago

Thanks for pdf lesson Lukas! I also now realize that my source graph has a ~5px top and left border that was throwing me off. Your help is much appreciated!