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 )