Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discrepancy between PDF & PNG when translating an SVG that contains a linear gradient #19

Closed
larrylynn-wf opened this issue Sep 18, 2019 · 14 comments

Comments

@larrylynn-wf
Copy link
Contributor

Greetings Rototor.

First, I wanted to write and say thank you for your work on this awesome library. We're getting great results using it to embed SVGs in PDFs.

We did notice something that looked a bit weird when we had SVGs representing a chart that had a linear background gradient. When the SVG contains a linear gradient, a gradient vector image is embedded in the PDF that has a different orientation than the original SVG as rendered in a browser. See attached screenshot.
2019-09-18_1027

When I first noticed the visual discrepancy between SVG input & PDF output, I thought that it might be a bug in pdfbox-graphics2d. After reading up on the SVG and PDF specifications, I no longer think it's a bug.

The SVG specification
https://www.w3.org/TR/SVG11/pservers.html#LinearGradients
says that

When the object's bounding box is not square, the gradient normal which is initially perpendicular to the gradient vector within object bounding box space may render non-perpendicular relative to the gradient vector in user space.

However, the PDF specification states, in the section for "Type 2 (Axial) Shadings"
https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf

Type 2 (axial) shadings define a color blend that varies along a linear axis between two endpoints and extends indefinitely perpendicular to that axis.

So, I think that the root issue is a mismatch between the 2 specifications. In PDF, the gradient normal is always perpendicular to the gradient vector. In SVG, the gradient normal is sometimes perpendicular. Therefore, translating the SVG to a PDF gradient that has the gradient normal going perpendicular is technically correct according to my reading of the specification.

However, since pdfbox-graphics2d is intended as a bridge between SVGs and PDFBox, I think it's a reasonable feature request to ask for an option that allows us to make linear gradients look the same in a PDF as they do when the input SVG is rendered as a browser.

I've opened a pull request that can be used to demonstrate this issue:
#18

2019-09-18_1045
Running mvn test will translate that SVG to both a PDF and a PNG.
Note that the orientation of the gradient in the PDF does not match the orientation of the gradient in the PNG. See attached screenshot.

@rototor
Copy link
Owner

rototor commented Sep 19, 2019

Thanks for the detailed report and the test case. I'll try to look into, but I will likely have no time for that this week.

@rototor
Copy link
Owner

rototor commented Sep 22, 2019

Oh, I should not write fix in the commit message, as it would close the issue ...

I'm nearly there:
image

The start and end points need to be moved future away from the center. I tried different things, but that did not work. I had to disable this for now as it breaks other gradient SVG test cases. I assume as soon as the scaling is right the other test cases would be ok with that code. So this is WIP at the moment.

If you can spare some time could you try your luck here? It's in PdfBoxGraphics2DPaintApplier.java:374 and currently disabled:

            /*
             * Special handling for Batik
             */
            if (!isNormalParallel && false)

@larrylynn-wf
Copy link
Contributor Author

Thank you Emmeran. I will work on this today.

larrylynn-wf added a commit to larrylynn-wf/pdfbox-graphics2d that referenced this issue Sep 23, 2019
@larrylynn-wf
Copy link
Contributor Author

Hi Emmeran. Thanks again for your work on this issue.

I've pulled down your new code & re-enabled your updated logic by changing 'false' to 'true' on PdfBoxGraphics2DPaintApplier.java:374. I'm able to reproduce your results. When I translate long-gradient.svg to an SVG using your test harness, the PDF looks very close to the SVG as rendered in a web browser.

Unfortunately, I don't believe that you've found the general solution to this issue. I processed some other SVGs using your updated code and the translation to PDF looks quite a bit different. I've checked in a new SVG named 'tall-gradient.svg' and pushed it up to a branch in order to demonstrate this
https://github.com/rototor/pdfbox-graphics2d/compare/master...larrylynn-wf:gradient-issue-2?expand=1

Here's a screenshot of my results
2019-09-23_1553
It looks to me like the normal of the gradient (the white band) has been pushed too far to the right.

@rototor
Copy link
Owner

rototor commented Sep 24, 2019

TBH I'm not surprised. I don't really understand what

may render non-perpendicular relative to the gradient vector in user space

should mean. Especially that "may render" seems like "sometimes", which makes not that much sense to me... I did only try&error, as I not really understand yet whats wrong here.

I just found the draft SVG 2 spec, which may clear up that thing a bit.
https://svgwg.org/svg2-draft/pservers.html#LinearGradientElement

@larrylynn-wf
Copy link
Contributor Author

Hi Emmeran.
I made an attempt to implement the feature requested in Issue 19. My PR is here:
#20

I found this stack overflow post to be useful in clarifying the situation
https://stackoverflow.com/questions/50617275/svg-linear-gradients-objectboundingbox-vs-userspaceonuse
So, it looks like there are 2 modes for rendering linear gradients in SVGs: objectBoundingBox & userSpaceOnUse. I believe that the code in the master branch of pdfbox-graphics2d renders SVG gradients properly if they are using the userSpaceOnUse mode. My code is an attempt to add support for SVG gradients in the objectBoundingBox mode. I think objectBoundingBox mode is the default for SVG linear gradients, so hopefully this is a useful addition to the library.

The basic approach I've used is to start with a special case where SVG linear gradient default layout matches PDF axial gradient layout. The special case is a perfect square. It doesn't make sense to have a gradient over a rectangle with a zero or negative width or height. So for my base case, I use a 1x1 square. Then I use the affine transform that we already have on the state object to warp the space of the box, scaling it up to a rectangle of arbitrary size. Warping the space after applying the gradient results in a painted rectangle that looks like the SVG gradient in objectBoundingBox mode rendered in a browser.

I've isolated my code in a subroutine named linearGradientObjectBoundingBoxShading. I'm switching on a new boolean flag on PdfBoxGraphics2DPaintApplier which is named emulateObjectBoundingBox. The default of that flag is false, so without any configuration, the behavior of buildLinearGradientShading() is unchanged from that of master. My gradient layout mode can be enabled with

PdfBoxGraphics2DPaintApplier paintApplier = new PdfBoxGraphics2DPaintApplier()
paintApplier.setLinearGraidientEmulateObjectBoundingBox(true);
pdfBoxGraphics2D.setPaintApplier(paintApplier);

Where pdfBoxGraphics2D is an instance of the PdfBoxGraphics2D class.

You can check the output of my code by running mvn test and looking at the output files corresponding the the 4 new SVGs that I've added.

I expect that you'd probably want to refactor my code because the coding style is not in harmony with yours (especially in the way that I bolted on extra functionality to the existing test harness). But I think that the approach is sound, and I hope this code is useful at least as an example.

Please note that my code is not a general purpose solution to the linear gradient layout problem. I tried running my code against the rest of the test docs in ./src/test/resources/de/rototor/pdfbox/graphics2d/. Most of the SVGs look good after translation, but not all. Specifically, compuserver_msn_Ford_Focus.svg has serious regressions when translated with my code. I think that this is because that SVG has a number of linear gradients with a mix of both objectBoundingBox & userSpaceOnUse modes. I do not expect my code to handle the userSpaceOnUse mode properly.

rototor added a commit that referenced this issue Oct 9, 2019
… paint transform. #19

If it is square than we are in UserSpaceOnUse mode, otherwise we are in ObjectBoundingBox mode. But we only check that if we are using Batik.
@rototor
Copy link
Owner

rototor commented Oct 9, 2019

I've removed the EmulateObjectBoundingBox boolean and instead used the scaling factors of the transform matrix to detect object bounding box. This seems to work correctly, and also does not break the more complex SVGs in the test. Please verify that it works for all your cases, then I will release a new version.

Thanks for your help with this issue!

@larrylynn-wf
Copy link
Contributor Author

I will test this today. Thanks again for your work on this issue.

@larrylynn-wf
Copy link
Contributor Author

Hi Emmeran,
I've tested out the new code in master with some help from one of my colleagues.

My colleague found a use case that I missed that introduces a regression. The problem is in the code that I submitted in #20. If an SVG had a gradient that was exactly vertical or horizontal in orientation, I created a rectangle of height or width of zero, which was later used as a clip path. This had undesirable results.

I've created a pull request with an SVG that demonstrates this problem as well as a bugfix to resolve it:
#21

I tested again with the codebase that included #21. The output PDFs looked much improved. All of the PDFs produced by testGradientSVGEmulateObjectBoundingBox() looked perfect as far as I could tell. I did find one other regression in the test for displayWebStats.svg.

Testing on master plus PR 21, the buttons in the interactive web statistics dashboard display as rectangles. Testing on the latest tagged release, graphics2d-0.24, those buttons are displayed with rounded corners. There is a similar problem with the grippies down in the bottom of the SVG. See attached screenshot.

2019-10-09_1718

I'm not quite sure what is going on there, but I suspect that it has something to do with the r attributes in the rectangle

<rect id="grip" x="-6" y="-12" width="12" height="24" rx="8" ry="8" fill="inherit" stroke="none"/>

I suspect that the clipping box that gets injected for gradients interferes with the rounding defined in those attributes. I don't know how to resolve that yet.

rototor added a commit that referenced this issue Oct 10, 2019
Issue-19-bugfix | regression with vertical or horizontal gradients #19
rototor added a commit that referenced this issue Oct 10, 2019
#19

But we also must ensure that we not do an empty clip bei accident after
that. So there is a new flag to keep track if a path is on the content
stream. This fixes the round boxes in the displayWebStats case.
@rototor
Copy link
Owner

rototor commented Oct 10, 2019

The problem here is, that the state.contentStream.addRect(...) you added in the object bounding box case should set a clipping path. This does not always work as expected, as you not explicit clip but rely on the fact the the graphics adapter does a clip anyway. But it does not do that in all cases.

But also just calling state.contentStream.clip() after the addRect() does not work, because that can later lead to an empty clipping path. I.e., in some case clip() is called again after that. macOS preview etc. don't care about that, but Acrobat Reader really does not like a clip() without a path and just aborts rendering.

So to correctly fix that there is a new boolean flag in the Graphics2D adapter which tracks if we currently have a path on the content stream which has not been closed. And when we try to clip it first checks if there is a path and only clips in that case.

Now that dashboard case also works for me. Please test again, thanks.

@larrylynn-wf
Copy link
Contributor Author

Hi Emeran,
Thank you for your recent code updates.

As it happens I also thought about adding boolean flag in the Graphics2D adapter to help manage clipping. But I thought that you might be more amenable to accepting a code contribution from a new contributor if I could isolate all of my changes to a single method. I guess I should have explored all options.

I've tested the most recent code in master. I can find no regressions in the translation of any of the SVGs in ./src/test/resources/de/rototor/pdfbox/graphics2d/. SVGs with linear gradients in both the objectBoundingBox and the userSpaceOnUse modes now seem to translate properly. Rectangles with rounded corners are not adversely effected. I also did some exploratory testing translating SVGs generated by our own internal systems. The exploratory testing passed. All of the test cases I can fabricate look great when translated with the new codebase.

We would be interested in using this code as soon as a new version of pdfbox-graphics2d is released.

@rototor
Copy link
Owner

rototor commented Oct 10, 2019

@larrylynn-wf Nice that it works for you. I don't mind any contribution as long as it's sound.

I've tried to released version 0.25, but sonatype currently has problems (see https://status.maven.org/incidents/t40ylgmsmbl2?u=2thbn6r7vdfk), so I'll retry later. I'll give you an update if publishing works again and the releasing worked.

@rototor
Copy link
Owner

rototor commented Oct 12, 2019

I could finally publish version 0.25, so you can use that now. Thanks again for the report and help with this issue.

@rototor rototor closed this as completed Oct 12, 2019
@larrylynn-wf
Copy link
Contributor Author

We have a preliminary build of our software integrated with pdfbox-graphics2d version 0.25. The results look great so far.

Thanks again for your work on this issue, and for my part, I was happy to be of assistance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants