View on GitHub

PDF-Lite-raku

Minimal class for the creation and editing of PDF documents

[Raku PDF Project] / PDF::Lite

Actions Status

PDF::Lite

PDF::Lite is a minimal class for creating or editing PDF documents, including:

use v6;
use PDF::Lite;

my PDF::Lite $pdf .= new;
$pdf.media-box = 'Letter';
my PDF::Lite::Page $page = $pdf.add-page;
constant X-Margin = 10;
constant Padding = 10;

$page.graphics: {
    enum <x0 y0 x1 y1>;
    my $font = $pdf.core-font( :family<Helvetica>, :weight<bold>, :style<italic> );
    my @position = [10, 10];
    my @box = .say: "Hello World", :@position, :$font;

    my PDF::Lite::XObject $img = .load-image: "t/images/lightbulb.gif";
    .do: $img, :position[@box[x1] + Padding, 10];
}

given $pdf.Info //= {} {
    .CreationDate = DateTime.now;
}

$pdf.save-as: "examples/hello-world.pdf";

example.pdf

Text

.say and .print are simple convenience methods for displaying simple blocks of text with encoding, optional line-wrapping, alignment and kerning.

These methods return a rectangle given the rendered text region;

use PDF::Lite;
enum <x0 y0 x1 y1>;
my PDF::Lite $pdf .= new;
$pdf.media-box = [0, 0, 500, 150];
my PDF::Lite::Page $page = $pdf.add-page;
my $font = $pdf.core-font( :family<Helvetica> );
my $header-font = $pdf.core-font( :family<Helvetica>, :weight<bold> );

$page.text: -> $txt {
    $txt.font = $header-font, 16;
    $txt.text-position = 250, 112;
    $txt.say: 'Some Sample PDF::Lite Text', :align<center>, :valign<bottom>;
    my $width := 200;
    my $text = q:to"--END--";
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
    ut labore et dolore magna aliqua.
    --END--
            
    $txt.font = $font, 12;
    # output text with left, top corner at (20, 100)
    my @box = $txt.say: $text, :$width, :position[:left(20), :top(100)];
    note "text height: {@box[y1] - @box[y0]}";

    # output kerned paragraph, flow from right to left, right, top edge at (450, 100)
    $txt.say( $text, :$width, :height(150), :align<right>, :kern, :position[450, 100] );
    # add another line of text, flowing on to the next line
    $txt.font = $pdf.core-font( :family<Helvetica>, :weight<bold> ), 12;
    $txt.say( "But wait, there's more!!", :align<right>, :kern );
}

$pdf.save-as: "examples/sample-text.pdf";

sample-text.pdf

Images (.load-image and .do methods):

The .load-image method can be used to open an image. The .do method can them be used to render it.

use PDF::Lite;
my PDF::Lite $pdf .= new;
$pdf.media-box = [0, 0, 450, 250];
my PDF::Lite::Page $page = $pdf.add-page;

$page.graphics: -> $gfx {
    my PDF::Lite::XObject $img = $gfx.load-image("t/images/snoopy-happy-dance.jpg");
    $gfx.do($img, 50, 40, :width(150) );

    # displays the image again, semi-transparently with translation, rotation and scaling

    $gfx.transform( :translate[180, 100]);
    $gfx.transform( :rotate(-.5), :scale(.75) );
    $gfx.FillAlpha = 0.5;
    $gfx.do($img, :width(150) );
}
$pdf.save-as: "examples/sample-image.pdf";

sample-image.pdf

Note: at this stage, only the JPEG, GIF and PNG image formats are supported.

Text effects

To display card suits symbols, using the ZapfDingbats core-font, with diamonds and hearts colored red:

use PDF::Lite;
use PDF::Content::Color :rgb;
my PDF::Lite $pdf .= new;
$pdf.media-box = [0, 0, 400, 120];
my PDF::Lite::Page $page = $pdf.add-page;

$page.graphics: {

    $page.text: {
	.text-position = [20, 70];
	.font = [ $pdf.core-font('ZapfDingbats'), 24];
	.WordSpacing = 16;
	.print("♠ ♣\c[NO-BREAK SPACE]");
	.FillColor = rgb(1, .3, .3);  # reddish
	.say("♦ ♥");
    }

    # Display outline, slanted text

    my $header-font = $pdf.core-font( :family<Helvetica>, :weight<bold> );

    $page.text: {
	 use PDF::Content::Ops :TextMode;
	.font = ( $header-font, 18);
	.TextRender = TextMode::FillOutlineText;
	.LineWidth = .5;
        .text-transform( :skew[0, -6], :translate[10, 30] );
	.FillColor = rgb(.6, .7, .9);
	.print('Outline Slanted Text @(10,30)');
    }
}

$pdf.save-as: "examples/text-effects.pdf";

text-effects.pdf

Fonts

This module has built-in support for the PDF core fonts: Courier, Times, Helvetica, ZapfDingbats and Symbol.

The companion module PDF::Font::Loader can be used to access a wider range of fonts:

use PDF::Lite;
use PDF::Font::Loader :load-font;
my PDF::Lite $pdf .= new;
$pdf.media-box = [0, 0, 400, 120];
my PDF::Lite::Page $page = $pdf.add-page;
my $noto = load-font: :file<t/fonts/NotoSans-Regular.ttf>;
# or find a system font by family and attributes (also requires FontConfig)
# $noto = load-font: :family<NotoSans>, :weight<book>;

$page.text: {
    .text-position = [10,100];
    .font = $noto;
    .say: "Noto Sans Regular";
}

$pdf.save-as: "examples/fonts.pdf";

example.pdf

Page sizes

The media-box method is used most commonly used to set page sizes. It can be set on the PDF::Lite object to set a default page size.

Individual pages can be given different page sizes.

use v6;
use PDF::Lite;
use PDF::Content::Page :PageSizes, :&to-landscape;

my PDF::Lite $pdf .= new;
$pdf.media-box = Letter; # Set a default page size

my $page1 = $pdf.add-page;
say $page1.media-box; # [0 0 612 792]

my $page2 = $pdf.add-page;
$page2.media-box = A4;
say $page2.media-box; # [0 0 595 842]

my $page3 = $pdf.add-page;
$page3.media-box = to-landscape(A4);
say $page3.media-box; # [0 0 842 595]

Forms and Patterns

Forms are a reusable graphics component. They can be used wherever images can be used.

A pattern can be used to fill an area with a repeating graphic.

use PDF::Lite;
use PDF::Content::Color :rgb;
my PDF::Lite $pdf .= new;
$pdf.media-box = [0, 0, 400, 120];
my PDF::Lite::Page $page = $pdf.add-page;

$page.graphics: {
    my $font = $pdf.core-font( :family<Helvetica> );
    my PDF::Lite::XObject $form = .xobject-form(:BBox[0, 0, 95, 25]);
    $form.graphics: {
        # Set a background color
        .FillColor = rgb(.8, .9, .9);
        .Rectangle: |$form<BBox>;
        .paint: :fill;
        .font = $font;
        .FillColor = rgb(1, .3, .3);  # reddish
        .say("Simple Form", :position[2, 5]);
    }
     # display a simple form a couple of times
    .do($form, 10, 10);
    .transform: :translate(10,40), :rotate(.1), :scale(.75);
    .do($form, 10, 10);
}

$page.graphics: {
    my PDF::Lite::Tiling-Pattern $pattern = .tiling-pattern(:BBox[0, 0, 25, 25], );
    $pattern.graphics: {
        # Set a background color
        .FillColor = rgb(.8, .8, .9);
        .Rectangle: |$pattern<BBox>;
        .paint: :fill;
        # Display an image
        my PDF::Lite::XObject $img = .load-image("t/images/lightbulb.gif");
        .do($img, 6, 2 );
    }
    # fill a rectangle using this pattern
    .FillColor = .use-pattern($pattern);
    .Rectangle(125, 10, 200, 100);
    .paint: :stroke, :fill;
}

$pdf.save-as: "examples/forms-and-patterns.pdf";

forms-and-patterns.pdf

Resources and Reuse

The to-xobject method can be used to convert a page to an XObject Form to lay-up one or more input pages on an output page.

use PDF::Lite;
my $pdf-with-images = PDF::Lite.open: "t/images.pdf";
my $pdf-with-text = PDF::Lite.open: "examples/sample-text.pdf";

my PDF::Lite $new-doc .= new;
$new-doc.media-box = [0, 0, 500, 400];

# add a page; layup imported pages and images
my PDF::Lite::Page $page = $new-doc.add-page;

my PDF::Lite::XObject $xobj-image = $pdf-with-images.page(1).images[6];
my PDF::Lite::XObject $xobj-with-text = $pdf-with-text.page(1).to-xobject;
my PDF::Lite::XObject $xobj-with-images = $pdf-with-images.page(1).to-xobject;

$page.graphics: {
    # scale up an image; use it as a semi-transparent background
    .FillAlpha = 0.5; 
    .do($xobj-image, 0, 0, :width(500), :height(400) );
    };

$page.graphics: {
    # overlay pages; scale these down
    .do($xobj-with-text, 20, 100, :width(300) );
    .do($xobj-with-images, 300, 100, :width(200) );
}

# copy whole pages from a document
for 1 .. $pdf-with-text.page-count -> $page-no {
    $new-doc.add-page: $pdf-with-text.page($page-no);
}

$new-doc.save-as: "examples/reuse.pdf";

reuse.pdf Page 1 reuse.pdf Page 2

To list all images and forms for each page

use PDF::Lite;
my $pdf = PDF::Lite.open: "t/images.pdf";
for 1 ... $pdf.page-count -> $page-no {
    say "page: $page-no";
    my PDF::Lite::Page $page = $pdf.page: $page-no;
    # get all X-Objects (images and forms) on the page
    my PDF::Lite::XObject %object = $page.resources('XObject');

    # also report on images embedded in the page content
    my $k = "(inline-0)";

    %object{++$k} = $_
        for $page.gfx.inline-images;

    for %object.keys -> $key {
        my $xobject = %object{$key};
        my $subtype = $xobject<Subtype>;
        my $size = $xobject.encoded.codes;
        say "\t$key: $subtype {$xobject.width}x{$xobject.height} $size bytes"
    }
}

Resource types are: ExtGState (graphics state), ColorSpace, Pattern, Shading, XObject (forms and images) and Properties.

Resources of type Pattern and XObject/Image may have further associated resources.

Whole pages or individual resources may be copied from one PDF to another.

Graphics Operations

A full range of general graphics is available for drawing and displaying text.

use PDF::Lite;
my PDF::Lite $pdf .= new;
my $page = $pdf.add-page;

# Draw a simple Bézier curve:

# ------------------------
# Alternative 1: Using operator functions (see PDF::Content)

sub draw-curve1($gfx) {
    $gfx.Save;
    $gfx.MoveTo(175, 720);
    $gfx.LineTo(175, 700);
    $gfx.CurveToInitial( 300, 800,  400, 720 );
    $gfx.ClosePath;
    $gfx.Stroke;
    $gfx.Restore;
}

draw-curve1($page.gfx);

# ------------------------
# Alternative 2: draw from content instructions string:

sub draw-curve2($gfx) {
    $gfx.ops: q:to"--END--"
        q                     % save
          175 720 m           % move-to
          175 700 l           % line-to
          300 800 400 720 v   % curve-to
          h                   % close
          S                   % stroke
        Q                     % restore
        --END--
}
draw-curve2($pdf.add-page.gfx);

# ------------------------
# Alternative 3: draw from raw data

sub draw-curve3($gfx) {
    $gfx.ops: [
         'q',               # save,
         :m[175, 720],      # move-to
         :l[175, 700],      # line-to 
         :v[300, 800,
            400, 720],      # curve-to
         :h[],              # close (or equivalently, 'h')
         'S',               # stroke (or equivalently, :S[])
         'Q',               # restore
     ];
}
draw-curve3($pdf.add-page.gfx);

Please see PDF::API6 Appendix I - Graphics for a description of available operators and graphics.

Graphics can also be read from an existing PDF file:

use PDF::Lite;
my $pdf = PDF::Lite.open: "examples/hello-world.pdf";
say $pdf.page(1).gfx.ops;

Graphics and Rendering

A number of variables are maintained that describe the graphics state. In many cases these may be set directly:

use PDF::Lite;
my PDF::Lite $pdf .= new;
my PDF::Lite::Page $page = $pdf.add-page;
$page.graphics: {
    .text: {  # start a text block
        .CharSpacing = 1.0;     # show text with wide spacing
        # Set the font to twelve point helvetica
        my $face = $pdf.core-font( :family<Helvetica>, :weight<bold>, :style<italic> );
        .font = [ $face, 10 ];
        .TextLeading = 12; # new-line advances 12 points
        .text-position = 10, 20;
        .say("Sample Text", :position[10, 20]);
        # '$gfx.say' has updated the text position to the next line
        say .text-position;
    } # restore previous text state
    say .CharSpacing; # restored to 0
}

A renderer callback can be specified when reading content. This will be called for each graphics operation and has access to the graphics state, via the $*gfx dynamic variable.

use PDF::Lite;
use PDF::Content::Ops :OpCode;
my PDF::Lite $pdf .= open: "examples/hello-world.pdf";

my &callback = -> $op, *@args {
   given $op {
       when SetTextMatrix {
           say "text matrix set to: {$*gfx.TextMatrix}";
       }
   }
}
my $gfx = $pdf.page(1).render(:&callback);
# text matrix set to: 1 0 0 1 10 10

See also