prawnpdf / prawn

Fast, Nimble PDF Writer for Ruby
https://prawnpdf.org
Other
4.67k stars 688 forks source link

Question about re-using some Prawn code to render appearance streams for AcroForm annotations #1312

Closed ndbroadbent closed 9 months ago

ndbroadbent commented 1 year ago

Hello! I've been using Prawn for quite a while, and I need to add some missing features in my own code to handle AcroForm annotations. I've been taking some inspiration from https://github.com/hannesg/prawn-blank for text fields, but there are some missing features when it comes to rendering text in a text field. (Specifically when PDFs are opened in Preview, Safari, and DocuSign.) I'm also using prawn-templates to add text and fields to existing PDFs. I understand that none of these use-cases are officially supported by the Prawn maintainers, but I would still really appreciate some help or advice if you have some time!

Prawn has lots of great features for rendering text boxes, vertical alignment, shrink to fit, word wrapping, etc. and it actually writes all the same instructions that I would need for my AcroForm annotation appearance streams. I don't want to re-implement all the text layout stuff from scratch, so I was just wondering if there is a way that I can leverage the existing code in an abstract way, and pull out the text rendering instructions?

It might be helpful if I provide an example to show what I'm talking about. I have generated two PDFs - One with static text, and one with an Acroform field.

The static text PDF has /Contents stream like this, to render some example text onto the page:

Screenshot 2023-09-12 at 4 37 52 PM
BT
220.6823 663.7399 Td
/F1.0 11 Tf
[consectetur deser -15 unt amet ] TJ
ET

The editable AcroForm PDF has a Field with an /AP stream (appearance) like this:

Screenshot 2023-09-12 at 4 36 56 PM
BT
/F1.0 11 Tf
0.2667 0.2667 0.2667 rg
2 4.572580645161338 Td
(consectetur deserunt amet) Tj
ET

You can see that the streams are generating very similar instructions. The only problem is that the appearance stream is being manually generated by the prawn-blank gem, with lots of my own customizations and bug fixes. Today I had to spend hours figuring out rendering problems related to the ascent and descent ratios of fonts, to get the text to show correctly in Preview, Safari, and DocuSign.

Is there an easy way that I could re-use some of the code in Prawn::Text::Formatted to generate these appearance streams? I'd love to be able to re-use all the existing logic in Prawn that can lay out text, shrink to fit, and word wrap across multiple lines, instead of copy/pasting or rewriting all this logic myself.

I can see that this code often uses a @document instance variable, but I seem to remember that Prawn has a way to start a new stream and pass in a block, so it's writing all the instructions into an isolated section. (I don't know if that makes sense!)

I've been using the dry_run argument in some other parts of my code to calculate heights, etc.

box = Prawn::Text::Formatted::Box.new(lines, text_box_args.merge(document: pdf))
box.render(dry_run: true)

I was just wondering if there's a way I can get it to render all the text instructions, so that I can pull them out and place them into an appearance stream for annotations?

Thanks a lot for your help!

pointlessone commented 1 year ago

I think what you're trying to do might be hard to achieve with Prawn. The main reason is that Prawn commits everything the moment you call a method. Basically anything you do mutates the document in some way. dry_run is kind of an exception because it was specifically made to not do anything to the document just to calculate the text layout (end even then I'm not 100% sure it doesn't leave a trace).

That said, you might want to look at the Prawn::Stamp module in prawn and PDF::Core::Page::stamp_stream‎ method in pdf-core. stamp_stream is probably what you're after. It should give you a content stream in your dict. Though keep in mind that while content stream is preserved all other objects are dumped into the main document. I guess you want it anyway so that the content stream could reference those objects but if you're going to manipulate the content stream you might end up with a few loos object in your document. It probably won't break them but the objects would still take space.

pointlessone commented 9 months ago

I believe this has been addressed. Feel free to reopen if there's still an outstanding issue. Or start a new Discussion if there are more questions.