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

Allow to insert screen-only images that do not get printed #441

Open
Lucas-C opened this issue May 19, 2022 · 17 comments
Open

Allow to insert screen-only images that do not get printed #441

Lucas-C opened this issue May 19, 2022 · 17 comments

Comments

@Lucas-C
Copy link
Member

Lucas-C commented May 19, 2022

The idea comes from one of @digidigital recipes: https://github.com/digidigital/Extensions-and-Scripts-for-pyFPDF-fpdf2/tree/main/visibility (cf. #274)

This script allows to restrict the rendering of some elements to screen or printout. This can be useful, for instance, to put a background image or color that will show on screen but won't print.

To implement this feature, fpdf2 will need to insert Optional Content Groups, and Optional Content Usage Dictionaries.
Example from the spec:

% Within a content stream
...
/OC /OC2 BDC
% Draws a black rectangle frame
0 g
4 w
100 100 412 592 re s
EMC
...
<<
% The resource dictionary
/Properties << /OC2 20 0 R >>
>>
20 0 obj
<<
% Optional content usage dictionary
/Type /OCG
/Name (print)
/Usage << /Print << /PrintState /OFF >> >> % Maybe also set /View << /ViewState /ON >> ?
>>
endobj

There is some usage example in Python, once implemented:

from fpdf import FPDF

pdf = FPDF()
pdf.add_page()
pdf.image("docs/fpdf2-logo.png", x=20, y=60, visibility="ViewerOnly")  # this image won't be included when the document is printed

visibility would be an optional parameter that could also receive the value "PrintOnly",
meaning that the image would be only displayed when printed.


By implementing this feature you, as a benevolent FLOSS developper, will provide access to the large community of fpdf2 users to a useful PDF functionality.
As a contributor you will get review feedbacks from maintainers & other contributors, and learn about the lifecycle & structure of a Python library on the way.
You will also be added into the contributors list & map.

This issue can count as part of hacktoberfest

@Brianrmendes
Copy link

I would like to take up the issue!

@Lucas-C
Copy link
Member Author

Lucas-C commented Jul 21, 2022

Awesome, great!

There are a few pointers:

  • images are already inserted in a BDC / EMC marked sequence block when a title or alt_text is provided:
    https://github.com/PyFPDF/fpdf2/blob/2.5.5/fpdf/fpdf.py#L3366 There is just a need to add support for /OC entries in the _marked_sequence() method
  • the resource dictionary is already produced by fpdf2 there: https://github.com/PyFPDF/fpdf2/blob/2.5.5/fpdf/fpdf.py#L4180 The /Properties entry will just have to be added to it.
  • from the spec, I think /Properties does not have to be a reference to a PDF object, it can jut be a dictionary.
    This would mean that the following should work, with the benefit of being simpler and not producing an extra PDF object:
/Properties <<
  /OC1 <<
    /Type /OCG
    /Name (print)
    /Usage << /Print << /PrintState /OFF >> >> % Maybe also set /View << /ViewState /ON >> ?
  >>
  /OC2 << ... >>
>>
  • I think two global optional content properties should be enough. /OC1 & /OC2 are not very explicit so maybe /PrintOnly & /ViewerOnly?

@Brianrmendes
Copy link

Hey! @Lucas-C
I needed some explanation on the following 1) What exactly are BDC/EMC
2) In the _marked_sequence(), how exactly do we add support for /OC entries
3) In the _putresourcedict do we just add properties in the self._out("ProcSet [/PDF /TEXT.... /Properties]") something like this? And if yes then where do you expect us to write the properties function

(Ps:- I'm sorry for asking so many questions)

@Lucas-C
Copy link
Member Author

Lucas-C commented Aug 5, 2022

  1. What exactly are BDC/EMC

For detailed explanations, please refere to the 1.7 PDF spec.
Quoting it:

Marked-content operators may identify a portion of a PDF content stream as a marked-content
element of interest [...] The BMC, BDC, and EMC operators shall bracket a marked-content sequence of objects within the
content stream.


  1. In the _marked_sequence() method, how exactly do we add support for /OC entries

This method currently contains a call to self._out(f"/P <</MCID {mcid}>> BDC").
It should be modified so that, if the method receives the optional opt_content_id argument,
an extra f"/OC {opt_content_id}" is inserted, similar to the example provided in this issue description.


  1. In the _putresourcedict do we just add properties in the self._out("ProcSet [/PDF /TEXT.... /Properties]") something like this?

That's the idea yes!
You will probably have to perform some trial & error until you get a valid PDF file.

I suggest the following approach:

  1. Create a new python file in test/, by copying the existing tests.
  2. Write a basic usage scenario and call assert_pdf_equal(pdf, "test_name.pdf", tmp_path, generate=True).
    The generate=True argument is very useful to write new tests as it will generate the reference PDF file.
    Once you'll be happy with the PDF file generated, you will just have to remove generate=True and you'll get a fine non-regression unit test.
  3. Use qpdf --check $pdf_filepath to ensure the PDF has a valid syntax
  4. Finally, try to print the PDF to a virtual printer (e.g. Microsoft Print to PDF) to check if it functionally behaves a expected.

And if yes then where do you expect us to write the properties function?

I don't understand this question, sorry.
The PDF will not contain any "function", only configuration entries, like /Properties.
If your question is about "where in fpdf2 source code" should you put the extra code required by this feature,
then there is not a single answer, as several methods will change: image(), _marked_sequence() and _putresourcedict() at least.


(Ps:- I'm sorry for asking so many questions)

Please don't excuse yourself for asking questions!
It's a necessity to learn, and you're welcome 😊

The presence of the good first issue tag on this issue indicates that maintainers expect questions and clarifications to be given, in order for newcomers to be able to contribute to the project.

Thank you for your interest in implementing this!

@Lucas-C
Copy link
Member Author

Lucas-C commented Aug 25, 2022

@Brianrmendes: are you still working on this feature, or is it up-for-grabs for another contributor to implement this?

@Brianrmendes
Copy link

@Lucas-C Hey! I'm sorry for the delay, I've been having submissions and exams lately in my college. I'm working on it though!

@Lucas-C
Copy link
Member Author

Lucas-C commented Aug 25, 2022

@Lucas-C Hey! I'm sorry for the delay, I've been having submissions and exams lately in my college. I'm working on it though!

OK, great 😊 I hope your exams will go fine!
No worries, I was just curious if you lost interest in this feature, take your time on this!

@Lucas-C
Copy link
Member Author

Lucas-C commented Sep 13, 2022

Hi @Brianrmendes!
Do you think you'll have time to submit a PR before or during hacktoberfest?
Otherwise I think we could let potential hacktoberfest participants come and give it a try 😊

@Brianrmendes
Copy link

We can absolutely let potential hacktoerfest participants take part in this! I'm having a hard time in solving the issue, so it would be great to see how others approach this issue!

@devdev29
Copy link

devdev29 commented May 1, 2023

So I was looking at this issue yesterday and tried to implement a nested ViewOnly/PrintOnly dictionary into the properties entry directly as Lucas suggested but that seems to create very unreadable deeply nested code -

properties_dict = pdf_dict(
            {
                "/Properties": pdf_dict(
                    {
                        "/ViewerOnly": pdf_dict(
                            ...
                        )
                    }
                )
            }
        )

So I can either create the ViewerOnly/PrintOnly dictionaries separately and just embed them into the properties dict like so -

viewer_only = pdf_dict(...) 
print_only = pdf_dict(...)
properties_dict = pdf_dict(
    {
        ...
       "/ViewerOnly": viewer_only,
       ...
    }
) 

Or I can define separate pdf objects and pass them as a reference into the properties dict (this is the approach Lucas said to avoid as it creates extra pdf objects but seems cleaner to me)

What do you guys think? @Lucas-C @gmischler

@Lucas-C
Copy link
Member Author

Lucas-C commented May 1, 2023

So I can either create the ViewerOnly/PrintOnly dictionaries separately and just embed them into the properties dict like so

This seems like the best option to me 😊

@Lucas-C
Copy link
Member Author

Lucas-C commented Aug 2, 2023

Hi @devdev29!

Are you still working on this feature? 😊

Otherwise, no worries, but it may be worth mentioning it so that other contributors could consider working on this

@devdev29
Copy link

devdev29 commented Aug 2, 2023

Heyy, I'm still working on it but had a hectic summer, but of course other contributors are welcome to work on it since I'm not sure when I'll be able to finish on it. (I probably should've mentioned this sooner, my bad😶‍🌫️)

@Lucas-C
Copy link
Member Author

Lucas-C commented Aug 2, 2023

Heyy, I'm still working on it but had a hectic summer, but of course other contributors are welcome to work on it since I'm not sure when I'll be able to finish on it. (I probably should've mentioned this sooner, my bad😶‍🌫️)

Alright, no worries! 😊👍

I wish you a great summer!

@soniabhishek46
Copy link

Hi Lucas,
I would like to take up this issue, I am currently trying to understand the code, and the changes I need to make.
Thanks

@Lucas-C
Copy link
Member Author

Lucas-C commented Oct 6, 2024

Hi @soniabhishek46

You are very welcome to tackle this issue, thank you!

Given the last message from @devdev29, I think this totally fine for you to submit a PR to implement this.

Are you willing to contribute as part of hacktoberfest?

If you have any questions, go on and ask 🙂

@soniabhishek46
Copy link

Yes sure, Lucas, I can contribute as part of Hacktoberfest. I am working on it, but only after my fulltime work is over and maybe some time in weekends, have some questions also, will ask you in some time, as I get more organized in my thoughts, as to what i need to ask. Still exploring the code though..

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

No branches or pull requests

4 participants