PDF-Make

 view release on metacpan or  search on metacpan

lib/PDF/Make/Builder.pm  view on Meta::CPAN

    my ($self, %args) = @_;
    _encrypt_args $self, \%args;
    return $self;
}

# ── Layers/OCG ────────────────────────────────────────────

sub add_layer {
    my ($self, $name, %args) = @_;
    my $xs_doc = doc $self;
    my $layer = PDF::Make::Layer->create($xs_doc, $name);
    $layer->visible($args{visible}) if defined $args{visible};
    my $num = $layer->write_to_doc($xs_doc);
    my $cur = page $self;
    if ($cur) {
        $cur->xs_page->add_ocg($layer->res_name, $num);
    }
    my $layers = _layers $self;
    $layers->{$name} = $layer;
    _layers $self, $layers;
    return $self;
}

sub begin_layer {
    my ($self, $name) = @_;
    my $layers = _layers $self;
    my $layer = $layers->{$name} // die "PDF::Make::Builder: unknown layer '$name'";
    my $cur = page $self;
    die "PDF::Make::Builder: no current page" unless $cur;
    $cur->canvas->begin_layer($layer->res_name);
    return $self;
}

sub end_layer {
    my ($self) = @_;
    my $cur = page $self;
    die "PDF::Make::Builder: no current page" unless $cur;
    $cur->canvas->end_layer;
    return $self;
}

# ── Redaction ─────────────────────────────────────────────

sub mark_redaction {
    my ($self, %args) = @_;
    my $page_index = delete $args{page} // 0;
    my $ps = pages $self;
    die "PDF::Make::Builder: page index out of range"
        unless $page_index >= 0 && $page_index < scalar @$ps;

    my $bp = $ps->[$page_index];

    # Register the /Redact annotation for downstream tools.
    PDF::Make::Redaction->mark($bp->xs_page, %args);

    my $rect = $args{rect} or return $self;
    my ($x0, $y0, $x1, $y1) = @$rect;
    my ($w, $h) = ($x1 - $x0, $y1 - $y0);
    return $self if $w <= 0 || $h <= 0;

    my $colour = $args{overlay_colour} // $args{overlay_color} // '#000';
    my $text   = $args{overlay_text};
    my $size   = $args{overlay_font_size} // 10;

    # Remember the redaction so save-time can rewrite the content stream
    # to actually remove text that falls inside the rect, then repaint
    # the overlay on top of the filtered stream.
    my $list = $bp->redactions;
    push @$list, {
        rect         => [$x0, $y0, $x1, $y1],
        overlay_text => $text,
        overlay_size => $size,
        overlay_fill => $colour,
    };
    $bp->redactions($list);

    # Eagerly paint the opaque cover so that even a user who never calls
    # apply_redactions sees the sensitive area visually hidden.
    my $font   = $self->font;
    my ($r, $g, $b) = $font->hex_to_rgb($colour);
    my $canvas = $bp->canvas;

    $canvas->q->rg($r, $g, $b)->re($x0, $y0, $w, $h)->f->Q;

    if (defined $text && length $text) {
        my $res = $font->ensure_loaded($bp->xs_page, 'normal');
        my ($tr, $tg, $tb) = $font->hex_to_rgb('#fff');
        my $tw = $font->measure_text($text) * ($size / ($font->size || 9));
        my $tx = $x0 + ($w - $tw) / 2;
        $tx = $x0 + 4 if $tw > $w - 4;
        my $ty = $y0 + ($h - $size) / 2 + 1;
        $canvas->q
               ->BT
               ->rg($tr, $tg, $tb)
               ->Tf($res, $size)
               ->Tm(1, 0, 0, 1, $tx, $ty)
               ->Tj($text)
               ->ET
               ->Q;
    }

    return $self;
}

sub apply_redactions {
    my ($self) = @_;
    _apply_redactions_pending $self, 1;
    return $self;
}

sub sanitize {
    my ($self) = @_;
    _sanitize_pending $self, 1;
    return $self;
}

# Internal: filter the canvas bytes for one page through the redaction
# rewriter and re-paint overlay text.  Called from save() / to_bytes()
# when $builder->_apply_redactions_pending is set.
sub _rewrite_redacted_canvas_bytes {
    my ($self, $bp) = @_;
    my $reds = $bp->redactions;
    return $bp->canvas->to_bytes unless $reds && @$reds;

    my $raw_bytes = $bp->canvas->to_bytes;
    my @rects = map { $_->{rect} } @$reds;
    my $filtered = PDF::Make::Redaction->rewrite_stream($raw_bytes, \@rects);

    # Re-paint overlay text for each redaction on a fresh small canvas
    # so those BT..ET blocks come AFTER the filtered stream and are not
    # themselves dropped by the filter.
    my $overlay_canvas = PDF::Make::Canvas->new;
    my $font = $self->font;
    for my $r (@$reds) {
        my ($x0, $y0, $x1, $y1) = @{$r->{rect}};
        my ($w, $h) = ($x1 - $x0, $y1 - $y0);
        next if $w <= 0 || $h <= 0;

        # Black rect (re-paint since the filter kept the original, but
        # redundant paints are harmless and guard against any edge case
        # where the original rect op was adjacent to a dropped block).
        my ($br, $bg, $bb) = $font->hex_to_rgb($r->{overlay_fill} // '#000');
        $overlay_canvas->q->rg($br, $bg, $bb)->re($x0, $y0, $w, $h)->f->Q;

        my $text = $r->{overlay_text};
        next unless defined $text && length $text;

        my $size = $r->{overlay_size} || 10;
        my $res  = $font->ensure_loaded($bp->xs_page, 'normal');
        my ($tr, $tg, $tb) = $font->hex_to_rgb('#fff');
        my $tw = $font->measure_text($text) * ($size / ($font->size || 9));
        my $tx = $x0 + ($w - $tw) / 2;
        $tx = $x0 + 4 if $tw > $w - 4;
        my $ty = $y0 + ($h - $size) / 2 + 1;
        $overlay_canvas->q
                       ->BT
                       ->rg($tr, $tg, $tb)
                       ->Tf($res, $size)
                       ->Tm(1, 0, 0, 1, $tx, $ty)
                       ->Tj($text)
                       ->ET
                       ->Q;
    }

    return $filtered . $overlay_canvas->to_bytes;
}

# ── Color Management ──────────────────────────────────────

sub set_color_space {
    my ($self, $type, %args) = @_;
    my $cs;
    if ($type eq 'sRGB') {
        $cs = PDF::Make::Color->srgb;
    } elsif ($type eq 'separation') {
        $cs = PDF::Make::Color->separation(
            $args{name}, $args{c} // 0, $args{m} // 0, $args{y} // 0, $args{k} // 0
        );
    } else {
        die "PDF::Make::Builder: unknown color space '$type'";
    }
    my $xs_doc = doc $self;
    $cs->write_to_doc($xs_doc);
    return $self;
}

# ── Tagged PDF / Accessibility ────────────────────────────

sub enable_tagging {
    my ($self) = @_;
    my $xs_doc = doc $self;
    my $tree = PDF::Make::Structure->create_tree($xs_doc);
    _struct_tree $self, $tree;
    _tagging $self, 1;
    return $self;
}

# ── Forms ─────────────────────────────────────────────────

my %_field_class = (
    text     => 'PDF::Make::Builder::Form::Field::Text',
    checkbox => 'PDF::Make::Builder::Form::Field::Checkbox',
    radio    => 'PDF::Make::Builder::Form::Field::Radio',
    combo    => 'PDF::Make::Builder::Form::Field::Combo',
    dropdown => 'PDF::Make::Builder::Form::Field::Combo',
    listbox  => 'PDF::Make::Builder::Form::Field::Listbox',
    list     => 'PDF::Make::Builder::Form::Field::Listbox',
    button   => 'PDF::Make::Builder::Form::Field::Button',
);

sub add_field {
    my ($self, %args) = @_;

    my $type = delete $args{type} // die "PDF::Make::Builder: add_field requires type";
    my $name = delete $args{name} // die "PDF::Make::Builder: add_field requires name";

    my $class = $_field_class{$type}
        // die "PDF::Make::Builder: unknown field type '$type'";

    # Default to structured mode. Enter raw mode for explicit coordinates
    # or when requested directly.
    my $raw_mode = delete $args{raw_mode};
    if (!defined $raw_mode) {
        $raw_mode = (exists $args{rect} || exists $args{x} || exists $args{y}) ? 1 : 0;
    }

lib/PDF/Make/Builder.pm  view on Meta::CPAN


    # Field-specific passthrough
    $fargs{options} = $args{options} if exists $args{options};
    $fargs{caption} = $args{caption} if exists $args{caption};
    $fargs{on_value} = $args{on_value} if exists $args{on_value};

    my $field = $class->new(%fargs);
    $field->add($self);

    return $self;
}

sub flatten_form {
    my ($self) = @_;
    _flatten_pending $self, 1;
    return $self;
}

# ── Digital Signatures ────────────────────────────────────

sub sign {
    my ($self, %args) = @_;
    _sign_args $self, \%args;
    return $self;
}

# ── TOC ────────────────────────────────────────────────────

sub add_toc {
    my ($self, %args) = @_;
    my $cfg = configure $self;
    my $toc_cfg = $cfg->{toc} // {};
    my $cur = page $self;
    my $default_toc_page = $cur ? ($cur->num - 1) : 0;
    %args = (%$toc_cfg, %args);
    $args{page_index} = $default_toc_page unless exists $args{page_index};
    toc $self, PDF::Make::Builder::TOC->new(%args);
    return $self;
}

# ── Save ───────────────────────────────────────────────────

sub save {
    my ($self) = @_;

    # Render TOC if present
    my $cur = page $self;
    if ($cur) {
        my $t = toc $self;
        if ($t && @{$t->entries}) {
            $t->render($self);
        }
    }

    # Render headers/footers onto each page's canvas, then finalize
    my $all_pages = pages $self;
    my $offset = page_offset $self;
    my $rewrite = _apply_redactions_pending $self;
    for my $bp (@$all_pages) {
        if ($bp->imported) {
            # Imported pages keep their original content, but any overlay
            # drawing issued against the Builder's canvas (e.g. a box
            # around extracted text) needs to be appended so it renders
            # on top of the source graphics.
            my $overlay = $bp->canvas->to_bytes;
            if (defined $overlay && length $overlay) {
                $bp->xs_page->append_content($overlay);
            }
            next;
        }
        my $hdr = $bp->header;
        my $ftr = $bp->footer;
        my $pnum = $bp->num + $offset;

        if ($hdr) {
            $hdr->render($self, $bp, $pnum);
        }
        if ($ftr) {
            $ftr->render($self, $bp, $pnum);
        }

        # Finalize page content; filter through the redaction rewriter
        # when the user has called apply_redactions.
        my $bytes = ($rewrite && @{$bp->redactions})
            ? $self->_rewrite_redacted_canvas_bytes($bp)
            : $bp->canvas->to_bytes;
        $bp->xs_page->set_content($bytes);
    }

    if (_sanitize_pending $self) {
        PDF::Make::Redaction->sanitize(doc $self);
    }

    # Apply deferred watermarks after all page content is set
    {
        my $wms = _watermarks $self;
        if ($wms && @$wms) {
            my $xs_doc = doc $self;
            for my $wm (@$wms) {
                $xs_doc->add_watermark($wm);
            }
        }
    }

    # Flatten form fields if requested (after set_content so canvas is committed)
    if (_flatten_pending $self) {
        my $xs_doc = doc $self;
        my $form = eval { PDF::Make::FormPtr::get($xs_doc) };
        $form->flatten if $form;
    }

    # Finalize form if any fields were added
    {
        my $xs_doc = doc $self;
        my $form = eval { PDF::Make::FormPtr::get($xs_doc) };
        if ($form) {
            PDF::Make::FormPtr::finalize($form);
        }
    }

    # Apply encryption if configured.  The actual /Encrypt dict is built,
    # and per-object encryption applied, inside pdfmake_doc_write.
    my $enc = _encrypt_args $self;
    if ($enc) {
        my $algo  = $enc->{algorithm}      // 'AES-256';
        my $user  = $enc->{user_password}  // $enc->{password} // '';
        my $owner = $enc->{owner_password} // $user;

lib/PDF/Make/Builder.pm  view on Meta::CPAN

For builder-layer coordinates (bottom-left origin), provide C<x>, C<y>, C<w>, and
C<h> instead of C<rect>. These are converted to PDF annotation coordinates
automatically.

=head2 Attachments

=head3 attach(%args)

    $b->attach(
        name        => 'data.csv',          # required
        data        => $csv_string,         # provide data or path
        path        => '/path/to/file',     # alternative to data
        mime        => 'text/csv',          # auto-detected if omitted
        description => 'Raw export data',
    );

Embed a file attachment in the PDF.

=head2 Watermarks

=head3 add_watermark(%args)

    $b->add_watermark(
        text     => 'DRAFT',       # required
        opacity  => 0.3,
        rotation => 45,
        color    => [0.8, 0.2, 0.2],
        size     => 72,
    );

Add a text watermark to all pages. See L<PDF::Make::Watermark> for all
options.

=head2 Layers (Optional Content Groups)

=head3 add_layer($name, %args)

    $b->add_layer('Dimensions', visible => 1);

Create a named layer on the current page.

=head3 begin_layer($name)

    $b->begin_layer('Dimensions');

Start drawing on the named layer.

=head3 end_layer()

    $b->end_layer;

Stop drawing on the current layer.

=head2 Redaction

=head3 mark_redaction(%args)

    $b->mark_redaction(
        page          => 0,                     # 0-based page index
        rect          => [100, 700, 300, 720],
        overlay_color => [0, 0, 0],
        overlay_text  => 'REDACTED',
    );

Mark a rectangular area for redaction.

=head3 apply_redactions()

Apply all redaction marks across all pages.

=head3 sanitize()

Remove all metadata (title, author, etc.) from the document.

=head2 Color Spaces

=head3 set_color_space($type, %args)

    $b->set_color_space('sRGB');
    $b->set_color_space('separation',
        name => 'PANTONE 185 C', c => 0, m => 0.81, y => 0.69, k => 0);

Register a color space in the document.

=head2 Tagged PDF (Accessibility)

=head3 enable_tagging()

    $b->enable_tagging;

Enable tagged PDF output with a structure tree for accessibility.

=head2 Form Fields

=head3 add_field(%args)

Structured mode (cursor-based component rendering):

    $b->add_field(
        type          => 'text',
        name          => 'email',
        label         => 'Email Address',
        w             => 300,
        default_value => 'user@example.com',
    );

    $b->add_field(
        type  => 'checkbox',
        name  => 'agree',
        label => 'I agree to the terms',
        w     => 16, h => 16,
    );

Raw mode (explicit coordinates):

    $b->add_field(
        type     => 'text',
        name     => 'email',
        raw_mode => 1,
        rect     => [72, 700, 300, 720],
        default  => 'user@example.com',
    );



( run in 0.580 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )