File Coverage

blib/lib/PDF/Make/Builder.pm
Criterion Covered Total %
statement 883 918 96.1
branch 221 292 75.6
condition 184 321 57.3
subroutine 130 132 98.4
pod 58 79 73.4
total 1476 1742 84.7


line stmt bran cond sub pod time code
1             package PDF::Make::Builder;
2 42     42   4263221 use strict;
  42         66  
  42         1359  
3 42     42   166 use warnings;
  42         58  
  42         2180  
4 42     42   227 use File::Basename qw(dirname);
  42         61  
  42         2966  
5 42     42   186 use File::Path qw(make_path);
  42         113  
  42         2198  
6 42     42   19132 use Object::Proto;
  42         25740  
  42         2131  
7 42     42   16015 use PDF::Make::Document;
  42         79  
  42         1419  
8 42     42   15992 use PDF::Make::Canvas;
  42         95  
  42         1764  
9 42     42   14953 use PDF::Make::Import;
  42         94  
  42         1202  
10 42     42   15238 use PDF::Make::Page qw(:fonts);
  42         91  
  42         6469  
11 42     42   16879 use PDF::Make::Builder::Font;
  42         99  
  42         1311  
12 42     42   16556 use PDF::Make::Builder::Page;
  42         102  
  42         1291  
13 42     42   17657 use PDF::Make::Builder::Page::Header;
  42         96  
  42         1223  
14 42     42   16775 use PDF::Make::Builder::Page::Footer;
  42         117  
  42         1109  
15 42     42   16410 use PDF::Make::Builder::Text;
  42         105  
  42         1315  
16 42     42   15632 use PDF::Make::Builder::Text::H1;
  42         121  
  42         1022  
17 42     42   15906 use PDF::Make::Builder::Text::H2;
  42         96  
  42         1044  
18 42     42   15357 use PDF::Make::Builder::Text::H3;
  42         115  
  42         1097  
19 42     42   15430 use PDF::Make::Builder::Text::H4;
  42         106  
  42         1020  
20 42     42   15518 use PDF::Make::Builder::Text::H5;
  42         105  
  42         1012  
21 42     42   15696 use PDF::Make::Builder::Text::H6;
  42         103  
  42         1112  
22 42     42   15718 use PDF::Make::Builder::Shape::Line;
  42         109  
  42         1112  
23 42     42   15818 use PDF::Make::Builder::Shape::Box;
  42         239  
  42         1187  
24 42     42   16151 use PDF::Make::Builder::Shape::Circle;
  42         110  
  42         1057  
25 42     42   16192 use PDF::Make::Builder::Shape::Ellipse;
  42         112  
  42         1037  
26 42     42   15963 use PDF::Make::Builder::Shape::Pie;
  42         119  
  42         1155  
27 42     42   16988 use PDF::Make::Builder::TOC;
  42         109  
  42         1207  
28 42     42   15613 use PDF::Make::Builder::Image;
  42         115  
  42         1088  
29 42     42   15948 use PDF::Make::Builder::Layout;
  42         104  
  42         1066  
30 42     42   17387 use PDF::Make::Builder::Form::Field;
  42         97  
  42         1197  
31 42     42   16298 use PDF::Make::Builder::Form::Field::Text;
  42         95  
  42         1001  
32 42     42   15344 use PDF::Make::Builder::Form::Field::Checkbox;
  42         103  
  42         976  
33 42     42   15494 use PDF::Make::Builder::Form::Field::Radio;
  42         94  
  42         1048  
34 42     42   15626 use PDF::Make::Builder::Form::Field::Combo;
  42         95  
  42         1061  
35 42     42   16075 use PDF::Make::Builder::Form::Field::Listbox;
  42         114  
  42         1100  
36 42     42   15939 use PDF::Make::Builder::Form::Field::Button;
  42         139  
  42         1143  
37 42     42   16464 use PDF::Make::Attachment;
  42         100  
  42         973  
38 42     42   13930 use PDF::Make::Color;
  42         84  
  42         941  
39 42     42   13407 use PDF::Make::Extract;
  42         91  
  42         875  
40 42     42   14494 use PDF::Make::Extract::Result;
  42         104  
  42         1108  
41 42     42   223 use PDF::Make::Font;
  42         58  
  42         907  
42 42     42   15543 use PDF::Make::Layer;
  42         94  
  42         998  
43 42     42   15538 use PDF::Make::Parser;
  42         108  
  42         900  
44 42     42   136 use PDF::Make::Reader;
  42         54  
  42         535  
45 42     42   13858 use PDF::Make::Redaction;
  42         88  
  42         975  
46 42     42   16681 use PDF::Make::Signature;
  42         123  
  42         1175  
47 42     42   14331 use PDF::Make::Structure;
  42         98  
  42         992  
48 42     42   14176 use PDF::Make::Watermark;
  42         102  
  42         2789  
49              
50             our $VERSION = '0.03';
51              
52             BEGIN {
53 42     42   4806 Object::Proto::define('PDF::Make::Builder',
54             'file_name:Str:required',
55             'doc:Any',
56             'pages:ArrayRef:default([])',
57             'page:Any',
58             'page_args:HashRef:default({})',
59             'page_offset:Int:default(0)',
60             'onsave_cbs:ArrayRef:default([])',
61             'configure:HashRef:default({})',
62             'font:Any',
63             'toc:Any',
64             '_header_args:HashRef',
65             '_footer_args:HashRef',
66             '_outlines:HashRef:default({})',
67             '_layers:HashRef:default({})',
68             '_encrypt_args:HashRef',
69             '_sign_args:HashRef',
70             '_watermarks:ArrayRef:default([])',
71             '_tagging:Any',
72             '_struct_tree:Any',
73             '_struct_stack:ArrayRef:default([])',
74             '_flatten_pending:Bool:default(0)',
75             '_apply_redactions_pending:Bool:default(0)',
76             '_sanitize_pending:Bool:default(0)',
77             );
78 42         137291 Object::Proto::import_accessors('PDF::Make::Builder');
79             }
80              
81             sub BUILD {
82 167     167 0 6033611 my ($self) = @_;
83 167         5840 doc $self, PDF::Make::Document->new;
84              
85             # Default font
86 167         376 my $cfg = configure $self;
87 167   100     1093 my $font_args = $cfg->{text}{font} // {};
88             my %font_init = (
89             colour => $font_args->{colour} // '#000',
90             size => $font_args->{size} // 9,
91 167   100     1791 family => $font_args->{family} // 'Helvetica',
      100        
      100        
92             );
93 167 50       487 $font_init{line_height} = $font_args->{line_height} if defined $font_args->{line_height};
94 167         1833 font $self, PDF::Make::Builder::Font->new(%font_init);
95              
96             # Store header/footer config for new pages
97 167 100       564 if ($cfg->{page_header}) {
98 1         4 _header_args $self, $cfg->{page_header};
99             }
100 167 100       851 if ($cfg->{page_footer}) {
101 1         8 _footer_args $self, $cfg->{page_footer};
102             }
103             }
104              
105             # ── Page management ────────────────────────────────────────
106              
107             sub add_page {
108 146     146 1 2453 my ($self, %args) = @_;
109              
110 146   100     427 my $page_size = $args{page_size} // 'A4';
111 146         487 my ($pw, $ph) = PDF::Make::Builder::Page::page_dimensions($page_size);
112 146   100     474 my $padding = $args{padding} // 20;
113 146         246 my $xs_doc = doc $self;
114 146         2126 my $xs_page = $xs_doc->add_page($pw, $ph);
115 146         3282 my $canvas = PDF::Make::Canvas->new;
116              
117 146         300 my $pages = pages $self;
118 146         242 my $num = scalar(@$pages) + 1;
119              
120             # Build header/footer from stored config
121 146         250 my $hdr_args = _header_args $self;
122 146         184 my $ftr_args = _footer_args $self;
123 146 100       416 my $header = $hdr_args ? PDF::Make::Builder::Page::Header->new(%$hdr_args) : undef;
124 146 100       355 my $footer = $ftr_args ? PDF::Make::Builder::Page::Footer->new(%$ftr_args) : undef;
125              
126             my $bp = PDF::Make::Builder::Page->new(
127             page_size => $page_size,
128             background => $args{background} // '#fff',
129 146   100     2182 columns => $args{columns} // 1,
      100        
130             padding => $padding,
131             num => $num,
132             w => $pw,
133             h => $ph,
134             canvas => $canvas,
135             xs_page => $xs_page,
136             header => $header,
137             footer => $footer,
138             );
139              
140             # Draw background if not white
141 146         396 my $bg = $bp->background;
142 146 100 66     662 if ($bg && $bg ne '#fff' && $bg ne '#ffffff') {
      66        
143 2         10 my ($r, $g, $b) = (font $self)->hex_to_rgb($bg);
144 2         146 $canvas->q->rg($r, $g, $b)->re(0, 0, $pw, $ph)->f->Q;
145             }
146              
147 146         294 push @$pages, $bp;
148 146         236 pages $self, $pages;
149 146         234 page $self, $bp;
150              
151 146         446 return $self;
152             }
153              
154             sub open_page {
155 4     4 1 815 my ($self, $num) = @_;
156 4         9 my $pages = pages $self;
157 4 100 66     38 die "PDF::Make::Builder: page $num does not exist" unless $num >= 1 && $num <= scalar @$pages;
158 3         16 page $self, $pages->[$num - 1];
159 3         8 return $self;
160             }
161              
162             sub set_columns {
163 4     4 1 19 my ($self, $n) = @_;
164 4         9 my $p = page $self;
165 4 100       17 die "PDF::Make::Builder: no current page" unless $p;
166 3         17 $p->columns($n);
167 3         8 return $self;
168             }
169              
170             sub page_width {
171 2     2 0 10 my ($self) = @_;
172 2         4 my $p = page $self;
173 2 100       15 return $p ? $p->w : 0;
174             }
175              
176             sub page_height {
177 2     2 0 10 my ($self) = @_;
178 2         4 my $p = page $self;
179 2 100       10 return $p ? $p->h : 0;
180             }
181              
182             sub current_x {
183 2     2 0 5 my ($self) = @_;
184 2         4 my $p = page $self;
185 2 100 33     12 return $p ? ($p->x || $p->content_x) : 0;
186             }
187              
188             sub current_y {
189 7     7 0 12 my ($self) = @_;
190 7         8 my $p = page $self;
191 7 100       20 return $p ? $p->cursor_y : 0;
192             }
193              
194             sub content_left {
195 2     2 0 4 my ($self) = @_;
196 2         3 my $p = page $self;
197 2 100       8 return $p ? $p->content_x : 0;
198             }
199              
200             sub content_right {
201 2     2 0 5 my ($self) = @_;
202 2         3 my $p = page $self;
203 2 100       8 return $p ? ($p->content_x + $p->width) : 0;
204             }
205              
206             sub content_bottom {
207 2     2 0 4 my ($self) = @_;
208 2         2 my $p = page $self;
209 2 100       9 return $p ? $p->bottom_y : 0;
210             }
211              
212             sub content_top {
213 2     2 0 3 my ($self) = @_;
214 2         4 my $p = page $self;
215 2 100       8 return $p ? $p->top_y : 0;
216             }
217              
218             sub cursor_move_to {
219 4     4 0 12 my ($self, $x, $y) = @_;
220 4         6 my $p = page $self;
221 4 100       15 die "PDF::Make::Builder: no current page" unless $p;
222 3 100       10 $p->x($x) if defined $x;
223 3 100       6 $p->y($y) if defined $y;
224 3         7 return $self;
225             }
226              
227             sub cursor_advance_y {
228 3     3 0 533 my ($self, $dy) = @_;
229 3         7 my $p = page $self;
230 3 100       13 die "PDF::Make::Builder: no current page" unless $p;
231 2   100     7 $dy //= 0;
232 2         5 $p->y($p->cursor_y + $dy);
233 2         4 return $self;
234             }
235              
236             # ── Layout ────────────────────────────────────────────────
237              
238             sub layout {
239 9     9 0 35 my ($self) = @_;
240 9         59 return PDF::Make::Builder::Layout->new(builder => $self);
241             }
242              
243             # ── Header/Footer ─────────────────────────────────────────
244              
245             sub add_page_header {
246 14     14 1 138 my ($self, %args) = @_;
247 14         38 _header_args $self, \%args;
248 14         23 my $p = page $self;
249 14 100       26 if ($p) {
250 1         14 $p->header(PDF::Make::Builder::Page::Header->new(%args));
251             }
252 14         32 return $self;
253             }
254              
255             sub add_page_footer {
256 7     7 1 347 my ($self, %args) = @_;
257 7         19 _footer_args $self, \%args;
258 7         11 my $p = page $self;
259 7 100       16 if ($p) {
260 1         14 $p->footer(PDF::Make::Builder::Page::Footer->new(%args));
261             }
262 7         36 return $self;
263             }
264              
265             sub remove_page_header {
266 2     2 1 2535 my ($self) = @_;
267 2         9 _header_args $self, undef;
268 2         4 my $p = page $self;
269 2 50       20 $p->header(undef) if $p;
270 2         7 return $self;
271             }
272              
273             sub remove_page_footer {
274 2     2 1 5 my ($self) = @_;
275 2         9 _footer_args $self, undef;
276 2         2 my $p = page $self;
277 2 50       27 $p->footer(undef) if $p;
278 2         5 return $self;
279             }
280              
281             sub remove_page_header_and_footer {
282 1     1 1 3 my ($self) = @_;
283 1         5 $self->remove_page_header;
284 1         3 $self->remove_page_footer;
285 1         5 return $self;
286             }
287              
288             # ── Font ───────────────────────────────────────────────────
289              
290             sub load_font {
291 1     1 1 5 my ($self, %args) = @_;
292 1         15 font $self, PDF::Make::Builder::Font->new(%args);
293 1         5 return $self;
294             }
295              
296             # ── Text content ───────────────────────────────────────────
297              
298             sub _apply_configure {
299 352     352   546 my ($self, $type, $args) = @_;
300 352         418 my $cfg = configure $self;
301 352   100     645 my $defaults = $cfg->{$type} // {};
302 352 100       570 if ($defaults->{font}) {
303 3   50     6 $args->{font} = { %{$defaults->{font}}, %{$args->{font} // {}} };
  3         10  
  3         46  
304             }
305 352         456 return $args;
306             }
307              
308             sub _tag_begin {
309 362     362   475 my ($self, $tag_type) = @_;
310 362 100       640 return unless _tagging $self;
311 2         3 my $tree = _struct_tree $self;
312 2 50       17 return unless $tree;
313 2         3 my $stack = _struct_stack $self;
314 2 50       12 my $parent = @$stack ? $stack->[-1] : $tree->root;
315 2         16 my $elem = $parent->add_child($tag_type);
316 2         3 push @$stack, $elem;
317 2         7 _struct_stack $self, $stack;
318             }
319              
320             sub _tag_end {
321 362     362   444 my ($self) = @_;
322 362 100       700 return unless _tagging $self;
323 2         3 my $stack = _struct_stack $self;
324 2 50       4 pop @$stack if @$stack;
325 2         6 _struct_stack $self, $stack;
326             }
327              
328             sub add_text {
329 308     308 1 4728 my ($self, %args) = @_;
330 308         707 $self->_apply_configure('text', \%args);
331 308         552 $self->_tag_begin('P');
332 308         1582 my $t = PDF::Make::Builder::Text->new(%args);
333 308         781 $t->add($self);
334 308         664 $self->_tag_end;
335 308         957 return $self;
336             }
337              
338             sub add_h1 {
339 30     30 1 396 my ($self, %args) = @_;
340 30         96 $self->_apply_configure('h1', \%args);
341 30         68 $self->_tag_begin('H1');
342 30         305 my $t = PDF::Make::Builder::Text::H1->new(%args);
343 30         116 $t->add($self);
344 30         98 $self->_tag_end;
345 30         163 return $self;
346             }
347              
348             sub add_h2 {
349 5     5 0 370 my ($self, %args) = @_;
350 5         16 $self->_apply_configure('h2', \%args);
351 5         13 $self->_tag_begin('H2');
352 5         55 my $t = PDF::Make::Builder::Text::H2->new(%args);
353 5         22 $t->add($self);
354 5         21 $self->_tag_end;
355 5         34 return $self;
356             }
357              
358             sub add_h3 {
359 3     3 0 321 my ($self, %args) = @_;
360 3         12 $self->_apply_configure('h3', \%args);
361 3         9 $self->_tag_begin('H3');
362 3         41 my $t = PDF::Make::Builder::Text::H3->new(%args);
363 3         14 $t->add($self);
364 3         9 $self->_tag_end;
365 3         20 return $self;
366             }
367              
368             sub add_h4 {
369 2     2 0 311 my ($self, %args) = @_;
370 2         7 $self->_apply_configure('h4', \%args);
371 2         6 $self->_tag_begin('H4');
372 2         27 my $t = PDF::Make::Builder::Text::H4->new(%args);
373 2         10 $t->add($self);
374 2         6 $self->_tag_end;
375 2         46 return $self;
376             }
377              
378             sub add_h5 {
379 2     2 0 363 my ($self, %args) = @_;
380 2         7 $self->_apply_configure('h5', \%args);
381 2         5 $self->_tag_begin('H5');
382 2         29 my $t = PDF::Make::Builder::Text::H5->new(%args);
383 2         11 $t->add($self);
384 2         6 $self->_tag_end;
385 2         22 return $self;
386             }
387              
388             sub add_h6 {
389 2     2 1 357 my ($self, %args) = @_;
390 2         6 $self->_apply_configure('h6', \%args);
391 2         6 $self->_tag_begin('H6');
392 2         26 my $t = PDF::Make::Builder::Text::H6->new(%args);
393 2         17 $t->add($self);
394 2         4 $self->_tag_end;
395 2         14 return $self;
396             }
397              
398             sub add_lines {
399 1     1 1 8 my ($self, @lines) = @_;
400 1         2 for my $line (@lines) {
401 3 100       6 if (ref $line eq 'HASH') {
402 1         4 $self->add_text(%$line);
403             } else {
404 2         4 $self->add_text(text => $line);
405             }
406             }
407 1         5 return $self;
408             }
409              
410             # ── Shapes ─────────────────────────────────────────────────
411              
412             sub add_line {
413 6     6 1 400 my ($self, %args) = @_;
414 6         81 my $s = PDF::Make::Builder::Shape::Line->new(%args);
415 6         43 $s->add($self);
416 6         54 return $self;
417             }
418              
419             sub add_box {
420 15     15 1 407 my ($self, %args) = @_;
421 15         128 my $s = PDF::Make::Builder::Shape::Box->new(%args);
422 15         63 $s->add($self);
423 15         73 return $self;
424             }
425              
426             sub add_circle {
427 1     1 1 5 my ($self, %args) = @_;
428 1         16 my $s = PDF::Make::Builder::Shape::Circle->new(%args);
429 1         6 $s->add($self);
430 1         8 return $self;
431             }
432              
433             sub add_ellipse {
434 1     1 1 7 my ($self, %args) = @_;
435 1         22 my $s = PDF::Make::Builder::Shape::Ellipse->new(%args);
436 1         6 $s->add($self);
437 1         9 return $self;
438             }
439              
440             sub add_pie {
441 1     1 1 7 my ($self, %args) = @_;
442 1         18 my $s = PDF::Make::Builder::Shape::Pie->new(%args);
443 1         6 $s->add($self);
444 1         14 return $self;
445             }
446              
447             # ── Images ─────────────────────────────────────────────────
448              
449             sub add_image {
450 10     10 1 76 my ($self, %args) = @_;
451 10         85 $self->_tag_begin('Figure');
452 10         111 my $img = PDF::Make::Builder::Image->new(%args);
453 10         44 $img->add($self);
454 10         66 $self->_tag_end;
455 10         139 return $self;
456             }
457              
458             # ── Metadata ───────────────────────────────────────────────
459              
460             sub title {
461 3     3 1 14 my ($self, $val) = @_;
462 3         41 (doc $self)->title($val);
463 3         12 return $self;
464             }
465              
466             sub author {
467 3     3 1 11 my ($self, $val) = @_;
468 3         19 (doc $self)->author($val);
469 3         25 return $self;
470             }
471              
472             sub subject {
473 1     1 1 3 my ($self, $val) = @_;
474 1         7 (doc $self)->subject($val);
475 1         3 return $self;
476             }
477              
478             sub keywords {
479 1     1 1 3 my ($self, $val) = @_;
480 1         7 (doc $self)->keywords($val);
481 1         3 return $self;
482             }
483              
484             sub creator {
485 1     1 1 3 my ($self, $val) = @_;
486 1         7 (doc $self)->creator($val);
487 1         3 return $self;
488             }
489              
490             sub producer {
491 1     1 1 3 my ($self, $val) = @_;
492 1         7 (doc $self)->producer($val);
493 1         3 return $self;
494             }
495              
496             # ── Outlines/Bookmarks ────────────────────────────────────
497              
498             sub add_outline {
499 10     10 1 52 my ($self, $title, %args) = @_;
500 10   50     48 my $page_index = $args{page} // 0;
501 10   100     37 my $dest_type = $args{dest} // 'Fit';
502 10         12 my $parent_key = $args{parent};
503 10   100     27 my $left = $args{left} // 0;
504 10   100     23 my $top = $args{top} // 0;
505 10   100     23 my $zoom = $args{zoom} // 0;
506              
507 10         16 my $outlines = _outlines $self;
508 10         11 my $item;
509              
510 10 100 100     46 if ($parent_key && $outlines->{$parent_key}) {
511 3         98 $item = $outlines->{$parent_key}->add_child(
512             $title, $page_index, $dest_type, $left, $top, $zoom
513             );
514             } else {
515 7         57 $item = (doc $self)->add_outline(
516             $title, $page_index, $dest_type, $left, $top, $zoom
517             );
518             }
519              
520 10         22 $outlines->{$title} = $item;
521 10         16 _outlines $self, $outlines;
522 10         36 return $self;
523             }
524              
525             # ── Links/Actions ─────────────────────────────────────────
526              
527             sub add_link {
528 20     20 1 884 my ($self, %args) = @_;
529 20         37 my $xs_doc = doc $self;
530              
531 20         41 my $target_builder_page;
532 20 100       46 if (defined $args{on_page}) {
533 10         13 my $idx = $args{on_page};
534 10         12 my $all = pages $self;
535 10 100 33     56 die "PDF::Make::Builder: on_page $idx does not exist" unless $all && $idx >= 0 && $idx < @$all;
      66        
536 9         14 $target_builder_page = $all->[$idx];
537             } else {
538 10         19 $target_builder_page = page $self;
539 10 50       24 die "PDF::Make::Builder: add_link requires a current page" unless $target_builder_page;
540             }
541              
542 19         23 my $rect;
543 19 100 33     58 if ($args{rect}) {
    50 33        
      0        
544 16         25 $rect = $args{rect};
545             } elsif (defined $args{x} || defined $args{y} || defined $args{w} || defined $args{h}) {
546             die "PDF::Make::Builder: add_link builder coords require x,y,w,h"
547 3 100 33     35 unless defined $args{x} && defined $args{y} && defined $args{w} && defined $args{h};
      66        
      66        
548              
549 2         6 my ($x, $y, $w, $h) = @args{qw/x y w h/};
550 2         3 my $x0 = $x;
551 2         3 my $x1 = $x + $w;
552 2         3 my $y0 = $y;
553 2         4 my $y1 = $y + $h;
554 2         5 $rect = [$x0, $y0, $x1, $y1];
555             } else {
556 0         0 die "PDF::Make::Builder: add_link requires rect => [x0,y0,x1,y1] or builder coords x,y,w,h";
557             }
558              
559 18   50     59 my $hl = $args{highlight} // 'Invert';
560 18         20 my $annot_num;
561              
562 18 100       60 if ($args{url}) {
    100          
    100          
    100          
563 3         53 $annot_num = $xs_doc->add_link_uri(@$rect, $args{url});
564             } elsif (defined $args{page}) {
565 12         130 $annot_num = $xs_doc->add_link_goto(@$rect, $args{page});
566             } elsif ($args{action}) {
567             # Named action: NextPage, PrevPage, FirstPage, LastPage, Print
568 1         13 $annot_num = $xs_doc->add_link_named_action(@$rect, $args{action}, $hl);
569             } elsif ($args{file}) {
570             # External PDF link (GoToR)
571 1   50     13 my $action = $xs_doc->action_gotor($args{file}, $args{file_page} // 0, $args{new_window} // 0);
      50        
572 1         15 $annot_num = $xs_doc->add_link_with_action(@$rect, $action, $hl);
573             } else {
574 1         9 die "PDF::Make::Builder: add_link requires url, page, action, or file";
575             }
576              
577 17 50       38 if ($annot_num) {
578 17         17 my $page_obj;
579 17 100       28 if (defined $args{on_page}) {
580 9         11 my $target_page = $args{on_page};
581 9         24 $page_obj = $xs_doc->get_page($target_page);
582 9 50       16 die "PDF::Make::Builder: on_page $target_page does not exist" unless $page_obj;
583             } else {
584 8         43 $page_obj = $target_builder_page->xs_page;
585             }
586 17         76 $page_obj->add_annot($annot_num);
587             }
588 17         55 return $self;
589             }
590              
591             # ── Annotations ──────────────────────────────────────────
592              
593             sub add_note {
594 10     10 1 806 my ($self, %args) = @_;
595              
596             # Visual note mode: draw a coloured callout box with lines of text
597 10 100 100     67 if (exists $args{lines} || (exists $args{text} && ref $args{text} eq 'ARRAY')) {
      100        
598 4   66     13 my $lines = $args{lines} // $args{text};
599 4   100     11 my $x = $args{x} // 72;
600 4   100     12 my $w = $args{w} // 300;
601 4   100     11 my $h = $args{h} // 70;
602 4   33     18 my $bg = $args{bg_colour} // $args{fill_colour} // '#fffbeb';
      50        
603 4   100     10 my $padding = $args{padding} // 12;
604 4   50     9 my $line_gap = $args{line_gap} // 14;
605 4   50     11 my $colour = $args{colour} // '#92400e';
606 4   50     11 my $size = $args{size} // 10;
607              
608 4         6 my $cur = page $self;
609 4 100       15 die "PDF::Make::Builder: add_note requires a current page" unless $cur;
610              
611 3         4 my $y;
612 3 100       8 if (defined $args{y}) {
613 1         3 $y = $args{y};
614             } else {
615 2         5 $y = $cur->cursor_y - $h;
616 2         6 $cur->advance_y($h + 6);
617             }
618              
619 3         10 $self->add_box(fill_colour => $bg, x => $x, y => $y, w => $w, h => $h);
620              
621 3         5 my $ty = $y + $h - $padding - $size;
622 3         7 for my $line (@$lines) {
623             my ($text, $fsize, $fcol, $italic) =
624             ref $line eq 'HASH'
625 7 100       22 ? (@{$line}{qw(text size colour italic)})
  1         3  
626             : ($line, undef, undef, 0);
627 7   66     22 $fsize //= $size;
628 7   66     44 $fcol //= $colour;
629 7 100       32 $self->add_text(
630             text => $text,
631             x => $x + $padding,
632             y => $ty,
633             w => $w - $padding * 2,
634             font => { size => $fsize, colour => $fcol, ($italic ? (italic => 1) : ()) },
635             );
636 7         17 $ty -= $line_gap;
637             }
638 3         16 return $self;
639             }
640              
641             # Annotation note mode (PDF sticky note)
642 6         17 my $xs_doc = doc $self;
643 6   100     30 my $rect = $args{rect} // die "PDF::Make::Builder: add_note requires rect or lines";
644 5   100     23 my $text = $args{text} // '';
645 5   100     24 my $icon = $args{icon} // 'Note';
646 5   100     20 my $open = $args{open} // 0;
647              
648             # Resolve target page (defaults to current)
649 5         11 my $target = $args{page};
650 5         10 my $xs_page;
651 5 100       29 if (defined $target) {
652 2         4 my $ps = pages $self;
653 2 100 66     22 die "PDF::Make::Builder: add_note: page index out of range"
654             unless $target >= 0 && $target < scalar @$ps;
655 1         4 $xs_page = $ps->[$target]->xs_page;
656             } else {
657 3 50       11 my $cur = page $self or die "PDF::Make::Builder: add_note requires a current page";
658 3         11 $xs_page = $cur->xs_page;
659             }
660              
661 4         80 my $annot_num = $xs_doc->add_text_annot(@$rect, $text, $icon, $open);
662 4 50       29 $xs_page->add_annot($annot_num) if $annot_num;
663 4         25 return $self;
664             }
665              
666             sub add_stamp {
667 7     7 1 46 my ($self, %args) = @_;
668              
669             # Visual stamp mode: draw a coloured box with centred bold label
670 7 100       162 if (exists $args{text}) {
671 3         8 my $text = $args{text};
672 3   50     14 my $x = $args{x} // 72;
673 3   50     12 my $w = $args{w} // 200;
674 3   50     12 my $h = $args{h} // 50;
675 3   33     18 my $bg = $args{bg_colour} // $args{fill_colour} // '#e5e7eb';
      50        
676 3   50     11 my $colour = $args{colour} // '#111827';
677 3   50     11 my $size = $args{size} // 20;
678 3   50     10 my $border = $args{border} // 0;
679 3   33     12 my $border_col = $args{border_colour} // $colour;
680              
681 3         7 my $cur = page $self;
682 3 100       19 die "PDF::Make::Builder: add_stamp requires a current page" unless $cur;
683              
684 2         4 my $y;
685 2 100       6 if (defined $args{y}) {
686 1         3 $y = $args{y};
687             } else {
688 1         6 $y = $cur->cursor_y - $h;
689 1         4 $cur->advance_y($h + 6);
690             }
691              
692 2 50       12 $self->add_box(
693             fill_colour => $bg,
694             x => $x,
695             y => $y,
696             w => $w,
697             h => $h,
698             ($border ? (border_colour => $border_col, border_width => $border) : ()),
699             );
700 2         19 $self->add_text(
701             text => $text,
702             x => $x,
703             y => $y + ($h / 2) + ($size * 0.66),
704             w => $w,
705             align => 'center',
706             font => { size => $size, colour => $colour, bold => 1 },
707             );
708 2         16 return $self;
709             }
710              
711             # Annotation stamp mode
712 4         12 my $xs_doc = doc $self;
713 4   100     24 my $rect = $args{rect} // die "PDF::Make::Builder: add_stamp requires rect or text";
714 3   50     10 my $type = $args{type} // 'Draft';
715 3         60 $xs_doc->add_stamp(@$rect, $type);
716 3         19 return $self;
717             }
718              
719             # ── Bates Numbering ──────────────────────────────────────
720              
721             sub add_bates {
722 1     1 1 12 my ($self, %args) = @_;
723 1         33 my $stamp = PDF::Make::Stamp->bates(%args);
724 1         3 my $xs_doc = doc $self;
725 42     42   311 no warnings 'uninitialized';
  42         63  
  42         176540  
726 1         49 $xs_doc->apply_stamp($stamp);
727 1         7 return $self;
728             }
729              
730             # ── Custom Metadata ──────────────────────────────────────
731              
732             sub set_meta {
733 2     2 1 455 my ($self, $key, $value) = @_;
734 2         20 (doc $self)->set_meta($key, $value);
735 2         8 return $self;
736             }
737              
738             sub get_meta {
739 2     2 1 6 my ($self, $key) = @_;
740 2         22 return (doc $self)->get_meta($key);
741             }
742              
743             # ── Page Info ────────────────────────────────────────────
744              
745             sub page_count {
746 22     22 1 1195 my ($self) = @_;
747 22         30 return scalar @{pages $self};
  22         117  
748             }
749              
750             # ── Output ───────────────────────────────────────────────
751              
752             sub to_bytes {
753 1     1 1 5 my ($self) = @_;
754              
755             # Finalize all pages (same as save but return bytes)
756 1         2 my $all_pages = pages $self;
757 1         2 my $offset = page_offset $self;
758 1         2 my $rewrite = _apply_redactions_pending $self;
759 1         3 for my $bp (@$all_pages) {
760 1 50       4 next if $bp->imported;
761 1         3 my $hdr = $bp->header;
762 1         15 my $ftr = $bp->footer;
763 1         4 my $pnum = $bp->num + $offset;
764 1 50       3 $hdr->render($self, $bp, $pnum) if $hdr;
765 1 50       3 $ftr->render($self, $bp, $pnum) if $ftr;
766 1 50 33     7 my $bytes = ($rewrite && @{$bp->redactions})
767             ? $self->_rewrite_redacted_canvas_bytes($bp)
768             : $bp->canvas->to_bytes;
769 1         6 $bp->xs_page->set_content($bytes);
770             }
771              
772 1 50       2 if (_sanitize_pending $self) {
773 0         0 PDF::Make::Redaction->sanitize(doc $self);
774             }
775              
776             # Finalize form
777             {
778 1         2 my $xs_doc = doc $self;
  1         1  
779 1         2 my $form = eval { PDF::Make::FormPtr::get($xs_doc) };
  1         5  
780 1 50       3 if ($form) {
781 0         0 PDF::Make::FormPtr::finalize($form);
782             }
783             }
784              
785 1         137 return (doc $self)->to_bytes;
786             }
787              
788             # ── Attachments ───────────────────────────────────────────
789              
790             sub attach {
791 1     1 1 10 my ($self, %args) = @_;
792 1         2 my $xs_doc = doc $self;
793 1         32 PDF::Make::Attachment->attach($xs_doc, %args);
794 1         19 return $self;
795             }
796              
797             # ── Watermarks ────────────────────────────────────────────
798              
799             sub add_watermark {
800 1     1 1 7 my ($self, %args) = @_;
801 1   50     4 my $text = delete $args{text} // die "PDF::Make::Builder: add_watermark requires text";
802 1         36 my $wm = PDF::Make::Watermark->text($text, %args);
803 1         3 my $wms = _watermarks $self;
804 1         2 push @$wms, $wm;
805 1         2 _watermarks $self, $wms;
806 1         6 return $self;
807             }
808              
809             # ── Encryption ────────────────────────────────────────────
810              
811             sub encrypt {
812 6     6 1 37 my ($self, %args) = @_;
813 6         17 _encrypt_args $self, \%args;
814 6         12 return $self;
815             }
816              
817             # ── Layers/OCG ────────────────────────────────────────────
818              
819             sub add_layer {
820 2     2 1 412 my ($self, $name, %args) = @_;
821 2         5 my $xs_doc = doc $self;
822 2         21 my $layer = PDF::Make::Layer->create($xs_doc, $name);
823 2 100       70 $layer->visible($args{visible}) if defined $args{visible};
824 2         19 my $num = $layer->write_to_doc($xs_doc);
825 2         5 my $cur = page $self;
826 2 50       8 if ($cur) {
827 2         20 $cur->xs_page->add_ocg($layer->res_name, $num);
828             }
829 2         5 my $layers = _layers $self;
830 2         7 $layers->{$name} = $layer;
831 2         3 _layers $self, $layers;
832 2         10 return $self;
833             }
834              
835             sub begin_layer {
836 1     1 1 3 my ($self, $name) = @_;
837 1         2 my $layers = _layers $self;
838 1   50     4 my $layer = $layers->{$name} // die "PDF::Make::Builder: unknown layer '$name'";
839 1         3 my $cur = page $self;
840 1 50       3 die "PDF::Make::Builder: no current page" unless $cur;
841 1         29 $cur->canvas->begin_layer($layer->res_name);
842 1         23 return $self;
843             }
844              
845             sub end_layer {
846 1     1 1 2 my ($self) = @_;
847 1         1 my $cur = page $self;
848 1 50       3 die "PDF::Make::Builder: no current page" unless $cur;
849 1         9 $cur->canvas->end_layer;
850 1         6 return $self;
851             }
852              
853             # ── Redaction ─────────────────────────────────────────────
854              
855             sub mark_redaction {
856 6     6 1 40 my ($self, %args) = @_;
857 6   50     20 my $page_index = delete $args{page} // 0;
858 6         13 my $ps = pages $self;
859 6 50 33     31 die "PDF::Make::Builder: page index out of range"
860             unless $page_index >= 0 && $page_index < scalar @$ps;
861              
862 6         10 my $bp = $ps->[$page_index];
863              
864             # Register the /Redact annotation for downstream tools.
865 6         143 PDF::Make::Redaction->mark($bp->xs_page, %args);
866              
867 6 50       15 my $rect = $args{rect} or return $self;
868 6         15 my ($x0, $y0, $x1, $y1) = @$rect;
869 6         12 my ($w, $h) = ($x1 - $x0, $y1 - $y0);
870 6 50 33     23 return $self if $w <= 0 || $h <= 0;
871              
872 6   33     41 my $colour = $args{overlay_colour} // $args{overlay_color} // '#000';
      50        
873 6         6 my $text = $args{overlay_text};
874 6   50     22 my $size = $args{overlay_font_size} // 10;
875              
876             # Remember the redaction so save-time can rewrite the content stream
877             # to actually remove text that falls inside the rect, then repaint
878             # the overlay on top of the filtered stream.
879 6         12 my $list = $bp->redactions;
880 6         34 push @$list, {
881             rect => [$x0, $y0, $x1, $y1],
882             overlay_text => $text,
883             overlay_size => $size,
884             overlay_fill => $colour,
885             };
886 6         16 $bp->redactions($list);
887              
888             # Eagerly paint the opaque cover so that even a user who never calls
889             # apply_redactions sees the sensitive area visually hidden.
890 6         17 my $font = $self->font;
891 6         19 my ($r, $g, $b) = $font->hex_to_rgb($colour);
892 6         18 my $canvas = $bp->canvas;
893              
894 6         115 $canvas->q->rg($r, $g, $b)->re($x0, $y0, $w, $h)->f->Q;
895              
896 6 100 66     17 if (defined $text && length $text) {
897 4         10 my $res = $font->ensure_loaded($bp->xs_page, 'normal');
898 4         9 my ($tr, $tg, $tb) = $font->hex_to_rgb('#fff');
899 4   50     7 my $tw = $font->measure_text($text) * ($size / ($font->size || 9));
900 4         6 my $tx = $x0 + ($w - $tw) / 2;
901 4 50       8 $tx = $x0 + 4 if $tw > $w - 4;
902 4         6 my $ty = $y0 + ($h - $size) / 2 + 1;
903 4         39 $canvas->q
904             ->BT
905             ->rg($tr, $tg, $tb)
906             ->Tf($res, $size)
907             ->Tm(1, 0, 0, 1, $tx, $ty)
908             ->Tj($text)
909             ->ET
910             ->Q;
911             }
912              
913 6         29 return $self;
914             }
915              
916             sub apply_redactions {
917 2     2 1 1048 my ($self) = @_;
918 2         6 _apply_redactions_pending $self, 1;
919 2         8 return $self;
920             }
921              
922             sub sanitize {
923 2     2 1 4 my ($self) = @_;
924 2         5 _sanitize_pending $self, 1;
925 2         3 return $self;
926             }
927              
928             # Internal: filter the canvas bytes for one page through the redaction
929             # rewriter and re-paint overlay text. Called from save() / to_bytes()
930             # when $builder->_apply_redactions_pending is set.
931             sub _rewrite_redacted_canvas_bytes {
932 1     1   2 my ($self, $bp) = @_;
933 1         3 my $reds = $bp->redactions;
934 1 50 33     5 return $bp->canvas->to_bytes unless $reds && @$reds;
935              
936 1         5 my $raw_bytes = $bp->canvas->to_bytes;
937 1         1 my @rects = map { $_->{rect} } @$reds;
  2         5  
938 1         39 my $filtered = PDF::Make::Redaction->rewrite_stream($raw_bytes, \@rects);
939              
940             # Re-paint overlay text for each redaction on a fresh small canvas
941             # so those BT..ET blocks come AFTER the filtered stream and are not
942             # themselves dropped by the filter.
943 1         18 my $overlay_canvas = PDF::Make::Canvas->new;
944 1         3 my $font = $self->font;
945 1         2 for my $r (@$reds) {
946 2         2 my ($x0, $y0, $x1, $y1) = @{$r->{rect}};
  2         6  
947 2         3 my ($w, $h) = ($x1 - $x0, $y1 - $y0);
948 2 50 33     8 next if $w <= 0 || $h <= 0;
949              
950             # Black rect (re-paint since the filter kept the original, but
951             # redundant paints are harmless and guard against any edge case
952             # where the original rect op was adjacent to a dropped block).
953 2   50     24 my ($br, $bg, $bb) = $font->hex_to_rgb($r->{overlay_fill} // '#000');
954 2         15 $overlay_canvas->q->rg($br, $bg, $bb)->re($x0, $y0, $w, $h)->f->Q;
955              
956 2         3 my $text = $r->{overlay_text};
957 2 50 33     7 next unless defined $text && length $text;
958              
959 2   50     4 my $size = $r->{overlay_size} || 10;
960 2         6 my $res = $font->ensure_loaded($bp->xs_page, 'normal');
961 2         4 my ($tr, $tg, $tb) = $font->hex_to_rgb('#fff');
962 2   50     5 my $tw = $font->measure_text($text) * ($size / ($font->size || 9));
963 2         4 my $tx = $x0 + ($w - $tw) / 2;
964 2 50       5 $tx = $x0 + 4 if $tw > $w - 4;
965 2         4 my $ty = $y0 + ($h - $size) / 2 + 1;
966 2         30 $overlay_canvas->q
967             ->BT
968             ->rg($tr, $tg, $tb)
969             ->Tf($res, $size)
970             ->Tm(1, 0, 0, 1, $tx, $ty)
971             ->Tj($text)
972             ->ET
973             ->Q;
974             }
975              
976 1         10 return $filtered . $overlay_canvas->to_bytes;
977             }
978              
979             # ── Color Management ──────────────────────────────────────
980              
981             sub set_color_space {
982 4     4 1 19 my ($self, $type, %args) = @_;
983 4         10 my $cs;
984 4 100       15 if ($type eq 'sRGB') {
    100          
985 2         66 $cs = PDF::Make::Color->srgb;
986             } elsif ($type eq 'separation') {
987             $cs = PDF::Make::Color->separation(
988 1   50     33 $args{name}, $args{c} // 0, $args{m} // 0, $args{y} // 0, $args{k} // 0
      50        
      50        
      50        
989             );
990             } else {
991 1         11 die "PDF::Make::Builder: unknown color space '$type'";
992             }
993 3         8 my $xs_doc = doc $self;
994 3         43 $cs->write_to_doc($xs_doc);
995 3         25 return $self;
996             }
997              
998             # ── Tagged PDF / Accessibility ────────────────────────────
999              
1000             sub enable_tagging {
1001 1     1 1 6 my ($self) = @_;
1002 1         2 my $xs_doc = doc $self;
1003 1         18 my $tree = PDF::Make::Structure->create_tree($xs_doc);
1004 1         3 _struct_tree $self, $tree;
1005 1         1 _tagging $self, 1;
1006 1         5 return $self;
1007             }
1008              
1009             # ── Forms ─────────────────────────────────────────────────
1010              
1011             my %_field_class = (
1012             text => 'PDF::Make::Builder::Form::Field::Text',
1013             checkbox => 'PDF::Make::Builder::Form::Field::Checkbox',
1014             radio => 'PDF::Make::Builder::Form::Field::Radio',
1015             combo => 'PDF::Make::Builder::Form::Field::Combo',
1016             dropdown => 'PDF::Make::Builder::Form::Field::Combo',
1017             listbox => 'PDF::Make::Builder::Form::Field::Listbox',
1018             list => 'PDF::Make::Builder::Form::Field::Listbox',
1019             button => 'PDF::Make::Builder::Form::Field::Button',
1020             );
1021              
1022             sub add_field {
1023 28     28 1 2596 my ($self, %args) = @_;
1024              
1025 28   100     90 my $type = delete $args{type} // die "PDF::Make::Builder: add_field requires type";
1026 27   100     68 my $name = delete $args{name} // die "PDF::Make::Builder: add_field requires name";
1027              
1028 26   100     83 my $class = $_field_class{$type}
1029             // die "PDF::Make::Builder: unknown field type '$type'";
1030              
1031             # Default to structured mode. Enter raw mode for explicit coordinates
1032             # or when requested directly.
1033 24         41 my $raw_mode = delete $args{raw_mode};
1034 24 50       65 if (!defined $raw_mode) {
1035 24 100 66     107 $raw_mode = (exists $args{rect} || exists $args{x} || exists $args{y}) ? 1 : 0;
1036             }
1037              
1038 24 100       48 if (!$raw_mode) {
1039 15 50 33     29 if (exists $args{default} && !exists $args{default_value}) {
1040 0         0 $args{default_value} = delete $args{default};
1041             }
1042 15         232 my $field = $class->new(field_name => $name, %args);
1043 15         70 $field->add($self);
1044 15         128 return $self;
1045             }
1046              
1047             # Raw mode (direct widget placement)
1048 9         20 my $default = delete $args{default};
1049 9 100       242 $default = delete $args{default_value} unless defined $default;
1050              
1051 9         24 my $cur = page $self;
1052 9 50       24 die "PDF::Make::Builder: no current page" unless $cur;
1053              
1054 9         16 my ($x, $y, $w, $h);
1055 9 100       23 if ($args{rect}) {
1056 8         13 ($x, $y, $w, $h) = @{delete $args{rect}};
  8         23  
1057             } else {
1058             # Cursor-relative placement like add_text
1059 1   33     4 $x = delete $args{x} // $cur->content_x;
1060 1   33     7 $w = delete $args{w} // $cur->width;
1061 1   50     7 $h = delete $args{h} // 22;
1062 1 50       7 if (defined $args{y}) {
1063 0         0 $y = delete $args{y};
1064             } else {
1065 1         3 $y = $cur->cursor_y - $h;
1066             }
1067             # Advance cursor past the field
1068 1         4 $cur->advance_y($h + 4);
1069             }
1070 9 50 33     96 die "PDF::Make::Builder: field requires coordinates"
      33        
      33        
1071             unless defined $x && defined $y && defined $w && defined $h;
1072              
1073 9         50 my %fargs = (
1074             field_name => $name,
1075             x => $x,
1076             y => $y,
1077             w => $w,
1078             h => $h,
1079             raw_mode => 1,
1080             );
1081              
1082 9 100       24 $fargs{default_value} = $default if defined $default;
1083              
1084 9 50       27 $fargs{readonly} = $args{readonly} ? 1 : 0 if exists $args{readonly};
    100          
1085 9 50       29 $fargs{required} = $args{required} ? 1 : 0 if exists $args{required};
    100          
1086 9 100       23 $fargs{da} = $args{da} if exists $args{da};
1087              
1088             # Field-specific passthrough
1089 9 100       42 $fargs{options} = $args{options} if exists $args{options};
1090 9 100       22 $fargs{caption} = $args{caption} if exists $args{caption};
1091 9 50       26 $fargs{on_value} = $args{on_value} if exists $args{on_value};
1092              
1093 9         202 my $field = $class->new(%fargs);
1094 9         69 $field->add($self);
1095              
1096 9         97 return $self;
1097             }
1098              
1099             sub flatten_form {
1100 2     2 1 8 my ($self) = @_;
1101 2         4 _flatten_pending $self, 1;
1102 2         17 return $self;
1103             }
1104              
1105             # ── Digital Signatures ────────────────────────────────────
1106              
1107             sub sign {
1108 1     1 1 6 my ($self, %args) = @_;
1109 1         4 _sign_args $self, \%args;
1110 1         4 return $self;
1111             }
1112              
1113             # ── TOC ────────────────────────────────────────────────────
1114              
1115             sub add_toc {
1116 5     5 1 61 my ($self, %args) = @_;
1117 5         8 my $cfg = configure $self;
1118 5   50     24 my $toc_cfg = $cfg->{toc} // {};
1119 5         12 my $cur = page $self;
1120 5 50       18 my $default_toc_page = $cur ? ($cur->num - 1) : 0;
1121 5         16 %args = (%$toc_cfg, %args);
1122 5 100       17 $args{page_index} = $default_toc_page unless exists $args{page_index};
1123 5         58 toc $self, PDF::Make::Builder::TOC->new(%args);
1124 5         14 return $self;
1125             }
1126              
1127             # ── Save ───────────────────────────────────────────────────
1128              
1129             sub save {
1130 114     114 1 13059 my ($self) = @_;
1131              
1132             # Render TOC if present
1133 114         191 my $cur = page $self;
1134 114 50       341 if ($cur) {
1135 114         184 my $t = toc $self;
1136 114 100 66     285 if ($t && @{$t->entries}) {
  5         20  
1137 5         20 $t->render($self);
1138             }
1139             }
1140              
1141             # Render headers/footers onto each page's canvas, then finalize
1142 114         185 my $all_pages = pages $self;
1143 114         158 my $offset = page_offset $self;
1144 114         177 my $rewrite = _apply_redactions_pending $self;
1145 114         207 for my $bp (@$all_pages) {
1146 158 100       537 if ($bp->imported) {
1147             # Imported pages keep their original content, but any overlay
1148             # drawing issued against the Builder's canvas (e.g. a box
1149             # around extracted text) needs to be appended so it renders
1150             # on top of the source graphics.
1151 34         92 my $overlay = $bp->canvas->to_bytes;
1152 34 50 33     87 if (defined $overlay && length $overlay) {
1153 0         0 $bp->xs_page->append_content($overlay);
1154             }
1155 34         53 next;
1156             }
1157 124         269 my $hdr = $bp->header;
1158 124         240 my $ftr = $bp->footer;
1159 124         264 my $pnum = $bp->num + $offset;
1160              
1161 124 100       238 if ($hdr) {
1162 17         66 $hdr->render($self, $bp, $pnum);
1163             }
1164 124 100       259 if ($ftr) {
1165 12         40 $ftr->render($self, $bp, $pnum);
1166             }
1167              
1168             # Finalize page content; filter through the redaction rewriter
1169             # when the user has called apply_redactions.
1170 124 100 100     1103 my $bytes = ($rewrite && @{$bp->redactions})
1171             ? $self->_rewrite_redacted_canvas_bytes($bp)
1172             : $bp->canvas->to_bytes;
1173 124         1062 $bp->xs_page->set_content($bytes);
1174             }
1175              
1176 114 100       447 if (_sanitize_pending $self) {
1177 1         8 PDF::Make::Redaction->sanitize(doc $self);
1178             }
1179              
1180             # Apply deferred watermarks after all page content is set
1181             {
1182 114         147 my $wms = _watermarks $self;
  114         171  
1183 114 100 66     438 if ($wms && @$wms) {
1184 1         2 my $xs_doc = doc $self;
1185 1         2 for my $wm (@$wms) {
1186 1         41 $xs_doc->add_watermark($wm);
1187             }
1188             }
1189             }
1190              
1191             # Flatten form fields if requested (after set_content so canvas is committed)
1192 114 100       320 if (_flatten_pending $self) {
1193 2         3 my $xs_doc = doc $self;
1194 2         4 my $form = eval { PDF::Make::FormPtr::get($xs_doc) };
  2         10  
1195 2 50       64 $form->flatten if $form;
1196             }
1197              
1198             # Finalize form if any fields were added
1199             {
1200 114         141 my $xs_doc = doc $self;
  114         173  
1201 114         162 my $form = eval { PDF::Make::FormPtr::get($xs_doc) };
  114         1192  
1202 114 100       269 if ($form) {
1203 6         442 PDF::Make::FormPtr::finalize($form);
1204             }
1205             }
1206              
1207             # Apply encryption if configured. The actual /Encrypt dict is built,
1208             # and per-object encryption applied, inside pdfmake_doc_write.
1209 114         193 my $enc = _encrypt_args $self;
1210 114 100       214 if ($enc) {
1211 5   50     13 my $algo = $enc->{algorithm} // 'AES-256';
1212 5   66     140 my $user = $enc->{user_password} // $enc->{password} // '';
      50        
1213 5   66     15 my $owner = $enc->{owner_password} // $user;
1214 5   50     15 my $perms = $enc->{permissions} // 0xFFFFFFFC;
1215 5         7 my $xs_doc = doc $self;
1216 5         22 $xs_doc->set_encryption($algo, $user, $owner, $perms);
1217             }
1218              
1219             # Apply digital signature if configured. pdfmake_doc_sign returns
1220             # the signed PDF bytes (original doc + /Sig dict + PKCS#7 /Contents),
1221             # so when signing is active we write the returned bytes, not the
1222             # unsigned doc->to_bytes() output.
1223 114         161 my $sig = _sign_args $self;
1224 114         138 my $signed_bytes;
1225 114 50 66     252 if ($sig && $sig->{pkcs12} && -f $sig->{pkcs12}) {
      33        
1226             my $identity = PDF::Make::Signature->load_identity(
1227             file => $sig->{pkcs12},
1228 0   0     0 password => $sig->{password} // '',
1229             );
1230 0 0 0     0 die "PDF::Make::Builder: sign: identity has no usable signing key"
1231             unless $identity && $identity->can_sign;
1232              
1233 0         0 my $xs_doc = doc $self;
1234             $signed_bytes = PDF::Make::Signature::_sign_document($xs_doc,
1235             identity => $identity,
1236             reason => $sig->{reason},
1237             location => $sig->{location},
1238             contact => $sig->{contact},
1239             name => $sig->{name},
1240             hash => $sig->{hash},
1241             timestamp_url => $sig->{timestamp_url},
1242             tsa_timeout => $sig->{tsa_timeout},
1243             visible => $sig->{visible},
1244             page => $sig->{page},
1245             rect => $sig->{rect},
1246             appearance => $sig->{appearance},
1247 0         0 );
1248             }
1249              
1250             # Write to file
1251 114         149 my $xs_doc = doc $self;
1252 114         198 my $fname = file_name $self;
1253 114 50       663 $fname .= '.pdf' unless $fname =~ /\.pdf$/i;
1254 114         4407 my $dir = dirname($fname);
1255 114 50 33     699 if (defined $dir && length $dir && $dir ne '.') {
      33        
1256 114 50       2481 make_path($dir) unless -d $dir;
1257             }
1258 114 50       269 if (defined $signed_bytes) {
1259 0 0       0 open my $fh, '>:raw', $fname or die "PDF::Make::Builder: cannot write '$fname': $!";
1260 0         0 print $fh $signed_bytes;
1261 0         0 close $fh;
1262             } else {
1263 114         621027 $xs_doc->to_file($fname);
1264             }
1265              
1266 114         732 return $self;
1267             }
1268              
1269             # ── Onsave callback management ─────────────────────────────
1270              
1271             sub onsave {
1272 0     0 1 0 my ($self, $key, $method, %args) = @_;
1273 0         0 my $cbs = onsave_cbs $self;
1274 0         0 push @$cbs, [$key, $method, %args];
1275 0         0 onsave_cbs $self, $cbs;
1276 0         0 return $self;
1277             }
1278              
1279             # ── Text extraction (phase 13) ────────────────────────────
1280              
1281             sub extract_text {
1282 1     1 1 4 my ($self, $file, $page_index) = @_;
1283 1   50     5 $page_index //= 0;
1284 1         60 my $parser = PDF::Make::Parser->from_file($file);
1285 1         14 return PDF::Make::Extract->extract($parser, $page_index);
1286             }
1287              
1288             sub extract_structured {
1289 54     54 0 59327 my ($self, $file, %args) = @_;
1290 54   100     231 my $page_index = $args{page} // 0;
1291             my $include_invisible = exists $args{invisible}
1292 54 100       140 ? ($args{invisible} ? 1 : 0)
    100          
1293             : 1;
1294 54         6128 my $parser = PDF::Make::Parser->from_file($file, repair => 1);
1295 54         2840 $parser->parse;
1296 54         114341 my $reader = PDF::Make::Reader->new($parser);
1297 54 100 100     368 if ($reader->is_encrypted && !$reader->is_authenticated) {
1298 3   50     9 my $pw = $args{password} // '';
1299 3         129144 my $rc = $reader->set_password($pw);
1300 3 50       22 die "PDF::Make::Builder: extract_structured: authentication failed for '$file'"
1301             if $rc < 0;
1302             }
1303 54         33841 my $raw = PDF::Make::Extract->_extract_structured(
1304             $reader, $page_index, $include_invisible);
1305 54         1250 return PDF::Make::Extract::Result->new(data => $raw);
1306             }
1307              
1308             sub extract_annotations {
1309 2     2 0 8107 my ($self, $file, %args) = @_;
1310 2         210 my $parser = PDF::Make::Parser->from_file($file, repair => 1);
1311 2         121 $parser->parse;
1312 2         255 my $reader = PDF::Make::Reader->new($parser);
1313 2 50 33     14 if ($reader->is_encrypted && !$reader->is_authenticated) {
1314 0   0     0 my $pw = $args{password} // '';
1315 0         0 my $rc = $reader->set_password($pw);
1316 0 0       0 die "PDF::Make::Builder: extract_annotations: authentication failed for '$file'"
1317             if $rc < 0;
1318             }
1319 2         161 my $list = PDF::Make::Extract->_extract_annotations($reader);
1320 2 50       55 return wantarray ? @$list : $list;
1321             }
1322              
1323             sub detect_tables {
1324 2     2 0 6751 my ($self, $file, %args) = @_;
1325 2   50     11 my $page_index = $args{page} // 0;
1326 2         113 my $parser = PDF::Make::Parser->from_file($file, repair => 1);
1327 2         69 $parser->parse;
1328 2         122 my $reader = PDF::Make::Reader->new($parser);
1329 2 50 33     12 if ($reader->is_encrypted && !$reader->is_authenticated) {
1330 0   0     0 my $pw = $args{password} // '';
1331 0         0 my $rc = $reader->set_password($pw);
1332 0 0       0 die "PDF::Make::Builder: detect_tables: authentication failed for '$file'"
1333             if $rc < 0;
1334             }
1335 2         495 my $list = PDF::Make::Extract->_detect_tables($reader, $page_index);
1336 2 50       29 return wantarray ? @$list : $list;
1337             }
1338              
1339             # ── Open existing PDF ────────────────────────────────────
1340              
1341             sub open_existing {
1342 11     11 1 6050 my ($class, $file, %args) = @_;
1343              
1344 11         26 my $password = delete $args{password};
1345 11   66     98 my $out_file = delete $args{file_name} // $file;
1346              
1347 11 50       225 die "PDF::Make::Builder: open_existing: file '$file' not found" unless -f $file;
1348              
1349 11         132 my $self = $class->new(file_name => $out_file, %args);
1350              
1351             # Round-trip the source document: content + resources + dimensions.
1352 11         48 $self->append_pdf($file, password => $password);
1353              
1354 10         50 return $self;
1355             }
1356              
1357             # ── Append pages from another PDF ─────────────────────────
1358              
1359             sub append_pdf {
1360 26     26 0 96 my ($self, $file, %args) = @_;
1361              
1362 26 100       370 die "PDF::Make::Builder: append_pdf: file '$file' not found" unless -f $file;
1363              
1364 25         1788 my $parser = PDF::Make::Parser->from_file($file, repair => 1);
1365 25         1012 $parser->parse;
1366 25         398161 my $reader = PDF::Make::Reader->new($parser);
1367              
1368 25 100 100     230 if ($reader->is_encrypted && !$reader->is_authenticated) {
1369 5   50     26 my $pw = $args{password} // '';
1370 5         350092 my $rc = $reader->set_password($pw);
1371 5 100       110 die "PDF::Make::Builder: append_pdf: authentication failed for '$file'"
1372             if $rc < 0;
1373             }
1374              
1375 24         51 my $xs_doc = doc $self;
1376 24         262 my $importer = PDF::Make::Import->new($reader, $xs_doc);
1377              
1378 24         76 my $count = $reader->page_count;
1379 24 100       152 my @indices = $args{pages} ? @{$args{pages}} : (0 .. $count - 1);
  3         10  
1380              
1381 24         54 for my $idx (@indices) {
1382 45 100 66     191 die "PDF::Make::Builder: append_pdf: page index $idx out of range (file has $count pages)"
1383             unless $idx >= 0 && $idx < $count;
1384              
1385 44         158 my $before = $xs_doc->page_count;
1386 44         108103 my $ok = $importer->import_page($idx);
1387 44 50       128 unless ($ok) {
1388 0         0 warn "PDF::Make::Builder: append_pdf: failed to import page $idx from '$file'";
1389 0         0 next;
1390             }
1391 44         118 my $after = $xs_doc->page_count;
1392              
1393 44         112 for my $pi ($before .. $after - 1) {
1394 44         130 my $xs_page = $xs_doc->get_page($pi);
1395 44         71 my $pages = pages $self;
1396 44         65 my $num = scalar(@$pages) + 1;
1397 44         1241 my $bp = PDF::Make::Builder::Page->new(
1398             page_size => 'custom',
1399             background => '#fff',
1400             columns => 1,
1401             padding => 20,
1402             num => $num,
1403             w => $xs_page->width,
1404             h => $xs_page->height,
1405             canvas => PDF::Make::Canvas->new,
1406             xs_page => $xs_page,
1407             imported => 1,
1408             );
1409 44         87 push @$pages, $bp;
1410 44         55 pages $self, $pages;
1411 44         101 page $self, $bp;
1412             }
1413             }
1414              
1415 23         277 return $self;
1416             }
1417              
1418             sub merge {
1419 1     1 0 227 my ($class, $out_file, @inputs) = @_;
1420 1 50       5 die "PDF::Make::Builder::merge: no input files" unless @inputs;
1421              
1422 1         9 my $b = $class->new(file_name => $out_file);
1423 1         3 for my $f (@inputs) {
1424 3         8 $b->append_pdf($f);
1425             }
1426 1         5 $b->save;
1427 1         6 return $b;
1428             }
1429              
1430             # ── Page editing (phase 15) ───────────────────────────────
1431              
1432             sub remove_page {
1433 3     3 1 311 my ($self, $index) = @_;
1434 3         5 my $ps = pages $self;
1435 3 50 33     17 die "PDF::Make::Builder: page index out of range" unless $index >= 0 && $index < scalar @$ps;
1436 3         8 splice @$ps, $index, 1;
1437             # Re-number remaining pages
1438 3         23 for my $i (0 .. $#$ps) {
1439 3         13 $ps->[$i]->num($i + 1);
1440             }
1441 3         4 pages $self, $ps;
1442             # Switch to last page if current was removed
1443 3 100       8 if (scalar @$ps) {
1444 2         11 page $self, $ps->[-1];
1445             } else {
1446 1         9 page $self, undef;
1447             }
1448 3         8 return $self;
1449             }
1450              
1451             sub move_page {
1452 1     1 1 3 my ($self, $from, $to) = @_;
1453 1         2 my $ps = pages $self;
1454 1 50 33     8 die "PDF::Make::Builder: from index out of range" unless $from >= 0 && $from < scalar @$ps;
1455 1 50 33     3 die "PDF::Make::Builder: to index out of range" unless $to >= 0 && $to < scalar @$ps;
1456 1         2 my $pg = splice @$ps, $from, 1;
1457 1         2 splice @$ps, $to, 0, $pg;
1458 1         3 for my $i (0 .. $#$ps) {
1459 3         8 $ps->[$i]->num($i + 1);
1460             }
1461 1         2 pages $self, $ps;
1462 1         4 return $self;
1463             }
1464              
1465             sub duplicate_page {
1466 1     1 1 3 my ($self, $index) = @_;
1467 1         3 my $ps = pages $self;
1468 1 50 33     6 die "PDF::Make::Builder: page index out of range" unless $index >= 0 && $index < scalar @$ps;
1469              
1470 1         2 my $src = $ps->[$index];
1471              
1472             # Finalize source page content
1473 1         31 $src->xs_page->set_content($src->canvas->to_bytes);
1474              
1475             # Create a new page with same dimensions
1476 1         2 my $xs_doc = doc $self;
1477 1         53 my $xs_page = $xs_doc->add_page($src->w, $src->h);
1478 1         25 my $canvas = PDF::Make::Canvas->new;
1479              
1480 1         18 my $bp = PDF::Make::Builder::Page->new(
1481             page_size => $src->page_size,
1482             background => $src->background,
1483             columns => $src->columns,
1484             padding => $src->padding,
1485             num => scalar(@$ps) + 1,
1486             w => $src->w,
1487             h => $src->h,
1488             canvas => $canvas,
1489             xs_page => $xs_page,
1490             header => $src->header,
1491             footer => $src->footer,
1492             );
1493              
1494 1         2 push @$ps, $bp;
1495 1         1 pages $self, $ps;
1496 1         2 page $self, $bp;
1497              
1498 1         2 return $self;
1499             }
1500              
1501             sub rotate_page {
1502 4     4 1 498 my ($self, $index, $degrees) = @_;
1503 4         7 my $ps = pages $self;
1504 4 50 33     22 die "PDF::Make::Builder: page index out of range" unless $index >= 0 && $index < scalar @$ps;
1505             die "PDF::Make::Builder: rotation must be 0, 90, 180, or 270"
1506 4 50       7 unless grep { $degrees == $_ } (0, 90, 180, 270);
  16         25  
1507              
1508             # Swap width/height for 90/270 rotation
1509 4         7 my $pg = $ps->[$index];
1510 4 100 100     14 if ($degrees == 90 || $degrees == 270) {
1511 3         13 my ($w, $h) = ($pg->w, $pg->h);
1512 3         9 $pg->w($h);
1513 3         7 $pg->h($w);
1514             }
1515              
1516 4         11 return $self;
1517             }
1518              
1519             # ── Load TrueType font (phase 12) ────────────────────────
1520              
1521             sub load_ttf {
1522 0     0 1   my ($self, $path, %args) = @_;
1523 0           my $font_obj = PDF::Make::Font->from_file($path);
1524 0           my $doc = doc $self;
1525 0           my $obj_num = $font_obj->write_to_doc($doc);
1526              
1527 0   0       my $name = $args{name} // 'TT' . $obj_num;
1528 0           my $cur = page $self;
1529 0 0         if ($cur) {
1530 0           $cur->xs_page->add_font($name, $font_obj->base_font);
1531             }
1532              
1533 0           return $self;
1534             }
1535              
1536             1;
1537              
1538             __END__