Warm tip: This article is reproduced from serverfault.com, please click

Convert SVG to PDF with iText, SVG is not fully displayed in PDF

发布于 2020-12-02 12:50:39

I'm working on adding SVG image to PDF pages.

Firstly, I tried SvgConverter.createPDF to check if iText works with SVG.

Some svgs are fine. Unfortunately, the following SVG (using percentage position) is not correctly displayed / positioned in PDF.

My Conversion code

    String svgImage = resourceFile("svg/circle-sRGB-rgb.svg");
    String destination = targetFile("svg-itext_SVG2PDF.pdf");

    try(InputStream svgStream = new FileInputStream(new File(svgImage))) {
        try(OutputStream pdfStream = new FileOutputStream(new File(destination))) {
            SvgConverter.createPdf(svgStream, pdfStream);
        }
    }

SVG file

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 80 60">
    <circle cx="50%" cy="50%" r="30" fill="rgb(10, 200, 200)"/>
</svg>

SVG Preview

enter image description here

Generated PDF Preview

enter image description here

If I change the position (cx, cy) in SVG to absolute values, output seems to be good.

I also tried converting the SVG to an xObject, but it also didn't help.

private void addSvgImageToPdfPage(PdfPage page, String svgContent, float x, float y, float w, float h) {
    // convert svg to xObject
    PdfFormXObject xObject = SvgConverter.convertToXObject(svgContent, page.getDocument());

    // create page canvas
    PdfCanvas pdfCanvas = new PdfCanvas(page);

    // create AT
    AffineTransform at = AffineTransform.getTranslateInstance(x, y);
    at.concatenate(AffineTransform.getScaleInstance(w / xObject.getWidth(), h / xObject.getHeight()));

    float[] matrix = new float[6];
    at.getMatrix(matrix);

    // add svg xObject to canvas
    pdfCanvas.addXObjectWithTransformationMatrix(xObject, matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]);

    pdfCanvas.release();
}
Questioner
leoleozhu
Viewed
1
500 2020-12-18 17:25:39

As @A.Alexander correctly mentioned, iText out of the box doesn't support relative parameters for the circle element. However, you can register your own renderer for the circle tag (instead of CircleSvgNodeRenderer) that is able to deal with percents.

There is the parseAbsoluteLength method in AbstractSvgNodeRenderer which can be useful for that.

Custom renderer.

import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.svg.SvgConstants;

import com.itextpdf.svg.renderers.ISvgNodeRenderer;
import com.itextpdf.svg.renderers.SvgDrawContext;
import com.itextpdf.svg.utils.DrawUtils;

/**
 * {@link ISvgNodeRenderer} implementation for the &lt;circle&gt; tag.
 */
public class CustomCircleSvgNodeRenderer extends AbstractSvgNodeRenderer {

    private float cx;
    private float cy;
    float r;

    @Override
    protected void doDraw(SvgDrawContext context) {
        PdfCanvas cv = context.getCurrentCanvas();
        cv.writeLiteral("% ellipse\n");
        if (setParameters(context)) {
            // Use double type locally to have better precision of the result after applying arithmetic operations
            cv.moveTo((double) cx + (double) r, cy);
            DrawUtils.arc((double) cx - (double) r, (double) cy - (double) r, (double) cx + (double) r,
                    (double) cy + (double) r, 0, 360, cv);
        }
    }


    private boolean setParameters(SvgDrawContext context) {
        cx = 0;
        cy = 0;
        if (getAttribute(SvgConstants.Attributes.CX) != null) {
            cx = parseAbsoluteLength(getAttribute(SvgConstants.Attributes.CX), context.getCurrentViewPort().getWidth(), 0.0f, context);
        }
        if (getAttribute(SvgConstants.Attributes.CY) != null) {
            cy = parseAbsoluteLength(getAttribute(SvgConstants.Attributes.CY), context.getCurrentViewPort().getHeight(), 0.0f, context);
        }
        if (getAttribute(SvgConstants.Attributes.R) != null
                && parseAbsoluteLength(getAttribute(SvgConstants.Attributes.CY), context.getCurrentViewPort().getHeight(), 0.0f, context) > 0) {
            r = parseAbsoluteLength(getAttribute(SvgConstants.Attributes.CY), context.getCurrentViewPort().getHeight(), 0.0f, context);
        } else {
            return false; //No drawing if rx is absent
        }
        return true;
    }

    @Override
    public ISvgNodeRenderer createDeepCopy() {
        CustomCircleSvgNodeRenderer copy = new CustomCircleSvgNodeRenderer();
        deepCopyAttributesAndStyles(copy);
        return copy;
    }

}

Then you need to customize DefaultSvgNodeRendererFactory.

New Factory must create instance of CustomCircleSvgNodeRenderer every time SvgConverter renders a circle. e.g

public class CustomRendererFactory extends DefaultSvgNodeRendererFactory {

    @Override
    public ISvgNodeRenderer createSvgNodeRendererForTag(IElementNode tag, ISvgNodeRenderer parent) {
        if (SvgConstants.Tags.CIRCLE.equals(tag.name())) {
            return new CustomCircleSvgNodeRenderer();
        }
        return super.createSvgNodeRendererForTag(tag, parent);
    }
}

Enforce SVGConverter to use CustomRendererFactory

You should add this factory to ConverterProperties instance and then use methods from SvgConverter that have a parameter of type ISvgConverterProperties.

    SvgConverterProperties properties = new SvgConverterProperties();
    properties.setRendererFactory(new RendererFactory());
    // xObject
    SvgConverter.convertToXObject(svg, pdfDoc, properties);
    // or pdf
    SvgConverter.createPdf(svgStream, pdfStream, properties);