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

Improved gradient support #175

Merged
merged 7 commits into from
Dec 24, 2024
Merged

Conversation

tonymarklove
Copy link
Contributor

@tonymarklove tonymarklove commented Nov 7, 2024

Overview

This pull request does three main things:

  • Support transparent gradients with stop-opacity.
  • Support repeated and reflected gradients using spreadMethod.
  • Fixes some bugs with gradient transformations that were incorrectly being performed in PDF space.

Intro

Following up from my fairly small previous PR, I was doing a lot of investigation into the rendering of gradients. I found that when SVGs contained complex gradient transforms, there were some bugs in the prawn-svg implementation.

It turned out that Prawn makes a bunch of assumptions about how gradients and gradient matrices are handled, so the easiest way forward was to reimplement Prawns gradient handling. Once I had done that, it made sense to support transparency and repeated gradients as a way of justifying the new implementation.

I'm aware this has ended up being a rather large PR, so feel free to ask questions if anything doesn't make sense.

The Visuals

I created an example SVG to stress test the various gradient options. Here is what it looks like in Chrome:
chrome

But here is what it looks like rendered to PDF with the latest release of prawn-svg:
old

With the code in this PR, it now renders like this, which should be very close to the Chrome example:
new

Technical Details

Performing transforms in SVG-space

The bugs that I was seeing with gradient transforms, which show up as white boxes in the image above, were mostly due to a mismatch between PDF-space and SVG-space, where the origins are at bottom-left and top-left respectively.

I have switched this round so that the gradient transforms are read from the SVG in SVG-space. They are finally combined with an svg_to_pdf_matrix which transforms the final coordinates back into PDF page-space. This combined gradient matrix can be directly inserted into the PDF as the pattern matrix, and everything works out correctly.

AdditionalGradientTransforms is no longer needed

Because of the above gradient matrix, we no longer need to monkey-patch the gradient_coordinates method inside Prawn, and AdditionalGradientTransforms can be removed.

Unclamping objectBoundingBox

When gradientUnits="objectBoundingBox" is set in the SVG, the coordinate system for the provided attributes is relative to the element's bounding box. Previously this was being clamped to a range of 0-1, but there is no reason this needs to be the case. It is valid to make a gradient larger than the bounding box, so I have removed that restriction with the new percentage_or_proportion method.

Prawn::SVG::GradientRenderer

Prawn::SVG::GradientRenderer is where the core of the gradient generation logic sits. This should really go into Prawn itself, but I didn't want to have to submit pull requests to both projects at once to get it working.

I will also try to get a PR opened for Prawn with these changes. If that gets accepted GradientRenderer can probably be removed from prawn-svg entirely.

Because I decided to wrap the GradientRenderer in a class, rather than mix it in as a Prawn::Document.extensions module, I did need to call private methods with send in three places. Hopefully that seems reasonable, for the added encapsulation gained.

Integration tests

I have checked all impacted samples generated by the integration tests. Due to the new gradient generation code, any PDFs with a gradient will get a modified MD5 hash now.

These PDFs have changed since the last test run:
  spec/sample_output/gradient_transform.svg.pdf
  spec/sample_output/gradients-cmyk.svg.pdf
  spec/sample_output/gradients.svg.pdf
  spec/sample_output/ordering.svg.pdf
  spec/sample_output/radgrad01-bounding.svg.pdf
  spec/sample_output/radgrad01.svg.pdf

I have checked all of these by hand, and they either look identical or improved by this PR.

- Fixes transformations that were incorrectly being performed in PDF space.
- Support transparent gradients with `stop-opacity`.
- Support repeated and reflected gradients using `spreadMethod`.
@mogest
Copy link
Owner

mogest commented Dec 21, 2024

Hi @tonymarklove, thank you for this excellent PR! I'm sorry it's taken me such a long time to get to it. I'll review it over the next few days.

@tonymarklove
Copy link
Contributor Author

@mogest no problem. I know it's a big change! Hopefully it makes sense once you take a look.

Copy link
Owner

@mogest mogest left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks once again @tonymarklove, this is both extensive and beautiful and will make a lot of prawn-svg users very happy!

Functionality looks perfect. Just have a few comments on the code below; if you're short on time then please send back to me and I'm happy to make the minor tweaks.

lib/prawn/svg/elements/gradient.rb Outdated Show resolved Hide resolved
lib/prawn/svg/transform_utils.rb Outdated Show resolved Hide resolved
lib/prawn/svg/extension.rb Outdated Show resolved Hide resolved
lib/prawn/svg/gradient_renderer.rb Outdated Show resolved Hide resolved
@mogest mogest merged commit 4da7475 into mogest:main Dec 24, 2024
6 checks passed
@mogest
Copy link
Owner

mogest commented Dec 24, 2024

Thank you so much! I'll do a gem release in the next couple of days.

@mogest
Copy link
Owner

mogest commented Dec 24, 2024

Looks like there's a regression here. I was looking at mojavelinux's test case on #153 which was working in the latest published gem but only displays a gradient on the last image with the new main branch.

I think it's that the cache key has to also include the position on the screen. Something about the gradient also including coordinates inside it? I can't remember, it's been too long since I worked with this stuff!

I'll have a look over the next few days anyway; if the answer is obvious to you @tonymarklove then let me know otherwise I will grind it out. :)

@mogest
Copy link
Owner

mogest commented Dec 24, 2024

Quick check before bedtime confirms that replacing the SHA1 digest with a random key for each invocation of GradientRenderer#draw results in the regression being fixed. Don't know why yet tho.

@tonymarklove
Copy link
Contributor Author

@mogest yes, I think you are right. Because we pull in the position using prawn.current_transformation_matrix_with_translation and prawn.bounds those probably need to be included in the cache key too.

@mogest
Copy link
Owner

mogest commented Dec 26, 2024

At that point, am I right in thinking that we might as well just make a random/globally-incrementing key for every new gradient paint? If you have to be painting the same size, in the same area, with everything else the same to share the pattern, I can't imagine why anyone would want to do that twice?

@tonymarklove
Copy link
Contributor Author

Yes I think that makes sense. I considered doing that from the start but I didn't want to stray too far from the existing implementation in Prawn. But as you say, I don't think it's really possible to reuse a gradient so the cache key isn't doing anything useful.

@mogest
Copy link
Owner

mogest commented Dec 26, 2024

OK great! I've added ccbbdf3 and that seems to fix the regression.

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

Successfully merging this pull request may close these issues.

2 participants