-
Notifications
You must be signed in to change notification settings - Fork 688
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
Dynamic repeater can break colorspace #473
Comments
+1 |
Prawn::Document.generate("test.pdf") do
text "Test"
# start_new_page
table([['Test', 'Test']])
start_new_page
text "Test"
number_pages "Page <page>", size: 24, style: :bold
end |
@jlduran My example is simplified; in my use case, I can't use |
Is there a reason this was closed? It hasn't had any updates, but it is still an issue. (I just tested it on the master branch and it's still exhibiting the same problem). |
Hi @dmarkow, sorry for the confusion on this. In November we did a bulk close on any ticket that did not see recent activity, because we had something like 100+ open tickets that had never been properly addressed by our core team or the submitter had gone silent (some as old as 2-3 years!). There's a mailing list post about that here: https://groups.google.com/forum/#!topic/prawn-ruby/czGB6L4HZhQ The process for re-opening a ticket was simply leaving a comment and making sure there is a reproducing example, and this ticket meets both of those criteria. I've confirmed the bug still exists in Adobe Reader on master, so we'll look into it eventually. If you want to investigate and contribute a fix or some research, we're actively monitoring the tracker now, and a patch is absolutely welcome! |
Thanks! I did a comparison with and without the table and looked at the generated PDF files. I then manually removed the chunk relating to the table from the file with a table. After doing so, there was one difference between the files. With a table on Page 1, the repeater code looks like this:
And without a table, it looks like this:
The file with a table appears to be missing a |
Oh, that makes sense! It looks like the colorspace isn't being set for the stroke color when table is used, even though the fill color is being specified. Here's what the code is doing. Set non-stroke colorspace to RGB
Set nonstroke color to black
Set stroke colorspace to RGB
Set stroke color to black
It makes sense to me that missing the colorspace definition would lead to corruption. |
And if you look at the |
Dug in a little more. Part of the table code includes a class Table
class Cell
def draw_borders(pt)
@pdf.mask(:line_width, :stroke_color) do
end
end
end
end If I remove class Document
def mask(*fields) # :nodoc:
# Stores the current state of the named attributes, executes the block, and
# then restores the original values after the block has executed.
# -- I will remove the nodoc if/when this feature is a little less hacky
puts "stroke_color before: #{stroke_color}" # Outputs 000000
stored = {}
fields.each { |f| stored[f] = send(f) }
yield
fields.each { |f| send("#{f}=", stored[f]) }
puts "stroke_color after: #{stroke_color}" # Outputs 000000
end
end The only thing I can think of is that maybe there's something about the way sending |
|
Ok, so here's the code within
The problem is within
The stroke colorspace for DeviceRGB is already set, so it's not setting it again. However, the Coming back to my earlier comparison, the same way adding the So at this point I'm not sure which is best: Adding a conditional to |
Sounds like you nailed it! Next step is to write a test to reproduce the bad behavior. You should be able to use the tests in https://github.com/prawnpdf/prawn/blob/master/spec/graphics_spec.rb as a guide. |
@dmarkow: Here are some specific lines that should be useful for testing examples: https://github.com/prawnpdf/prawn/blob/master/spec/graphics_spec.rb#L233-L281 |
Yeah, I can get a spec written up, my only question is: what's the "correct" output?
or
I would assume it would be best to err on the side of caution and explicitly set the colorspace again even if it's already set, but I'm not super familiar with the inner workings of the PDF format. |
Oh, those are not repeated statements, the capitalization matters. the lowercase ones ( If both the fill color and stroke color have been set, I'd expect to see the first example, if only the fill color has been set, I'd expect to see the second example. For details on this, see page 240 of the PDF standard: |
I understand the case difference. I'm just confused why this (the "bad" output from the original issue) doesn't work:
Currently, prawn skips the I'm inclined to remove the conditional and allow setting |
It looks like the problem is on page 2, not page 1. Yes, the stroke colorspace is showing up on the first page (it's object 5 0, which uses the stream from object 4 0). But the stream in 4 0 resets the graphics stack to the original state as its last action (see the So by the time we get to rendering the second page (8 0), the colorspace is NOT set, but there are calls to |
Note, all of the above are from your original example at the top of this issue. |
We also have a test that says the colorspace should be set on every page to please fussy readers whenever the colors have been set, but it's clear from the output that's not happening in this case. Here's the test: |
I think this has to do with the way the repeater runs. After all the pages are generated, it goes back and runs the repeater on each page. I threw a
When I add a
There must be something telling the repeater to always use the first page's graphic state, even if we're running on a different page? |
To clarify a bit, you don't necessarily want to save the graphics state of the first page, what you want to do is create a complete snapshot of the graphics state wherever the repeater is defined, fully restore it before running the repeater code, and then fully revert to whatever the graphics state was on a given page after the repeater code runs. In other words, repeaters should not assume anything about the page they're being run on, they should push a new state onto the graphics stack, replay the settings from wherever they were defined, then pop the stack to ensure that none of their settings affect the rest of the page. Right now I think Prawn is trying to do that, but corrupting things in the process due to a mismatch between Prawn's internal state and what's being written to the PDF. I'd look at the following methods for further investigate:
Sorry for the spaghetti code. Once we narrow down the source of the bug and get a test written, I'll probably refactor this somewhat, because it clearly needs it. |
Thanks for pointing me in the right direction. I think (hope) I'm making some progress. In my original example, moving the repeater code to the bottom of the It looks like when the repeater runs, it executes So whatever state was used to initialize It looks like, in the
|
@dmarkow For the most part your understanding of what's going on reflects my basic understanding of the code as well. The problem with using whatever the current page's graphic state is would be that you wouldn't have consistent behavior for your stamp across pages. For example, if you have a page that sets the fill color to red but does not set it back to black before crossing a page boundary, do you really want your repeated text to be in red on that page, and only on that page? I can't imagine that's what people would want. If we wanted to simplify the semantics of the repeaters, the decent way may indeed be to call Once we are able to implement a clear state, we'd also be able to create a "restore the graphics state from the page the repeater was defined on", so it's just a matter of what behavior feels more natural. But I definitely think we should avoid having a variable graphics state from page to page... content generated by a repeater is meant to look the same on every page, or at least start from the same base state on every page. That's what omitting the argument to |
We do have this spec, which says that we should be using the graphic state in place when we created the repeater.
Looking at some other specs, I came across Document with a stamp should save the color space even when same as current page color space. So it seems that stamps had the same issue with using the stamp on a page with a different color_space, and a solution was added to force the color space to be set if it was being set from a stamp:
I think this is why non-dynamic repeaters don't exhibit the same issue: non-dynamic repeaters are just stamps, whereas dynamic repeaters are a little more complex. |
Now, the expected behavior may be "the repeater should use the exact state in place when it was defined", but it actually doesn't appear to be doing that. I'm defining my table after defining my repeater, but it's being affected by the stroke color_space set by the table anyway. I looked at the Repeater class's initializer and noticed that it By utilizing class Repeater #:nodoc:
...
def initialize(document, page_filter, dynamic = false, &block)
...
@graphic_state = PDF::Core::GraphicState.new(document.state.page.graphic_state) I don't know how I feel about that method call, but it works (a better way might be to add a That being said, if made a table and then defined my repeater, I'd still have the same issue all over again, even with the added line above. I agree that repeaters should not use the current page's state. However, I also don't think they should inherit anything when they are defined. Rather, they should be completely separate entities, starting with a perfectly clear state (e.g. black fill/stroke, empty color space, etc.). I think inheriting graphic state attributes that were defined before the repeater is misleading. And by inheriting the current graphic state, moving the repeater code to different parts of the Right now, the only way to get the repeater to behave completely independent of the rest of the document's graphic state is to define the repeater before you do anything else (at least once the |
Thanks a lot for the further research and pull request, I'll take a closer look at this in the next couple days. |
Thanks a ton for all your help. No rush, my branch is fixing the problem for me for the time being. |
Note to self for future investigation: It looks like if we push newly initialized graphics state object rather than the graphics state of whatever page the repeater is defined on, that would give us a blank slate to work with. Unsure whether we cache those values elsewhere in document though. |
It's been a while since I looked at this, but I'm pretty sure it was resolved by #635, so I'm gonna close it. Thanks! |
Thanks, please re-open if you run into the issue again. |
Using Adobe Acrobat or Reader as my viewer (both on OS X and Windows), with the code above, the "Page #" repeater only shows up on the first page. The second page still outputs the "Test" text, but there is no "Page 2" at the top.
If I uncomment the
start_new_page
call before thetable
call, then all 3 pages get "Page 1", "Page 2", "Page 3".OS X's Preview.app works fine, it's only Acrobat that has the problem (it seems common that Preview is a little more forgiving with PDF files?).
For now my workaround is to just not use dynamic repeaters (meaning I can't refer to the page count in them).
The text was updated successfully, but these errors were encountered: