PDF::Reuse(3pm)

πŸ’– PDF::Reuse πŸ’– (the PDF::Reuse perl module, 3pm, User Contributed Perl Documentation)

okay bestie sit DOWN because this one is a whole journey ✨ it's a Perl module, it lets you reuse and mass-produce PDF documents, and honestly? iconic behavior.


🌸 NAME 🌸

PDF::Reuse β€” Reuse and mass produce PDF documents 🫢


✨ SYNOPSIS ✨

use PDF::Reuse;
prFile('myFile.pdf');
prText(100, 500, 'Hello World !');
prEnd();

like. that's IT. that's the whole vibe check πŸŽ€


πŸ’… DESCRIPTION πŸ’…

So here's the tea β˜• β€” you reach for this module when you wanna mass produce a bunch of similar-but-not-identical PDF documents, and you also wanna reuse templates, JavaScripts and some other components while you're at it. It was built to be fast, and to give your programs the capacity to produce many pages per second, plus very big PDF documents if you need them. no cap πŸ”₯

It produces PDF-1.4 files. Some features of PDF-1.5, like "object streams" and "cross reference streams", ARE supported, but only at an experimental level β€” more testing is needed, lowkey. (If you get problems with a new document from Acrobat 6 or higher, try to save it or recreate it as a PDF-1.4 document first, before using it together with this module. that's the move πŸ’€)

🧚 Templates 🧚

Use your favorite program β€” probably a commercial visual tool β€” to produce single PDF-files that you'll use as templates, and then use this module to mass produce files from them.

(If you want small PDF-files, or you want special graphics, you can use this module for that too, but ngl visual tools are often the most practical.)

πŸ“ Lists πŸ“

The module uses "XObjects" extensively. That's a format that makes it possible to create big lists that are compact at the same time. both things. at once. we love efficiency πŸ’–

🌈 PDF-operators 🌈

The module gives you a good possibility to program at a "low level" with the basic graphic operators of PDF, if that's what you wanna do. You can build your own libraries of low level routines, with PDF-directives "controlled" by Perl. main character energy honestly πŸ‘‘

πŸŽ€ Archive-format πŸŽ€

If you want, you get your new documents logged in a format suitable for archiving or transfer.

PDF::Reuse::Tutorial might show you best what you can do with this module. πŸ‘€

⭐ JavaScript ⭐

You can attach JavaScripts to your PDF-files.

You can have libraries of JavaScripts. No cutting or pasting. and the girlies who include the scripts in documents only need to know how to initiate them. (Of course, those who write the scripts have to know Acrobat JavaScript well. no way around that one bestie πŸ™ˆ)

πŸ’€ Remarks about JavaScript πŸ’€

Some of the functions handling JavaScript have to be rewritten for Acrobat 7.

There are many limitations with Acrobat JavaScript, and the rules often change. So what works for one version of Acrobat/Reader might not work for another. Another complication is this: when documents are downloaded via the net by Acrobat, they are most often converted (!) and necessary JavaScripts are lost. tragic. 😭


πŸ¦‹ FUNCTIONS πŸ¦‹

All functions which are successful return specified values or 1.

The module doesn't make any attempt to import anything from encrypted files. periodt.

πŸ‘‘ Overview πŸ‘‘

To write a program with PDF::Reuse, you need these components:


πŸ’œ Mandatory Functions πŸ’œ

πŸŽ€ prFile β€” define output πŸŽ€

Alternative 1:

prFile ( $fileName );

Alternative 2, with parameters in an anonymous hash:

prFile ( { Name         => $fileName,
           HideToolbar  => 1,            # 1 or 0
           HideMenubar  => 1,            # 1 or 0
           HideWindowUI => 1,            # 1 or 0
           FitWindow    => 1,            # 1 or 0
           CenterWindow => 1   } );      # 1 or 0

Alternative 3:

prFile ( $r );  # For mod_perl 2 pass the request object

$fileName is optional, just like the rest of the parameters. It's the file to create. If another file is current when this function is called, the first one is written and closed. Only one file is processed at a single moment. If $fileName is undefined, output is written to STDOUT.

HideToolbar, HideMenubar, HideWindowUI, FitWindow and CenterWindow control the way the document is initially displayed.

Look at any program in this documentation for examples. prInitVars() shows how this function could be used together with a web server. πŸ’»

🌸 prEnd β€” end/flush buffers 🌸

prEnd ()

When the processing is going to end, the buffers of the last file have to be written to the disc. If this function is not called, the page structure, xref part and so on will be lost. do not skip this step bestie I am begging πŸ™

Look at any program in this documentation for an example.


🌈 Optional Functions 🌈

✨ prAdd β€” add "low level" instructions ✨

prAdd ( $string )

With this command you can add whatever you want to the current content stream. No syntactical checks are made, but if you use an internal name, the module tries to add the resource of the "name object" to the "Resources" of current page. "Name objects" always begin with a /.

(In this documentation I often talk about an "internal name". It denotes a "name object". When PDF::Reuse creates these objects, it assigns Ft1, Ft2, Ft3 ... for fonts, Ig1, Ig2, Ig3 for images, Fo1 .. for forms, Cs1 .. for Color spaces, Pt1 .. for patterns, Sh1 .. for shading directories, Gs0 .. for graphic state parameter dictionaries. These names are kept until the program finishes, and my ambition is also to keep the resources available in internal tables.)

This is a simple and very powerful function. You should study the examples and the "PDF-reference manual" if you want to use it. (When this text is written, a possible link to download it is: http://partners.adobe.com/asn/developer/acrosdk/docs.html)

This function is intended to give you detail control at a low level.

use PDF::Reuse;
use strict;

prFile('myFile.pdf');
my $string = "150 600 100 50 re\n";  # a rectangle
$string   .= "0 0 1 rg\n";           # blue (to fill)
$string   .= "b\n";                  # fill and stroke
prAdd($string);
prEnd();

πŸŽ€ prBookmark β€” define bookmarks πŸŽ€

prBookmark($reference)

Defines a "bookmark". $reference refers to a hash or array of hashes which looks something like this:

          {  text  => 'Document',
             act   => 'this.pageNum = 0; this.scroll(40, 500);',
             kids  => [ { text => 'Chapter 1',
                          act  => '1, 40, 600'
                        },
                        { text => 'Chapter 2',
                          act  => '10, 40, 600'
                        }
                      ]
          }

Each hash can have these components:

  • text β€” the text shown beside the bookmark
  • act β€” the action to be triggered. Has to be a JavaScript action. (Three simple numbers are translated to page, x and y in the sentences: this.pageNum = page; this.scroll(x, y); )
  • kids β€” will have a reference to another hash or array of hashes
  • close β€” if this component is present, the bookmark will be closed when the document is opened
  • color β€” 3 numbers, RGB-colors e.g. '0.5 0.5 1' for light blue
  • style β€” 0, 1, 2, or 3. 0 = Normal, 1 = Italic, 2 = Bold, 3 = Bold Italic

Creating bookmarks for a document:

use PDF::Reuse;
use strict;

my @pageMarks;

prFile('myDoc.pdf');

for (my $i = 0; $i < 100; $i++)
{   prText(40, 600, 'Something is written');
    # ...
    my $page = $i + 1;
    my $bookMark = { text => "Page $page",
                     act  => "$i, 40, 700" };
    push @pageMarks, $bookMark;
    prPage();
}
prBookmark( { text  => 'Document',
              close => 1,
              kids  => \@pageMarks } );
prEnd();

Traditionally bookmarks have mainly been used for navigation within a document, but they can be used for many more things. You can e.g. use them to navigate within your data. You can let your users go to external links also, so they can "drill down" to other documents. πŸ”₯

See "Remarks about JavaScript"

🫢 prCompress β€” compress/zip added streams 🫢

prCompress (1)

'1' here is a directive to compress all new streams of the current file. Streams which are included with prForm, prDocForm, prDoc or prSinglePage are not changed. New JavaScripts are also created as streams and compressed, if they are at least 100 bytes long. The streams are compressed in memory, so probably there is a limit of how big they can be. πŸ’­

prCompress(); is a directive not to compress. This is default.

See e.g. "Starting to reuse" in the tutorial for an example.

πŸ¦‹ prDoc β€” include pages from a document πŸ¦‹

prDoc ( $documentName, $firstPage, $lastPage )

or with the parameters in an anonymous hash:

prDoc ( { file  => $documentName,
          first => $firstPage,
          last  => $lastPage } );

Returns number of extracted pages.

If "first" is not given, 1 is assumed. If "last" is not given, you don't have any upper limit. N.B. The numbering of the pages differs from Acrobat JavaScript. In JavaScript the first page has index 0. (yes this trips everyone up, you're not alone πŸ™ˆ)

Adds pages from a document to the one you are creating. N.B. From version 0.32 of this module: if there are contents created with prText, prImage, prAdd, prForm and so on, prDoc tries to put the contents on the first extracted page from the old document.

If it is the first interactive component ( prDoc() or prDocForm() ) the interactive functions are kept and also merged with JavaScripts you have added, if any. But, if you specify a first page different than 1 or a last page, no JavaScript are extracted from the document, because then there is a risk that an included JavaScript function might refer to something not included.

use PDF::Reuse;
use strict;

prFile('myFile.pdf');                  # file to make
prJs('customerResponse.js');           # include a JavaScript file
prInit('nameAddress(12, 150, 600);');  # init a JavaScript function
prForm('best.pdf');                    # page 1 from best.pdf
prPage();
prDoc('long.pdf');                     # a document with 11 pages
prForm('best.pdf');                    # page 1 from best.pdf
prText(150, 700, 'Customer Data');     # a line of text
prEnd();

To extract pages 2-3 and 5-7 from a document and create a new document:

use PDF::Reuse;
use strict;

prFile('new.pdf');
prDoc( { file  => 'old.pdf',
         first => 2,
         last  => 3 });
prDoc( { file  => 'old.pdf',
         first => 5,
         last  => 7 });
prEnd();

To add a form, image and page number to each page of an 16 pages long document (The document Battery.pdf is cropped so each page is fairly small) β€” you could also have used prSinglePage, look at a very similar example under that function. ⭐

use PDF::Reuse;
use PDF::Reuse::Util;
use strict;

prFile('test.pdf');

  my $pageNumber = 0;
            for (my $page = 1; $page < 17; $page++)
            {   $pageNumber++;
                prForm(  { file =>'Words.pdf',
                           page => 5,
                           x    => 150,
                           y    => 150} );

                prImage( { file =>'Media.pdf',
                           page => 6,
                           imageNo => 1,
                           x  => 450,
                           y  => 450 } );
                blackText();
                prText( 360, 250, $pageNumber);
                prDoc('Battery.pdf', $pageNumber, $pageNumber);
            }
          prEnd;

okay bestie that loop is literally the main character of your script, it's out here stamping a form, dropping an image, flipping back to black text, and pulling a whole page out of Battery.pdf sixteen times like it's nothing πŸ’… no notes, it ate.


prDocDir β€” set directory for produced documents πŸ—‚οΈ

prDocDir ( $directoryName )

Sets directory for produced documents β€” that's it, that's the whole vibe. Once you call it, everything you make lands in there instead of wherever your script happened to be standing. ✨

          use PDF::Reuse;
          use strict;

          prDocDir('C:/temp/doc');
          prFile('myFile.pdf');         # writes to C:\temp\doc\myFile.pdf
          prForm('myFile.pdf');         # page 1 from ..\myFile.pdf
          prText(200, 600, 'New text');
          prEnd();

prDocForm β€” use an interactive page as a form πŸŽ€

Alternative 1) You put your parameters in an anonymous hash β€” only file is really necessary, the others just get their default values if you ghost them:

          prDocForm ( { file     => $pdfFile,       # template file
                        page     => $page,          # page number (of imported template)
                        adjust   => $adjust,        # try to fill the media box
                        effect   => $effect,        # action to be taken
                        tolerant => $tolerant,      # continue even with an invalid form
                        x        => $x,             # $x points from the left
                        y        => $y,             # $y points from the bottom
                        rotate   => $degree,        # rotate
                        size     => $size,          # multiply everything by $size
                        xsize    => $xsize,         # multiply horizontally by $xsize
                        ysize    => $ysize } )      # multiply vertically by $ysize

Ex.:

           my $internalName = prDocForm ( {file     => 'myFile.pdf',
                                           page     => 2 } );

Alternative 2) You put your parameters in this order:

           prDocForm ( $pdfFile, [$page, $adjust, $effect, $tolerant, $x, $y, $degree,
                   $size, $xsize, $ysize] )

Either way, the function returns a whole little friend group. In list context: $intName, @BoundingBox, $numberOfImages. In scalar context: $internalName of the form. 🫢

Peep prForm() for the full explanation of all those parameters, okay?

N.B. Usually you shouldn't adjust or change size and proportions of an interactive page. The graphic and interactive components are independent of each other and there is a great risk that any coordination is lost. like, do not do it. 😭

So what's actually happening under the hood: this function redefines a page into an "XObject" (the graphic parts), and then the page can be reused in a much better way. unfortunately there's an important limitation here β€” "XObjects" can only have single streams. If the page consists of many streams, you should concatenate them first. Adobe Acrobat can do that.

(If it is an important file, take a copy of it first. Sometimes the procedure fails. πŸ™ˆ)

Open the document with Acrobat. Then choose the "TouchUp Text" tool (icon or from the tools menu). Select a line of text somewhere on the page. Right-click the mouse. Choose "Attributes". Change font size or anything else, and then you change it back to the old value. Save the document. If there was no text on the page, use some other "Touch Up" tool. Alternatively, use GhostScript to convert multi-stream PDFs (see prForm documentation for the command).

          use PDF::Reuse;
          use strict;

          prDocDir('C:/temp/doc');
          prFile('newForm.pdf');
          prField('Mr/Ms', 'Mr');
          prField('First_Name', 'Lars');
          prDocForm('myFile.pdf');
          prFontSize(24);
          prText(75, 790, 'This text is added');
          prEnd();

(You can use the output from the example in prJs() as input to this example. Remember to save that file before closing it. ⭐)

See Remarks about JavaScript.


prExtract β€” extract an object group 🧚

prExtract ( $pdfFile, $pageNo, $oldInternalName )

oldInternalName, a "name"-object. This is the internal name you find in the original file. Returns a $newInternalName which can be used for "low level" programming. You have better look at graphObj_pl and modules it has generated for the tutorial, e.g. thermometer.pm, to see how this function can be used. πŸ‘‘

When you call this function, the necessary objects will be copied to your new PDF-file, and you can refer to them with the new name you receive. Slay.


prField β€” assign a value to an interactive field ✍️

prField ( $fieldName, $value )

$fieldName is an interactive field in the document you are creating. It has to be spelled exactly the same way here as it spelled in the document β€” no autocorrect energy, bestie, exact match only. $value is what you want to assigned to the field.

Put all your prField sentences early in your script. After prFile and before prDoc or prDocForm, and of course before prEnd. Each sentence with prField is translated to JavaScript and merged with old JavaScript.

See prDocForm() for an example.

If you're going to assign a value to a field consisting of several lines, you can write like this:

          my $string = "This is the first line \r second line \n 3:rd line";
          prField('fieldName', $string);

You can also let '$value' be a snippet of JavaScript-code that assigns something to the field. Then you have to put 'js:' first in "$value" like this:

          my $sentence = encrypt('This will be decrypted by "unPack"(JavaScript) ');
          prField('Interest_9', "js: unPack('$sentence')");

If you refer to a JavaScript function, it has to be included with prJs first. (The JavaScript interpreter will simply not be aware of old functions in the PDF-document, when the initiation is done.) like, it just won't know them, periodt. πŸ’€


prFont β€” set current font πŸ¦‹

prFont ( $fontName )

$fontName is an "external" font name. The parameter is optional.

In list context returns $internalName, $externalName, $oldInternalName, $oldExternalname. The first two variables refer to the current font, the two later to the font before the change. In scalar context returns b<$internalName>.

If a font wasn't found, Helvetica will be set. These names are always recognized: Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic, Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique, Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique or abbreviated TR, TB, TI, TBI, C, CB, CO, CBO, H, HB, HO, HBO. (Symbol and ZapfDingbats or abbreviated S, Z, also belong to the predefined fonts, but there is something with them that I really don't understand. You should print them first on a page, and then use other fonts, otherwise they are not displayed. 😭 iconic chaotic behavior honestly.)

You can also use a font name from an included page. It has to be spelled exactly as it is done there. Look in the file and search for "/BaseFont" and the font name. But take care β€” e.g. the PDFMaker which converts to PDF from different Microsoft programs, only defines exactly those letters you can see on the page. You can use the font, but perhaps some of your letters were not defined. πŸ™ˆ

In the distribution there is an utility program, 'reuseComponent_pl', which displays included fonts in a PDF-file and prints some letters. Run it to see the name of the font and if it is worth extracting. πŸ”

          use PDF::Reuse;
          use strict;
          prFile('myFile.pdf');

          ####### One possibility #########

          prFont('Times-Roman');     # Just setting a font
          prFontSize(20);
          prText(180, 790, "This is a heading");

          ####### Another possibility #######

          my $font = prFont('C');    # Setting a font, getting an
                                     # internal name
          prAdd("BT /$font 12 Tf 25 760 Td (This is some other text)Tj ET");
          prEnd();

The example above shows you two ways of setting and using a font. One simple, and one complicated with a possibility to detail control. pick your fighter πŸ’–


prFontSize β€” set current font size 🌸

prFontSize ( $size )

Returns $actualSize, $fontSizeBeforetheChange. Without parameters, prFontSize() sets the size to 12 points, which is default. so if you just want the factory setting, call it with nothing and it's giving 12. ✨


prForm β€” use a page from an old document as a form/background 🌈

Alternative 1) You put your parameters in an anonymous hash β€” again, only file is really necessary, the others get default values if not given:

          prForm ( { file     => $pdfFile,       # template file
                     page     => $page,          # page number (of imported template)
                     adjust   => $adjust,        # try to fill the media box
                     effect   => $effect,        # action to be taken
                     tolerant => $tolerant,      # continue even with an invalid form
                     x        => $x,             # $x points from the left
                     y        => $y,             # $y points from the bottom
                     rotate   => $degree,        # rotate
                     size     => $size,          # multiply everything by $size
                     xsize    => $xsize,         # multiply horizontally by $xsize
                     ysize    => $ysize } )      # multiply vertically by $ysize

Ex.:

           my $internalName = prForm ( {file     => 'myFile.pdf',
                                        page     => 2 } );

Alternative 2) You put your parameters in this order:

           prForm ( $pdfFile, $page, $adjust, $effect, $tolerant, $x, $y, $degree,
                   $size, $xsize, $ysize )

Either way, in list context it returns $intName, @BoundingBox, $numberOfImages, and in scalar context $internalName of the form. 🫢

file can be a filename, an IO::String object, a filehandle, or a scalar reference to PDF data in memory. lowkey flexible of it, we love to see it.

If page is excluded, 1 is assumed.

adjust, could be 1, 2 or 0/nothing.

  • If it is 1, the program tries to adjust the form to the current media box (paper size) and keeps the proportions unchanged. πŸŽ€
  • If it is 2, the program tries to fill as much of the media box as possible, without regards to the original proportions. And if this parameter is given, "x", "y", "rotate", "size", "xsize" and "ysize" will be ignored.
  • 0/nothing is just the do-nothing vibe.

effect can have 3 values:

  • 'print', which is default β€” loads the page in an internal table, adds it to the document and prints it to the current page. πŸ”₯
  • 'add' β€” loads the page and adds it to the document. (Now you can "manually" manage the way you want to print it to different pages within the document.) πŸ’…
  • 'load' β€” just loads the page in an internal table. (You can now take parts of a page like fonts and objects and manage them, without adding all the page to the document.) And heads up: you don't get any defined internal name of the form, if you let this parameter be 'load'.

okay bestie, back to the prForm() option menu, we were literally mid-list, no time to re-intro, let's GO πŸ’…

  • tolerant β€” can be nothing or literally anything. Leave it undefined and your program will throw an error if it tries to load a page the system can't actually handle, like one made of many streams. Set it to something and the vibe flips: now you have to check the first return value $internalName yourself to know if it worked. The program reuseComponent_pl has an example of this, go peek 🫢
  • x β€” where to start along the x-axis. (this one cannot be combined with adjust, sorry babe)
  • y β€” where to start along the y-axis. (also cannot be combined with adjust)
  • rotate β€” a degree from 0–360 that rotates the form counter-clockwise. (yep, cannot be combined with adjust either.) Warning bestie: often the form vanishes right out of the media box if degree >= 90 😭. That's when you drag it back with the x and y parameters. So: if degree == 90, add the width of the form to x; if degree == 180 add both width and height to x and y; if degree == 270 you can add the height to y.

rotate can also be one of 'q1', 'q2' or 'q3'. In that case the system rotates the form clockwise 90, 180 or 270 degrees and tries to keep the form inside the media box ✨

The rotation happens after the form has been resized or moved, just so we're clear.

Ex. To rotate from portrait (595 x 842 pt) to landscape (842 x 595 pt)
use PDF::Reuse;
use strict;

prFile('New_Report.pdf');
prMbox(0, 0, 842, 595);

prForm({file   => 'cert1.pdf',
        rotate => 'q1' } );
prEnd();

The same rotation can be achieved like this:

use PDF::Reuse;
use strict;

prFile('New_Report.pdf');
prMbox(0, 0, 842, 595);

prForm({file   => 'cert1.pdf',
        rotate => 270,
        y      => 595 } );
prEnd();
  • size β€” multiply every measure by this value (still can't hang with adjust bestie)
  • xsize β€” multiply horizontally by this value (no adjust combos allowed)
  • ysize β€” multiply vertically by $ysize (again, cannot be combined with adjust)

Now this function redefines a page into an "XObject" (aka just the graphic parts), and then the page can be reused and referred to like it's one whole unit ✨. But β€” and I cannot stress this enough β€” there's an important limitation here: "XObjects" can only have single streams. If your page is made of many streams, you have to concatenate them first. Adobe Acrobat can do that for you. (If it's a precious file, take a copy first, fr fr β€” sometimes the procedure just fails on you πŸ’€.)

Open the doc with Acrobat. Then pick the "TouchUp Text" tool. Select a line of text somewhere. Right-click the mouse. Choose "Attributes". Change font size or literally anything else, then change it back to the old value. Save the document. That's it, that's the trick πŸŽ€

You could alternatively save the file as Postscript and redistill it with the distiller or with Ghost script, but ngl that's a little more risky β€” you might loose fonts or something else 😬. Another alternative could be to use prSinglePage().

Alternatively, GhostScript can be used to convert multi-stream PDFs into single-stream format compatible with prForm:

gs -sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/default \
   -dNOPAUSE -dQUIET -dBATCH -dDetectDuplicateImages \
   -dCompressFonts=true -r150 -sOutputFile=output.pdf input.pdf
use PDF::Reuse;
use strict;

prFile('myFile.pdf');
prForm('best.pdf');    # Takes page No 1
prText(75, 790, 'Dear Mr Gates');
# ...
prPage();
prMbox(0, 0, 900, 960);
my @vec = prForm(   { file => 'EUSA.pdf',
                      adjust => 1 } );
prPage();
prMbox();
prText(35, 760, 'This is the final page');

# More text ..

#################################################################
# We want to put a miniature of EUSA.pdf, 35 points from the left
# 85 points up, and in the format 250 X 200 points
#################################################################

my $xScale = 250 / ($vec[3] - $vec[1]);
my $yScale = 200 / ($vec[4] - $vec[2]);

prForm ({ file => 'EUSA.pdf',
          xsize => $xScale,
          ysize => $yScale,
          x     => 35,
          y     => 85 });

prEnd();

The first prForm() up there is the simple, "normal" way of using the function β€” main character energy, no notes πŸ‘‘. The second time it shows up, the size of the imported page gets changed: it adjusts to the media box that's current at that moment. Also, data about the form gets captured, so you can control in more detail how it'll be displayed πŸ¦‹


🌸 prGetLogBuffer β€” get the log buffer 🌸

prGetLogBuffer ()

returns a $buffer containing the log of the current page. (Could be used e.g. to calculate an MD5-digest of what's been registered that far, instead of accumulating all the single values one by one.) Heads up: a log has to be active for this, see prLogDir() below ⭐

Look at "Using the template" and "Restoring a document from the log" in the tutorial for examples of usage.

πŸ¦‹ prGraphState β€” define a graphic state parameter dictionary πŸ¦‹

prGraphState ( $string )

This is a "low level" function. Returns $internalName. The $string has to be a complete dictionary with the initial << and terminating >>. No syntactical checks are made whatsoever. Perhaps you'll never even have to use this function, bestie πŸ’…

use PDF::Reuse;
use strict;

prFile('myFile.pdf');

###################################################
# Draw a triangle with Gs0 (automatically defined)
###################################################

my $str = "q\n";
$str   .= "/Gs0 gs\n";
$str   .= "150 700 m\n";
$str   .= "225 800 l\n";
$str   .= "300 700 l\n";
$str   .= "150 700 l\n";
$str   .= "S\n";
$str   .= "Q\n";
prAdd($str);

########################################################
# Define a new graph. state param. dic. and draw a new
# triangle further down
########################################################

$str = '<</Type/ExtGState/SA false/SM 0.02/TR2 /Default'
                  . '/LW 15/LJ 1/ML 1>>';
my $gState = prGraphState($str);
$str  = "q\n";
$str .= "/$gState gs\n";
$str .= "150 500 m\n";
$str .= "225 600 l\n";
$str .= "300 500 l\n";
$str .= "150 500 l\n";
$str .= "S\n";
$str .= "Q\n";
prAdd($str);

prEnd();

πŸ’– prImage β€” reuse an image from an old PDF document πŸ’–

Alternative 1) You tuck your parameters into an anonymous hash (only file is really necessary β€” everything else just gets default values if you don't hand them over).

prImage( { file     => $pdfFile,       # template file
           page     => $page,          # page number
           imageNo  => $imageNo        # image number
           adjust   => $adjust,        # try to fill the media box
           effect   => $effect,        # action to be taken
           x        => $x,             # $x points from the left
           y        => $y,             # $y points from the bottom
           rotate   => $degree,        # rotate
           size     => $size,          # multiply everything by $size
           xsize    => $xsize,         # multiply horizontally by $xsize
           ysize    => $ysize } )      # multiply vertically by $ysize

Ex.:

prImage( { file    => 'myFile.pdf',
           page    => 10,
           imageNo => 2 } );

Alternative 2) You put your parameters in this order:

prImage ( $pdfFile, [$page, $imageNo, $effect, $adjust, $x, $y, $degree,
        $size, $xsize, $ysize] )

Returns, in scalar context, $internalName. As a list: $internalName, $width, $height.

It assumes $pageNo and $imageNo are 1 if you don't specify them. If $effect is given and it's anything other than 'print', the image gets defined in the document but is not shown at that moment πŸ‘€

For all the other parameters, go look at prForm().

use PDF::Reuse;
use strict;

prFile('myFile.pdf');
my @vec = prImage({ file  => 'best.pdf',
                    x     => 10,
                    y     => 400,
                    xsize => 0.9,
                    ysize => 0.8 } );
prText(35, 760, 'This is some text');
# ...
prPage();
my @vec2 = prImage( { file    => 'destiny.pdf',
                      page    => 1,
                      imageNo => 1,
                      effect  => 'add' } );
prText(25, 760, "There shouldn't be any image on this page");
prPage();
########################################################
#  Now we make both images so that they could fit into
#  a box 300 X 300 points, and they are displayed
########################################################

prText(25, 800, 'This is the first image :');

my $xScale = 300 / $vec[1];
my $yScale = 300 / $vec[2];
if ($xScale < $yScale)
{  $yScale = $xScale;
}
else
{  $xScale = $yScale;
}
prImage({ file   => 'best.pdf',
          x      => 25,
          y      => 450,
          xsize  => $xScale,
          ysize  => $yScale} );

prText(25, 400, 'This is the second image :');

$xScale = 300 / $vec2[1];
$yScale = 300 / $vec2[2];
if ($xScale < $yScale)
{  $yScale = $xScale;
}
else
{  $xScale = $yScale;
}
prImage({ file   => 'destiny.pdf',
          x      => 25,
          y      => 25,
          xsize  => $xScale,
          ysize  => $yScale} );

prEnd();

So on the first page an image is displayed in a simple way. While the second page is being processed, prImage() loads an image, but it is not shown here. On the 3:rd page, the two images get scaled and shown 🌈

In the distribution there's a utility program, reuseComponent_pl, which displays included images in a PDF-file and their "names".

πŸŽ€ prInit β€” add JavaScript to be executed at initiation πŸŽ€

prInit ( $string, $duplicateCode )

$string can be any JavaScript code, but you can only refer to functions included with prJs. The JavaScript interpreter will not know other functions in the document. Often you can add new things, but you can't remove or change interactive fields, because the interpreter hasn't come that far when initiation is done β€” it's giving "not yet bestie" πŸ™ˆ

$duplicateCode is undefined or anything. It duplicates the JavaScript code that's been used at initiation, so you can look at it from within Acrobat and debug it. Warning: it makes the document bigger. Also, this parameter is deprecated ⚠️

use PDF::Reuse;
use strict;

prFile('myFile.pdf');
prInit('app.alert("This is displayed when opening the document");');

prEnd();
Remark, listen up bestie: try not to use return in the code you run at initiation. Fr fr. Here's the tea: if your user downloaded a page with Web Capture, and then opens a PDF-document where a JavaScript runs at initiation and that JavaScript has a return-statement, a bug happens. The JavaScript interpreter "exits" instead of returning, so the execution of the JavaScript might finish too early. It's a bug in Acrobat/Reader 5. Not us, not you, them. πŸ’€

✨ prInitVars β€” initiate global variables and internal tables

prInitVars(1)

Okay so if you're running programs with PDF::Reuse as persistent procedures, you probably need to initiate global variables. If you pass '1' (or literally anything) as the parameter, the internal tables for forms, images, fonts and interactive functions are not initiated. The module "learns" the offset and sizes of used objects and can process them faster β€” but at the same time the size of the program grows. πŸŽ€

use PDF::Reuse;
use strict;
prInitVars();     # To initiate ALL global variables and tables
# prInitVars(1);  # To make it faster, but more memory consuming

$| = 1;
print STDOUT "Content-Type: application/pdf \n\n";

prFile();         # To send the document uncatalogued to STDOUT

prForm('best.pdf');
prText(25, 790, 'Dear Mr. Anders Persson');
# ...
prEnd();

If you call this function with no parameters at all, every global variable, including the internal tables, gets initiated. ✨


πŸ–ΌοΈ prAltJpeg β€” import a low-res jpeg-image for display and a high-res jpeg-image for printing

prAltJpeg ( $imageData, $width, $height, $format, $altImageData, $altWidth, $altHeight, $altFormat )

$imageData holds 1 single jpeg-image. $width and $height also have to be specified, no skipping. $format tells us what format the image data takes: 0 for file, 1 for binary string. The $altImageData etc. follow the same format. Returns the $internalName. πŸ‘‘

use PDF::Reuse;
use Image::Info qw(image_info dim);
use strict;

my $file = 'myImage.jpg';
my $info = image_info($file);
my ($width, $height) = dim($info);    # Get the dimensions
my $colortype = $info->{color_type};  # get color space

my $alt_file = 'myImage.jpg';
my $alt_info = image_info($alt_file);
my ($alt_width, $alt_height) = dim($alt_info);

prFile('myFile.pdf');
my $intName = prAltJpeg("$file",         # Define the image
                         $width,         # in the document
                         $height,
                         0,
                         "$alt_file",
                         $alt_width,
                         $alt_height,
                         0);

my $str = "q\n";
$str   .= "$width 0 0 $height 10 10 cm\n";
$str   .= "/$intName Do\n";
$str   .= "Q\n";
prAdd($str);
prEnd();

🌸 prJpeg β€” import a jpeg-image

prJpeg ( $imageData, $width, $height, $format )

Same deal, bestie: $imageData contains 1 single jpeg-image. $width and $height must be specified. $format says what form the image data takes: 0 for file, 1 for binary string. Returns the $internalName. πŸ’–

use PDF::Reuse;
use Image::Info qw(image_info dim);
use strict;

my $file = 'myImage.jpg';
my $info = image_info($file);
my ($width, $height) = dim($info);    # Get the dimensions

prFile('myFile.pdf');
my $intName = prJpeg("$file",         # Define the image
                      $width,         # in the document
                      $height,
                      0);

my $str = "q\n";
$str   .= "$width 0 0 $height 10 10 cm\n";
$str   .= "/$intName Do\n";
$str   .= "Q\n";
prAdd($str);
prEnd();

This one is kinda like an extra or reserve routine for adding images to the document. The most simple way is to use prImage(). 🎨


⚑ prJs β€” add JavaScript

prJs ( $string|$fileName )

Adding JavaScript to your new document, slay. $string has to consist only of JavaScript functions: function a (..){ ... } function b (..) { ...} and so on. If $string doesn't contain '{', then $string is interpreted as a filename. In that case the file has to consist only of JavaScript functions too, no random vibes allowed.

See "Remarks about JavaScript" πŸ‘€


prLink( { page   => $pageNo,     # Starting with 1  !
          x      => $x,
          y      => $y,
          width  => $width,
          height => $height,
          URI    => $URI     } );

You can also call prLink like this:

prLink($page, $x, $y, $width, $height, $URI);

You have to put prLink after prFile and before the sentences where its page is created. The links are created at the page-breaks. If the page is already created, no new link will be inserted. Periodt. 🚫

Here's an example where the links of a 4 page document are preserved, and a link is added at the end of the document. We're assuming there's some suitable text at that place (x = 400, y = 350):

use strict;
use PDF::Reuse;

prFile('test.pdf');

prLink( {page   => 4,
         x      => 400,
         y      => 350,
         width  => 105,
         height => 15,
         URI    => 'http://www.purelyInvented.com/info.html' } );

prDoc('fourPages.pdf');

prEnd();

( If you're creating each page of a document separately, you can also use 'hyperLink' from PDF::Reuse::Util. Then you get an external text in Helvetica-Oblique, underlined and in blue. Iconic. )

use strict;
use PDF::Reuse;
use PDF::Reuse::Util;

prFile('test.pdf');
prForm('template.pdf', 5);
my ($from, $pos) = prText(25, 700, 'To get more information  ');

$pos = hyperLink( $pos, 700, 'Press this link',
                  'http://www.purelyInvented.com/info.html' );
($from, $pos) = prText( $pos, 700, ' And get connected');
prEnd();

'hyperLink' has a few parameters: $x, $y, $textToBeShown, $hyperLink and $fontSize (not shown in the example). It returns the current x-position. 🫢


πŸ“ prLog β€” add a string to the log

prLog ( $string )

Adds whatever you want to the current log (a reference No, a commentary, a tag ?). But bestie β€” a log has to be active, so go see prLogDir(). 🌈

Look at "Using the template" and "Restoring the document from the log" in the tutorial for an example. πŸ“š


πŸ“‚ prLogDir β€” set directory for the log

prLogDir ( $directory )

Sets a directory for the logs and activates the logging, let's go. A little log file is created for each PDF-file. Normally it should be much, much more compact than the PDF-file, and it should be possible to restore or verify a document with the help of it. (Of course you could compress or store the logs in a database to save even more space. Big brain energy. 🧠)

use PDF::Reuse;
use strict;

prDocDir('C:/temp/doc');
prLogDir('C:/run');

prFile('myFile.pdf');
prForm('best.pdf');
prText(25, 790, 'Dear Mr. Anders Persson');
# ...
prEnd();

In this example a log file with the name 'myFile.pdf.dat' is created in the directory 'C:\run'. If that directory doesn't exist, the system tries to create it. (But, just as mkdir does, it only creates the last level in a directory tree. So don't come for us.) 🌳


πŸ“„ prMbox β€” define the format (MediaBox) for a new page.

prMbox ( $lowerLeftX, $lowerLeftY, $upperRightX, $upperRightY )

If the function or the parameters are missing, they're set to 0, 0, 595, 842 points respectively. Only for new pages. Pages created with prDoc and prSinglePage keep their media boxes unchanged β€” no touchy. πŸ™…

See prForm() for an example. πŸ‘€


πŸ“ƒ prPage β€” create/insert a page

prPage ($noLog)

Don't use the optional parameter, bestie, it's only used internally, not to clutter the log when automatic page breaks are made. It's giving internal business only. 🀫

See prForm() for an example.


πŸ¦‹ prSinglePage β€” take single pages, one by one, from an old document

prSinglePage($file, $pageNumber)

$pageNumber is optional. If it's not given, the next page is assumed. Returns the number of remaining pages. This function is a variant of prDoc for single pages, with the addition that it has a counter of last page read and the total number of pages of the old document, so it can be used to loop through a document. Looping? We love a good loop. πŸ”

To add a form, image and page number to each page of a document (the document Battery.pdf is cropped so each page is fairly small). You could also have used prDoc, but only if you knew in advance the number of pages of the old document:

use PDF::Reuse;
use PDF::Reuse::Util;
use strict;

prFile('test.pdf');

my $pageNumber = 0;
my $left = 1;            # Every valid PDF-document has at least 1 page,
                         # so that can be assumed

while ($left)
{   $pageNumber++;
    prForm(  { file =>'Words.pdf',
               page => 5,
               x    => 150,
               y    => 150} );

    prImage( { file    =>'Media.pdf',
               page    => 6,
               imageNo => 1,
               x       => 450,
               y       => 450 } );
    blackText();
    prText( 360, 250, $pageNumber);
    $left = prSinglePage('Battery.pdf');
 }

 prEnd;

prSinglePage creates a new page from an old document and adds new content (to the array of streams of that page). Most often you can add new contents to the page like the example above, and it works fine β€” but sometimes you get surprises. πŸ‘» There can e.g. be instructions in the earlier contents to make filling color white, and then you will probably not see the added new text. That is why PDF::Reuse::Util::blackText() is used in the example. There can be other instructions like moving or rotating the user space. Also new contents can end up outside the crop-box.

⚠️ Of course all new programs should be tested. If prSinglePage can't be used, try to use prForm followed by prPage instead.

πŸ“ prStrWidth β€” calculate the string width

prStrWidth($string, $font, $fontSize)

Returns string width in points. Should be used in conjunction with one of these predefined fonts of Acrobat/Reader: Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic, Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique, Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique, or with a TrueType font embedded with prTTFont.

🫠 If some other font is given, Helvetica is used, and the returned value will at the best be approximate.

✍️ prText β€” add a text-string

prText ( $x, $y, $string, $align, $rotation )

Puts $string at position $x, $y πŸ’– Returns 1 in scalar context. Returns ($xFrom, $xTo) in list context. Heads up bestie: $xTo will not be defined together with a rotation. And the way it figures out how long your strings are? That's prStrWidth() doing the math, so only the predefined fonts together with Acrobat/Reader, or embedded TrueType fonts will give reliable values for $xTo. No cap. ✨

$align can be 'left' (= default), 'center' or 'right'. The parameter is optional.

$rotation can be a degree 0 - 360, 'q1', 'q2' or 'q3'. Also optional. πŸŒ€

Current font and font size are used. (If you use prAdd() before this function, many other things could also influence the text.)

use strict;
use PDF::Reuse;

prFile('test.pdf');

#####################################
# Use a "curser" ($pos) along a line
#####################################

my ($from, $pos) = prText(25, 800, 'First write this. ');
($from, $pos) = prText($pos, 800, 'Then write this. ');
prText($pos, 800, 'Finally write this.');

#####################################
# Right adjust and center sentences
#####################################

prText( 200, 750, 'A short sentence', 'right');
prText( 200, 735, 'This is a longer sentence', 'right');
prText( 200, 720, 'A word', 'right');

prText( 200, 705, 'Centered around a point 200 points from the left', 'center');
prText( 200, 690, 'The same center', 'center');
prText( 200, 675, '->.<-', 'center');

############
# Rotation
############

prText( 200, 550, ' Rotate 0 degrees','', 0);
prText( 200, 550, ' Rotate 60 degrees','', 60);
prText( 200, 550, ' Rotate 120 degrees','', 120);
prText( 200, 550, ' Rotate 180 degrees','', 180);
prText( 200, 550, ' Rotate 240 degrees','', 240);
prText( 200, 550, ' Rotate 300 degrees','', 300);

prText( 400, 430, 'Rotate 90 degrees clock-wise','','q1');
prText( 400, 430, 'Rotate 180 degrees clock-wise','', 'q2');
prText( 400, 430, 'Rotate 270 degrees clock-wise','', 'q3');

##########################
# Rotate and right adjust
##########################

prText( 200, 230, 'Rotate 90 degrees clock-wise ra->','right','q1');
prText( 200, 230, 'Rotate 180 degrees clock-wise ra->','right', 'q2');
prText( 200, 230, 'Rotate 270 degrees clock-wise ra->','right', 'q3');

prEnd();

🌸 prTTFont β€” select and embed a TrueType font 🌸

prTTFont ( "/path/to/font/file.ttf" )

This function is equivalent to prFont except that rather than restricting you to the list of core built-in fonts, it allows you to select an external TrueType font file and have it embedded in your PDF document. Iconic behaviour honestly. Using TrueType fonts also enables the prText function to accept UTF-8 strings, which allows you to use characters outside the Mac-Roman/Win-ANSI character sets used by the built-in fonts. 🌈

You can specify the same font path multiple times in one document and only one copy will be embedded. Alternatively, prTTFont returns an identifier which can be used to select the same font again:

my $arial = prTTFont('/path/to/Arial.ttf');
prFontSize(20);
prText(20, 700, 'Some text in Arial');
#
# ... later ...
#
prPage();
prTTFont($arial);
prFontSize(12);
prText(20, 700, 'Some more text in Arial');
#
#  to pass a UTF8 string to prText
#
prText(20, 675, "T\x{113}n\x{101} koutou");  # T?n? Koutou

In list context this function returns $internalName, $externalName, $oldInternalName, $oldExternalname. The first two variables refer to the current font, the last two refer to the font before the change. In scalar context only $internalName is returned. πŸŽ€

Note: To use this function, you must have the Font::TTF and Text::PDF modules installed. Like, you gotta have the prerequisites, bestie. πŸ’…

Text set in an embedded TrueType font is searchable and copyable: a /ToUnicode CMap mapping the embedded glyphs back to Unicode is generated automatically and written with the font. Only the glyphs actually used are mapped, so the mapping costs nothing for characters the document does not contain. Code points outside the Basic Multilingual Plane are encoded as UTF-16BE surrogate pairs, as the PDF specification requires. πŸ‘‘

No option is provided to suppress the CMap. It is small, it is what makes the text usable, and a PDF whose text cannot be extracted is rarely what anyone wants. Periodt. πŸ’―


πŸ’€ INTERNAL OR DEPRECATED FUNCTIONS πŸ’€

prBar β€” define and paint bars for bar fonts 🍫

prBar ($x, $y, $string)

Prints a bar font pattern at the current page. Returns $internalName for the font. $x and $y are coordinates in points and $string should consist of the characters '0', '1' and '2' (or 'G'). '0' is a white bar, '1' is a dark bar. '2' and 'G' are dark, slightly longer bars, guard bars. You can use e.g. GD::Barcode or one module in that group to calculate the bar code pattern. prBar "translates" the pattern to white and black bars. πŸ¦“

use PDF::Reuse;
use GD::Barcode::Code39;
use strict;

prFile('myFile.pdf');
my $oGdB = GD::Barcode::Code39->new('JOHN DOE');
my $sPtn = $oGdB->barcode();
prBar(100, 600, $sPtn);
prEnd();

Internally the module uses a font for the bars, so you might want to change the font size before calling this function. In that case, use prFontSize() . If you call this function without arguments it defines the bar font but does not write anything to the current page. πŸ‘€

An easier and often better way to produce bar codes is to use PDF::Reuse::Barcode. Look at that module! ✨

prCid β€” define time stamp/check id πŸ•

prCid ( $timeStamp )

An internal function. Don't bother about it. It is used in automatic routines when you want to restore a document. It gives modification time of the next PDF-file or JavaScript. See "Restoring a document from the log" in the tutorial for more about the time stamp

prId β€” define id-string of a PDF document πŸ†”

prId ( $string )

An internal function. Don't bother about it. It is used e.g. when a document is restored and an id has to be set, not calculated.

prIdType β€” define id-type πŸ”–

prIdType ( $string )

An internal function. Avoid using it. $string could be "Rep" for replace or "None" to avoid calculating an id.

Normally you don't use this function. Then an id is calculated with the help of Digest::MD5::md5_hex and some data from the run.

prTouchUp β€” make changes and reuse more difficult πŸ”’

prTouchUp (1);

By default and after you have issued prTouchUp(1), you can change the document with the TouchUp tool from within Acrobat. If you want to switch off this possibility, you use prTouchUp() without any parameter. Then the user shouldn't be able to change anything graphic by mistake. He has to do something premeditated and perhaps with a little effort. He could still save it as Postscript and redistill, or he could remove or add single pages. (Here is a strong reason why the log files, and perhaps also check sums, are needed. It would be very difficult to forge a document unless the forger also has access to your computer and knows how the check sums are calculated.) 😭

Avoid to switch off the TouchUp tool for your templates. It creates an extra level within the PDF-documents . Use this function for your final documents.

See "Using the template" in the tutorial for an example.

Careful bestie: This function works for pages created with prPage, but not with prDoc and prSinglePage, So it is more or less deprecated as these function have developed.

(To encrypt your documents: use the batch utility within Acrobat)

prVers β€” check version of log and program πŸ“Œ

prVers ( $versionNo )

To check version of this module in case a document has to be restored.


πŸ¦‹ SEE ALSO πŸ¦‹

  • PDF::Reuse::Tutorial
  • PDF::Reuse::Barcode
  • PDF::Reuse::OverlayChart

To program with PDF-operators, look at "The PDF-reference Manual" which probably is possible to download from http://partners.adobe.com/asn/tech/pdf/specifications.jsp Look especially at chapter 4 and 5, Graphics and Text, and the Operator summary. πŸ“–

Technical Note # 5186 contains the "Acrobat JavaScript Object Specification". I downloaded it from http://partners.adobe.com/asn/developer/technotes/acrobatpdf.html

If you are serious about producing PDF-files, you probably need Adobe Acrobat sooner or later. It has a price tag. πŸ’Έ Other good programs are GhostScript and GSview. I got them via http://www.cs.wisc.edu/~ghost/index.html Sometimes they can replace Acrobat. A nice little detail is e.g. that GSview shows the x- and y-coordinates better then Acrobat. If you need to convert HTML-files to PDF, HTMLDOC is a possible tool. Download it from http://www.easysw.com . A simple tool for vector graphics is Mayura Draw 2.04, download it from http://www.mayura.com. It is free. I have used it to produce the graphic OO-code in the tutorial. It produces postscript which the Acrobat Distiller (you get it together with Acrobat) or Ghostscript can convert to PDF.(The commercial product, Mayura Draw 4.01 or something higher can produce PDF-files straight away)

If you want to import jpeg-images, you might need

Image::Info

To get definitions for e.g. colors, take them from

PDF::API2::Util

🌸 LIMITATIONS 🌸

Meta data, info and many other features of the PDF-format have not been implemented in this module. F in the chat. 😭

Many things can be added afterwards, after creating the files. If you e.g. need files to be encrypted, you can use a standard batch routine within Adobe Acrobat.


πŸ’– THANKS TO πŸ’–

Martin Langhoff, Matisse Enzer, Yunliang Yu and others who have contributed with code, suggestions and error reports. 🫢

Grant McLean has implemented font embedding by grafting Font::TTF and Text::PDF::TTFont0 onto the PDF::Reuse API. He has written the embedded packages PDF::Reuse::DocProxy and PDF::Reuse::TTFont.

The functionality of prDoc and prSinglePage to include new contents was developed for a specific task with support from the Electoral Enrolment Centre, Wellington, New Zealand πŸ‡³πŸ‡Ώ


πŸ‘‘ MAILING LIST πŸ‘‘

http://groups.google.com/group/PDF-Reuse

✨ AUTHOR ✨

Lars Lundberg larslund@cpan.org Chris Nighswonger cnighs@cpan.org


Copyright (C) 2003 - 2004 Lars Lundberg, Solidez HB. Copyright (C) 2005 Karin Lundberg. Copyright (C) 2006 - 2010 Lars Lundberg, Solidez HB. Copyright (C) 2010 - 2019 Chris Nighswonger This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

πŸ’€ DISCLAIMER πŸ’€

okay bestie, quick real talk before you go πŸ’…

You get this module free, exactly as it is. And ngl, nothing is guaranteed to work β€” not the stuff stated implicitly in this doc, not the stuff stated explicitly in this doc. Like, none of it. πŸ™ˆ

And the way that everything you do, you do at your own risk β€” periodt. I will not take responsibility for any damage, loss of money and/or health that may arise from the use of this module. Any of those three. Or all three at once, which would be so rude of the universe. 😭

fr fr, read that one more time. no cap. this is the unglamorous part, but it's the part that matters. 🫢

and that's a wrap on the whole page, bestie!! πŸŽ€βœ¨ you read the entire thing, you're literally the main character πŸ‘‘

it's giving PDF::Reuse(3pm) from perl v5.40.1, dated 2026-07-31. go forth and merge those PDFs, you icon πŸ¦‹πŸŒΈπŸ’–

πŸ“¦ where this page comes from