|
2 | 2 |
|
3 | 3 | # This file is part of Koha. |
4 | 4 | # |
5 | | -# Copyright 2020 Koha Development team |
| 5 | +# Copyright 2020, 2026 Koha Development team |
6 | 6 | # Copyright (C) 2017 Mark Tompsett |
7 | 7 | # |
8 | 8 | # Koha is free software; you can redistribute it and/or modify it |
|
20 | 20 |
|
21 | 21 | use Modern::Perl; |
22 | 22 |
|
| 23 | +BEGIN { |
| 24 | + eval { require PDF::Reuse; PDF::Reuse->VERSION(0.43) }; |
| 25 | + if ($@) { |
| 26 | + require Test::More; |
| 27 | + Test::More::plan( skip_all => 'PDF::Reuse >= 0.43 required (see Bug 41717)' ); |
| 28 | + } |
| 29 | +} |
| 30 | + |
23 | 31 | use Test::NoWarnings; |
24 | | -use Test::More tests => 7; |
| 32 | +use Test::More tests => 11; |
| 33 | +use Test::Warn; |
25 | 34 | use t::lib::TestBuilder; |
26 | 35 | use t::lib::Mocks; |
27 | 36 |
|
28 | 37 | use MARC::Record; |
29 | 38 | use MARC::Field; |
30 | 39 | use Data::Dumper; |
| 40 | +use File::Temp qw( tempfile ); |
31 | 41 |
|
32 | 42 | use C4::Items; |
33 | 43 | use C4::Biblio; |
34 | 44 | use C4::Labels::Layout; |
| 45 | +use C4::Creators::PDF; |
35 | 46 |
|
36 | 47 | use Koha::Database; |
37 | 48 |
|
38 | 49 | use_ok('C4::Labels::Label'); |
39 | 50 |
|
40 | | -my $database = Koha::Database->new(); |
41 | | -my $schema = $database->schema(); |
42 | | -$schema->storage->txn_begin(); |
| 51 | +# Slurp whatever C4::Creators::PDF::End() prints to the currently selected |
| 52 | +# filehandle, return it as a string. Centralises the tempfile/select/End |
| 53 | +# dance so a future End() refactor only needs one edit. Dies if End() |
| 54 | +# dies (after restoring the selected filehandle) so cleanup-only callers |
| 55 | +# cannot silently pass through an End() regression. |
| 56 | +sub capture_pdf_end { |
| 57 | + my ($pdf) = @_; |
| 58 | + my ( $fh, $path ) = tempfile( SUFFIX => '.pdf', UNLINK => 1 ); |
| 59 | + open( my $out, '>', $path ) or die "Cannot open $path for write: $!"; |
| 60 | + my $orig = select $out; |
| 61 | + eval { $pdf->End() }; |
| 62 | + my $end_err = $@; |
| 63 | + select $orig; |
| 64 | + close $out; |
| 65 | + die "pdf End() failed: $end_err" if $end_err; |
| 66 | + open( my $in, '<', $path ) or die "Cannot open $path for read: $!"; |
| 67 | + local $/; |
| 68 | + my $content = <$in>; |
| 69 | + close $in; |
| 70 | + return $content; |
| 71 | +} |
| 72 | + |
| 73 | +my $schema = Koha::Database->new->schema; |
| 74 | +$schema->storage->txn_begin; |
43 | 75 |
|
44 | 76 | my $batch_id; |
45 | 77 | my ( $llx, $lly ) = ( 0, 0 ); |
@@ -152,6 +184,184 @@ is_deeply( |
152 | 184 | is( $barcode_width, '2.104', ); |
153 | 185 | is( $barcode_height, '0.01', ); |
154 | 186 |
|
| 187 | +# ======================================== |
| 188 | +# Subtest: draw_label_text() deep assertions |
| 189 | +# ======================================== |
| 190 | +subtest 'draw_label_text() returns correct text structure' => sub { |
| 191 | + plan tests => 6; |
| 192 | + |
| 193 | + # Local fixture: deterministic two-field format so this subtest does |
| 194 | + # not depend on whatever was last assigned to the top-level $format_string. |
| 195 | + my $local_format = '100a 245a,enumchron copynumber'; |
| 196 | + |
| 197 | + my $label_bib = C4::Labels::Label->new( |
| 198 | + %$label_info, |
| 199 | + format_string => $local_format, |
| 200 | + printing_type => 'BIB', |
| 201 | + justify => 'L', |
| 202 | + ); |
| 203 | + my $text_result = $label_bib->create_label(); |
| 204 | + |
| 205 | + ok( defined $text_result, 'create_label(BIB) returns defined value' ); |
| 206 | + is( ref $text_result, 'ARRAY', 'Return value is an arrayref' ); |
| 207 | + cmp_ok( scalar @$text_result, '>=', 2, 'Two-field format yields at least two text lines' ); |
| 208 | + |
| 209 | + my $first_line = $text_result->[0]; |
| 210 | + my $expected_llx = $label_info->{llx} + $label_info->{left_text_margin}; |
| 211 | + my $expected_first_y = $label_info->{lly} + $label_info->{height} - $label_info->{top_text_margin}; |
| 212 | + |
| 213 | + is( |
| 214 | + $first_line->{text_llx}, $expected_llx, |
| 215 | + 'First line text_llx = llx + left_text_margin (justify L)' |
| 216 | + ); |
| 217 | + is( |
| 218 | + $first_line->{text_lly}, $expected_first_y, |
| 219 | + 'First line text_lly = lly + height - top_text_margin (per _BIB)' |
| 220 | + ); |
| 221 | + cmp_ok( |
| 222 | + $text_result->[1]{text_lly}, '<', $first_line->{text_lly}, |
| 223 | + 'Second line text_lly is less than first (text flows downward)' |
| 224 | + ); |
| 225 | +}; |
| 226 | + |
| 227 | +# ======================================== |
| 228 | +# Subtest: draw_guide_box() |
| 229 | +# ======================================== |
| 230 | +subtest 'draw_guide_box() returns PDF stream when enabled' => sub { |
| 231 | + plan tests => 3; |
| 232 | + |
| 233 | + my $label_with_box = C4::Labels::Label->new( |
| 234 | + %$label_info, |
| 235 | + format_string => $format_string, |
| 236 | + guidebox => 1, |
| 237 | + llx => 10, |
| 238 | + lly => 20, |
| 239 | + width => 100, |
| 240 | + height => 50, |
| 241 | + ); |
| 242 | + my $box_stream = $label_with_box->draw_guide_box(); |
| 243 | + ok( $box_stream, 'draw_guide_box returns truthy when guidebox enabled' ); |
| 244 | + like( $box_stream, qr/10 20 100 50 re/, 'PDF stream contains expected rectangle coordinates' ); |
| 245 | + |
| 246 | + my $label_no_box = C4::Labels::Label->new( |
| 247 | + %$label_info, |
| 248 | + format_string => $format_string, |
| 249 | + guidebox => 0, |
| 250 | + ); |
| 251 | + ok( !$label_no_box->draw_guide_box(), 'draw_guide_box returns falsy when guidebox disabled' ); |
| 252 | +}; |
| 253 | + |
| 254 | +# ======================================== |
| 255 | +# Subtest: barcode() with all supported types |
| 256 | +# ======================================== |
| 257 | +subtest 'barcode() generates all supported barcode types' => sub { |
| 258 | + plan tests => 7; |
| 259 | + |
| 260 | + # Local fixture; do not inherit top-level $format_string state. |
| 261 | + my $local_format = '100a 245a'; |
| 262 | + |
| 263 | + my $pdf = C4::Creators::PDF->new( InitVars => 1 ); |
| 264 | + $pdf->Page(); |
| 265 | + |
| 266 | + # C4::Labels::Label::barcode() converts every PDF::Reuse::Barcode::* |
| 267 | + # failure to a warn (Label.pm:556,573,590,609), so a per-type test that |
| 268 | + # only checks "did it die" cannot distinguish success from a silent |
| 269 | + # barcode-generation failure. We therefore assert no barcode-generation |
| 270 | + # warning fires for each type, which is the actual breakage signal. |
| 271 | + # CODE39MOD10 (modulo-10 / 'siret') needs numeric data; alpha input |
| 272 | + # yields an empty checksum and silently degenerates. |
| 273 | + my @barcode_tests = ( |
| 274 | + { type => 'CODE39', barcode => 'TEST97531', desc => 'CODE39' }, |
| 275 | + { type => 'CODE39MOD', barcode => 'TEST97531', desc => 'CODE39MOD (modulo43 checksum)' }, |
| 276 | + { type => 'CODE39MOD10', barcode => '97531', desc => 'CODE39MOD10 (modulo10 checksum)' }, |
| 277 | + { type => 'COOP2OF5', barcode => '97531', desc => 'COOP2OF5 (numeric)' }, |
| 278 | + { type => 'INDUSTRIAL2OF5', barcode => '97531', desc => 'INDUSTRIAL2OF5 (numeric)' }, |
| 279 | + { type => 'EAN13', barcode => '5901234123457', desc => 'EAN13 (13-digit)' }, |
| 280 | + ); |
| 281 | + |
| 282 | + for my $bc_test (@barcode_tests) { |
| 283 | + my $bc_label = C4::Labels::Label->new( |
| 284 | + %$label_info, |
| 285 | + format_string => $local_format, |
| 286 | + printing_type => 'BAR', |
| 287 | + barcode_type => $bc_test->{type}, |
| 288 | + barcode => $bc_test->{barcode}, |
| 289 | + ); |
| 290 | + warnings_are { $bc_label->create_label() } [], |
| 291 | + "barcode() $bc_test->{desc} emits no barcode-generation warning"; |
| 292 | + } |
| 293 | + |
| 294 | + # Slurp the PDF and assert it contains at least one barcode graphic |
| 295 | + # stroke (Code39 / 2-of-5 / EAN13 all emit rectangle 're' operators), |
| 296 | + # so the per-type checks above can't all pass against an empty document. |
| 297 | + my $content = capture_pdf_end($pdf); |
| 298 | + like( $content, qr/\bre\b/, 'PDF contains barcode rectangle operators' ); |
| 299 | +}; |
| 300 | + |
| 301 | +# ======================================== |
| 302 | +# Subtest: create_label() printing type orchestration |
| 303 | +# ======================================== |
| 304 | +subtest 'create_label() printing type orchestration' => sub { |
| 305 | + plan tests => 5; |
| 306 | + |
| 307 | + my $local_format = '100a 245a'; |
| 308 | + |
| 309 | + my $pdf = C4::Creators::PDF->new( InitVars => 1 ); |
| 310 | + $pdf->Page(); |
| 311 | + |
| 312 | + # BIB - returns label_text (text only) |
| 313 | + my $label_bib = C4::Labels::Label->new( |
| 314 | + %$label_info, |
| 315 | + format_string => $local_format, |
| 316 | + printing_type => 'BIB', |
| 317 | + ); |
| 318 | + my $bib_result = $label_bib->create_label(); |
| 319 | + ok( defined $bib_result, 'BIB printing type returns label text' ); |
| 320 | + |
| 321 | + # BAR - returns undef (barcode only) |
| 322 | + my $label_bar = C4::Labels::Label->new( |
| 323 | + %$label_info, |
| 324 | + format_string => $local_format, |
| 325 | + printing_type => 'BAR', |
| 326 | + barcode_type => 'CODE39', |
| 327 | + barcode => 'ORCHTEST1', |
| 328 | + ); |
| 329 | + my $bar_result = $label_bar->create_label(); |
| 330 | + ok( !defined $bar_result, 'BAR printing type returns undef (barcode only)' ); |
| 331 | + |
| 332 | + # BIBBAR - text-above-barcode geometry (text near top of label) |
| 333 | + my $label_bibbar = C4::Labels::Label->new( |
| 334 | + %$label_info, |
| 335 | + format_string => $local_format, |
| 336 | + printing_type => 'BIBBAR', |
| 337 | + barcode_type => 'CODE39', |
| 338 | + barcode => 'ORCHTEST2', |
| 339 | + ); |
| 340 | + my $bibbar_result = $label_bibbar->create_label(); |
| 341 | + ok( defined $bibbar_result, 'BIBBAR printing type returns label text' ); |
| 342 | + |
| 343 | + # BARBIB - barcode-above-text geometry (text shifted downward) |
| 344 | + my $label_barbib = C4::Labels::Label->new( |
| 345 | + %$label_info, |
| 346 | + format_string => $local_format, |
| 347 | + printing_type => 'BARBIB', |
| 348 | + barcode_type => 'CODE39', |
| 349 | + barcode => 'ORCHTEST3', |
| 350 | + ); |
| 351 | + my $barbib_result = $label_barbib->create_label(); |
| 352 | + ok( defined $barbib_result, 'BARBIB printing type returns label text' ); |
| 353 | + |
| 354 | + # If _BIBBAR and _BARBIB were swapped, both return arrays with defined |
| 355 | + # entries and the four checks above would still pass. Pin the geometric |
| 356 | + # contract: BIBBAR puts text above BARBIB's text. |
| 357 | + cmp_ok( |
| 358 | + $bibbar_result->[0]{text_lly}, '>', $barbib_result->[0]{text_lly}, |
| 359 | + 'BIBBAR places first text line above BARBIB (text_lly higher)' |
| 360 | + ); |
| 361 | + |
| 362 | + capture_pdf_end($pdf); |
| 363 | +}; |
| 364 | + |
155 | 365 | $schema->storage->txn_rollback(); |
156 | 366 |
|
157 | 367 | 1; |
0 commit comments