File Coverage

blib/lib/Games/Sudoku/Pdf.pm
Criterion Covered Total %
statement 292 398 73.3
branch 110 198 55.5
condition 28 54 51.8
subroutine 21 25 84.0
pod 5 5 100.0
total 456 680 67.0


line stmt bran cond sub pod time code
1             package Games::Sudoku::Pdf;
2              
3 2     2   243276 use strict;
  2         4  
  2         76  
4 2     2   17 use warnings;
  2         7  
  2         215  
5              
6             our $VERSION = '0.06';
7              
8             require 5.006;
9              
10 2     2   1756 use PDF::API2 2.000;
  2         584956  
  2         142  
11 2     2   2158 use PDF::Table v0.9.3;
  2         29146  
  2         106  
12 2     2   1331 use Time::Local qw( timelocal );
  2         4371  
  2         12412  
13             $| = 1;
14              
15             ###########################################################
16             my $PDF_API2_IS_OLD = _make_version_comparable($PDF::API2::VERSION) < 2.042 ? 1 : 0;
17             my $PDF_TABLE_IS_OLD = _make_version_comparable($PDF::Table::VERSION) < 1.001 ? 1 : 0;
18             # Both modules had some fairly recent api changes. Switches were tested to work with
19             # PDF::API2 2.000 and 2.045
20             # PDF::Table 0.9.3 and 1.005
21             ###########################################################
22              
23             sub new {
24 1     1 1 190107 my $class = shift;
25              
26 1         16 bless _init({
27             pageFormat => 'A4',
28             puzzlesPerPage => '2x2',
29             pageOrientation => '',
30             appendFile => '',
31             extraRegion => 'n',
32             title => '',
33             author => '',
34             subject => '',
35             keywords => '',
36              
37             quiet => 0,
38             textFont => 'Helvetica-Oblique',
39             textEncoding => undef,
40             @_,
41             }), $class
42             }
43              
44             sub add_puzzle {
45             # add a table object with the puzzle to the pdf
46 26     26 1 1166 my $self = shift;
47 26         156 my %puzzle = @_;
48              
49 26 50       137 my $clues = $puzzle{clues} or return 0;
50 26         61 my $str_len = length($clues);
51 26 50       100 unless ($str_len == 81) {
52 0         0 warn "parameter 'clues' must be a string of 81 characters (has $str_len)";
53 0         0 return 0;
54             }
55 26         972 $clues =~ s/[^1-9]/ /g;
56              
57 26         68 my $jigsaw;
58 26 100       89 if ($puzzle{jigsaw}) {
59 8         20 $str_len = length($puzzle{jigsaw});
60 8 50       33 unless ($str_len == 81) {
61 0         0 warn "parameter 'jigsaw' must be a string of 81 characters (has $str_len)";
62 0         0 return 0;
63             }
64 8         41 $jigsaw = _create_matrix($puzzle{jigsaw});
65             }
66              
67 26   50     225 my $x_region = lc ($puzzle{extraRegion} || $self->{extraRegion} || 'n');
68              
69 26         65 my $pdf = $self->{pdf};
70 26         64 my $pos = $self->{nextPosOnPage}++;
71            
72 26         50 my $page;
73 26 100       85 if ($pos) {
74             # continue on current page
75 24         55 $page = $self->{page};
76              
77             } else {
78             # continue on a new page
79 2         16 $page = $self->{page} = $pdf->page();
80              
81 2         2174 my $pages_in_file;
82 2 50       8 if ($PDF_API2_IS_OLD) {
83 0         0 $page->mediabox(@{$self->{pageSize}});
  0         0  
84 0         0 $pages_in_file = $pdf->pages();
85             } else {
86 2         13 $page->size($self->{pageSize});
87 2         524 $pages_in_file = $pdf->page_count();
88             }
89 2         20 $self->{pagesAdded}++;
90 2 50       9 print "\nadding page ", $self->{pagesAdded}, "\n" unless $self->{quiet};
91              
92 2         10 my $text = $page->text();
93 2         1090 $text->font($self->{font}, 12);
94 2         533 $text->translate($self->{pageWidth} - 12, 24);
95              
96 2 50       929 if ($PDF_TABLE_IS_OLD) {
97 0         0 $text->text_right('page ' . $pages_in_file);
98             } else {
99 2         38 $text->text('page ' . $pages_in_file, align => 'right');
100             }
101             }
102              
103 26 50       849 print "\radding puzzle ", $pos + 1 unless $self->{quiet};
104 26         103 my $card = _create_matrix($clues);
105              
106 26         57 my $area_colors;
107 26 100 100     118 if ($x_region eq 'n' && $jigsaw) {
108 1         8 $area_colors = $self->_get_area_colors($jigsaw, $self->{puzzleCount} + 1);
109             }
110              
111 26         45 my $cell_props = [];
112 26         108 for (my $i = 0; $i < @$card; $i++) {
113 234         405 my $row = $card->[$i];
114 234         495 for (my $j = 0; $j < @$row; $j++) {
115 2106         5876 $cell_props->[$i][$j] = $self->_get_cell_prop($i, $j, $x_region, $jigsaw, $area_colors);
116             }
117             }
118              
119 26         75 my $settings = $self->{tableSettings};
120 26         60 my $factor = $self->{factor};
121 26         63 my $pos_data = $self->{positionData}[$pos];
122 26         89 my ($x, $y) = @$pos_data;
123              
124 26         185 my $pdftable = new PDF::Table;
125              
126 26 50       4242 my ($end_page, $pages_spanned, $table_bot_y) = $pdftable->table(
127             $pdf,
128             $page,
129             $card,
130             x => $x,
131             ($PDF_TABLE_IS_OLD ? 'start_y' : 'y') => $y,
132             cell_props => $cell_props,
133             %$settings
134             );
135              
136 26 50       8344284 if ($pages_spanned > 1) {
137 0         0 warn "\aTable overflow!\n";
138             }
139              
140 26         79 my ($content, $cell_size);
141 26 50       119 if ($PDF_API2_IS_OLD) {
142 0         0 $content = $page->gfx();
143             } else {
144 26         178 $content = $page->graphics();
145             }
146 26 50       8013 if ($PDF_TABLE_IS_OLD) {
147 0         0 $cell_size = $settings->{row_height};
148             } else {
149 26         99 $cell_size = $settings->{min_rh};
150             }
151              
152 26 100       88 if ($jigsaw) {
153 8         48 _draw_jigsaw_lines($content, $x, $y, $jigsaw, $settings->{w}, $cell_size, $factor);
154             } else {
155 18         115 _draw_straight_lines($content, $x, $y, $settings->{w}, $cell_size, $factor);
156             }
157            
158 26 100       897 if ($puzzle{bottomLine}) {
159 16         109 my $text = $end_page->text();
160 16         5699 $text->font($self->{font}, 12 * $factor);
161 16         5973 $text->translate($x, $table_bot_y - 14 * $factor);
162 16         8649 $text->text( $puzzle{bottomLine} );
163             }
164 26 100       8180 if ($self->{nextPosOnPage} == $self->{puzzlesPerPage}) {
165 1         3 $self->{nextPosOnPage} = 0;
166             }
167 26         939 $self->{puzzleCount}++;
168             }
169              
170             sub read_input_file {
171 0     0 1 0 my $self = shift;
172 0 0       0 my $file = shift() or die "The file to read from must be given as first argument to read_input_file().\n";
173 0         0 my %options = @_;
174 0         0 my $puzzle_numbers = $options{slice};
175              
176 0         0 my (@puzzle_numbers, $next_puzzle_number);
177 0 0       0 if ($puzzle_numbers) {
178 0         0 @puzzle_numbers = sort {$a <=> $b} @$puzzle_numbers;
  0         0  
179 0         0 $next_puzzle_number = shift @puzzle_numbers;
180             }
181              
182 0         0 my $puzzles_in_file = 0;
183             my $line_handler = $options{lineHandler} || sub {
184 0     0   0 my $line = shift;
185 0         0 $line =~ /^([0-9. ]{81})(?:[\|:,;\t]([1-9]{81})?)?(?:[\|:,;\t]([nNxXhHpPcCdDaAgG])?)?(?:[\|:,;\t](.+)?)?/ or return 0;
186 0         0 $puzzles_in_file++;
187              
188 0         0 if ($puzzle_numbers) {
189 0         0 $puzzles_in_file == $next_puzzle_number or return 0;
190             }
191              
192             return {
193 0         0 clues => $1,
194             bottomLine => ($4 || sprintf "puzzle # %d", ($next_puzzle_number || $puzzles_in_file)),
195             extraRegion => $3,
196             jigsaw => $2,
197             }
198 0   0     0 };
199              
200 0         0 my $hdl;
201 0 0       0 if (ref($file) eq 'GLOB') {
202 0         0 $hdl = $file;
203             } else {
204 0 0       0 open $hdl, '<', $file or die "$file: $!\n";
205 0         0 $file =~ s|.*\/||;
206             }
207              
208 0         0 my $pc = 0;
209              
210 0         0 while (<$hdl>) {
211 0         0 chomp();
212 0 0       0 my $puzzle = &$line_handler($_) or next;
213 0         0 $self->add_puzzle(%$puzzle);
214 0         0 $pc++;
215 0 0       0 if ($puzzle_numbers) {
216 0 0       0 $next_puzzle_number = shift(@puzzle_numbers) or last;
217             }
218             }
219 0 0       0 if (ref($file) eq 'GLOB') {
220 0         0 CORE::close $hdl;
221 0 0       0 print "\n$pc out of $puzzles_in_file puzzles processed from file handle\n" unless $self->{quiet};
222              
223             } else {
224             # if we got a handle, the user might want to keep it open
225 0 0       0 print "\n$pc out of $puzzles_in_file puzzles processed from file $file\n" unless $self->{quiet};
226             }
227             }
228              
229             sub page_break {
230 0     0 1 0 my $self = shift;
231 0         0 $self->{nextPosOnPage} = 0;
232             }
233              
234             sub close {
235 0     0 1 0 my $self = shift;
236              
237 0 0       0 printf "\n%d pages with %d sudoku added.\n", $self->{pagesAdded}, $self->{puzzleCount} unless $self->{quiet};
238              
239 0 0 0     0 my $file = (shift() || $self->{appendFile}) or die "No filename for pdf output provided during ->close().\n";
240              
241 0 0       0 if ($PDF_API2_IS_OLD) {
242 0         0 $self->{pdf}->info(ModDate => 'D:' . _format_time_w_zone_offset());
243 0         0 $self->{pdf}->saveas($file);
244 0         0 $self->{pdf}->release();
245             } else {
246 0         0 $self->{pdf}->modified('D:' . _format_time_w_zone_offset());
247 0         0 $self->{pdf}->save($file);
248 0         0 $self->{pdf}->close();
249             }
250 0         0 $self->{pdf} = undef;
251              
252 0 0       0 print "$file written\n" unless $self->{quiet};
253             }
254              
255             #########################################################################
256              
257             sub _init {
258 1     1   1 my $params = shift;
259              
260 1         3 $params->{creator} = __PACKAGE__ . ' v. ' . $VERSION;
261              
262 1         2 my $pdf;
263 1 50       3 if ($params->{appendFile}) {
264 0         0 $pdf = PDF::API2->open($params->{appendFile});
265             } else {
266 1         8 $pdf = PDF::API2->new();
267             }
268              
269 1 50       3048 if ($PDF_API2_IS_OLD) {
270 0         0 $params->{font} = $pdf->corefont($params->{textFont}, -encode => $params->{textEncoding});
271 0         0 my %info_hash = ();
272 0         0 foreach my $kw (qw(creator title author subject keywords)) {
273 0 0       0 $info_hash{ucfirst $kw} = $params->{$kw} if $params->{$kw};
274             }
275 0         0 $info_hash{CreationDate} = 'D:' . _format_time_w_zone_offset();
276 0         0 $pdf->info(%info_hash);
277              
278             } else {
279 1         7 $params->{font} = $pdf->font($params->{textFont}, -encode => $params->{textEncoding});
280 1         41161 $pdf->created('D:' . _format_time_w_zone_offset());
281 1 50       156 $params->{creator} and $pdf->creator($params->{creator});
282 1 50       48 $params->{title} and $pdf->title($params->{title});
283 1 50       9 $params->{author} and $pdf->author($params->{author});
284 1 50       48 $params->{subject} and $pdf->subject($params->{subject});
285 1 50       74 $params->{keywords} and $pdf->keywords($params->{keywords});
286             }
287 1         55 $params->{pdf} = $pdf;
288              
289 1         8 my $media_size = [PDF::API2::Util::page_size($params->{pageFormat})];
290 1 50 33     28 $media_size->[2] && $media_size->[3] or die "could not determine the page size!";
291              
292 1 50       5 if ($params->{pageOrientation}) {
293 0 0 0     0 if (($params->{pageOrientation} eq 'landscape' and $media_size->[2] < $media_size->[3])
      0        
      0        
294             || ($params->{pageOrientation} eq 'portrait' and $media_size->[2] > $media_size->[3])) {
295             # rotate media
296 0         0 my $temp = $media_size->[3];
297 0         0 $media_size->[3] = $media_size->[2];
298 0         0 $media_size->[2] = $temp;
299             }
300             }
301              
302 1         4 $params->{pageWidth} = $media_size->[2];
303 1         3 $params->{pageHeight} = $media_size->[3];
304 1         3 $params->{pageSize} = $media_size;
305              
306 1         3 my $puzzles_per_page = $params->{puzzlesPerPage};
307              
308 1         3 my ($h_puzzles_per_page, $v_puzzles_per_page);
309              
310 1 50       9 if ($puzzles_per_page =~ /^(\d+)x(\d+)$/i) {
    0          
    0          
    0          
    0          
311 1         4 $h_puzzles_per_page = $1;
312 1         3 $v_puzzles_per_page = $2;
313 1         5 $params->{puzzlesPerPage} = $1 * $2;
314             } elsif ($puzzles_per_page eq '1') {
315 0         0 $h_puzzles_per_page = 1;
316 0         0 $v_puzzles_per_page = 1;
317             } elsif ($puzzles_per_page eq '2') {
318 0         0 $h_puzzles_per_page = 1;
319 0         0 $v_puzzles_per_page = 2;
320             } elsif ($puzzles_per_page eq '3') {
321 0         0 $h_puzzles_per_page = 2;
322 0         0 $v_puzzles_per_page = 1;
323             } elsif ($puzzles_per_page eq '4') {
324 0         0 $h_puzzles_per_page = 2;
325 0         0 $v_puzzles_per_page = 2;
326             } else {
327 0         0 my $page_ratio = $params->{pageHeight} / $params->{pageWidth};
328 0         0 my $v_pages = $params->{puzzlesPerPage} / $page_ratio;
329 0         0 die "You might rather want to specify 'puzzlesPerPage' as a layout, like e.g. '2x3' (WxH)\n";
330             }
331              
332 1         5 _add_table_settings($params);
333 1         8 _add_table_properties($params);
334 1         5 _add_table_positions($params, $h_puzzles_per_page, $v_puzzles_per_page);
335              
336             $params->{puzzleCount} =
337             $params->{pagesAdded} =
338 1         7 $params->{nextPosOnPage} = 0;
339              
340 1         15 return $params
341             }
342              
343             sub _add_table_settings {
344 1     1   4 my ($params) = shift;
345              
346             # intialization of parameters for the PDF::Table->table() method
347             # some values will get overwritten during init() !
348             my $font = $PDF_API2_IS_OLD ?
349 1 50       9 $params->{pdf}->corefont('Helvetica-Bold') : $params->{pdf}->font('Helvetica-Bold');
350              
351 1 50       36279 $params->{tableSettings} = $PDF_TABLE_IS_OLD ? {
352             start_h => 280, # with 279, the table will be broken up, even when exactly 279 gets returned on a dry run (ink => 0)
353             w => 279,
354              
355             next_h => 500,
356             next_y => 700,
357              
358             border => 1,
359             padding_top => 1.5,
360             padding_bottom => 7.5,
361             row_height => 31,
362             font => $font,
363             font_size => 22,
364             column_props => [
365             (({min_w => 31, justify => 'center'}) x 9)
366             ],
367              
368             } : {
369             h => 280, # with 279, the table will be broken up, even when exactly 279 gets returned on a dry run (ink => 0)
370             w => 279,
371              
372             next_h => 500,
373             next_y => 700,
374              
375             border_w => 1,
376             # following 2 values should omit the table's outer borders, but don't work
377             #h_border_w => 0,
378             #v_border_w => 0,
379             padding => 0,
380             padding_top => 1.5,
381             min_rh => 31,
382             font => $font,
383             font_size => 22,
384             # global size and justify came stepwise in recent versions
385             #size => join(' ', ((31) x 9)),
386             #justify => 'center',
387             column_props => [
388             (({min_w => 31, justify => 'center'}) x 9)
389             ],
390             };
391             }
392              
393             sub _add_table_positions{
394 1     1   6 my ($params, $h_puzzles_per_page, $v_puzzles_per_page) = @_;
395              
396 1         3 my $trim = 12;
397 1         2 my $trim_top = 30;
398 1         3 my $trim_bottom = 35;
399 1         2 my $h_spacing = 12;
400             # the preset values estimate 2x2 puzzles, calculate a min vertical space by scaling fontsize + padding
401 1         5 my $v_spacing = 2 * ($params->{tableSettings}{font_size} + 3) / $v_puzzles_per_page;
402 1         6 my $h_size = ($params->{pageWidth} - 2 * $trim - (($h_puzzles_per_page - 1) * $h_spacing)) / $h_puzzles_per_page;
403 1         5 my $v_size = (($params->{pageHeight} - ($trim_top + $trim_bottom)) - ($v_puzzles_per_page * $v_spacing)) / $v_puzzles_per_page;
404              
405 1         3 my ($table_size, $cell_size);
406 1 50       5 if ($h_size < $v_size) {
407 0         0 $cell_size = int $h_size / 9;
408             } else {
409 1         4 $cell_size = int $v_size / 9;
410             }
411 1         2 $table_size = $cell_size * 9;
412              
413             $h_spacing = $h_puzzles_per_page > 1 ?
414 1 50       51 int ($params->{pageWidth} - 2 * $trim - ($h_puzzles_per_page * $table_size)) / ($h_puzzles_per_page - 1)
415             : 0;
416             $v_spacing = $v_puzzles_per_page > 1 ?
417 1 50       11 int (($params->{pageHeight} - ($trim_top + $trim_bottom)) - ($v_puzzles_per_page * $table_size)) / ($v_puzzles_per_page)
418             : 0;
419              
420 1         3 my @positions = ();
421 1         3 my $x = $trim;
422 1         2 my $y = $params->{pageHeight} - $trim_top;
423 1         8 for (my $r = 0; $r < $v_puzzles_per_page; $r++) {
424 3         6 $x = $trim;
425 3         11 for (my $c = 0; $c < $h_puzzles_per_page; $c++) {
426 15         39 push @positions, [
427             $x, $y,
428             ];
429 15         40 $x += ($table_size + $h_spacing);
430             }
431 3         10 $y -= ($table_size + $v_spacing);
432             }
433 1         4 $params->{positionData} = \@positions;
434 1         4 $params->{factor} = _scale_table($params->{tableSettings}, $table_size, $cell_size);
435            
436 1         3 return $params
437             }
438              
439             sub _scale_table {
440 1     1   4 my ($settings, $table_size, $cell_size) = @_;
441              
442 1         3 my $factor = $table_size / $settings->{w};
443 1 50       5 return 1 if $factor == 1;
444              
445 1 50       4 if ($PDF_TABLE_IS_OLD) {
446 0         0 $settings->{start_h} = $table_size + 1;
447 0         0 $settings->{w} = $table_size;
448 0         0 $settings->{row_height} = $cell_size;
449 0         0 $settings->{border} *= $factor;
450 0         0 $settings->{font_size} *= $factor;
451 0 0       0 $settings->{padding_top} *= $factor if $settings->{padding_top};
452 0 0       0 $settings->{padding_bottom} *= $factor if $settings->{padding_bottom};
453             $settings->{column_props} = [
454 0         0 (({min_w => $cell_size, justify => 'center'}) x 9)
455             ],
456              
457             } else {
458 1         3 $settings->{h} = $table_size + 1;
459 1         4 $settings->{w} = $table_size;
460 1         2 $settings->{min_rh} = $cell_size;
461 1         4 $settings->{border_w} *= $factor;
462 1         3 $settings->{font_size} *= $factor;
463 1 50       4 $settings->{padding_top} *= $factor if $settings->{padding_top};
464             $settings->{column_props} = [
465 1         8 (({min_w => $cell_size, justify => 'center'}) x 9)
466             ],
467             }
468 1         4 return $factor
469             }
470              
471             sub _add_table_properties {
472 1     1   3 my $params = shift;
473              
474             $params->{patternProp} = {
475             # color of extraRegion
476 1         30 background_color => '#6666ff',
477             };
478              
479             $params->{greyProp} = {
480             # color of odd 3x3 boxes
481             # '#e6e6e6' is to light, almost invisible in print!
482 1         7 background_color => '#d0d0d0',
483             };
484              
485             $params->{whiteProp} = {
486             # default cell color (undefined would be transparent)
487 1         5 background_color => '#ffffff',
488             };
489              
490             $params->{colorPattern} = {
491             # colors used in c-variant
492 1         13 '00' => '#ff6666', # red
493             '01' => '#808080', # dark grey
494             '02' => '#66ff66', # green
495             '10' => '#c0c0c0', # light grey
496             '11' => '#66ffff', # light blue
497             '12' => '#b266ff', # purple
498             '20' => '#ffff66', # yellow
499             '21' => '#ff66ff', # magenta
500             '22' => '#ffffff', # white
501             };
502              
503             $params->{areaColors} = {
504             # color sets used in jigsaw puzzles without extraRegion
505 1         8 '3' => [
506             '#ffab99', # red
507             '#99ff99', # green
508             '#99bcff', # light blue
509             ],
510             '4' => [
511             '#fff199', # yellow
512             '#99ff99', # green
513             '#99bcff', # light blue
514             '#ffab99', # red
515             ],
516             };
517              
518 1         5 return $params
519             }
520              
521             #########################################################################
522              
523             sub _create_matrix {
524             # put the string of 81 chars into a 9x9 array
525 34 50   34   130 my $line = shift() or return undef;
526              
527 34         606 my @numbers = split //, $line;
528              
529 34         76 my @matrix = ();
530 34         96 while (@numbers) {
531 306         1494 push @matrix, [ splice @numbers, 0, 9 ];
532             }
533              
534             \@matrix
535 34         110 }
536              
537             sub _is_pattern_area {
538 2106     2106   2837 my $self = shift;
539 2106         3654 my ($r, $c, $v) = @_;
540              
541 2106 100       5612 if ($v eq 'n') {
    100          
    100          
    100          
    100          
    100          
    100          
    50          
542             # standard
543 972         3741 return 0;
544             } elsif ($v eq 'x') {
545             # x-sudoku
546 162 100 100     460 return (($r == $c) || ($r == 8 - $c)) ? $self->{patternProp} : 0;
547             } elsif ($v eq 'h') {
548             # hyper
549 162 100 100     754 return ($r=~/^[123567]$/ && $c=~/^[123567]$/) ? $self->{patternProp} : 0;
550             } elsif ($v eq 'p') {
551             # percent
552 162 100 100     1114 return (($r == 8 - $c) || ($r=~/^[123]$/ && $c=~/^[123]$/) || ($r=~/^[567]$/ && $c=~/^[567]$/)) ? $self->{patternProp} : 0;
553             } elsif ($v eq 'd') {
554             # center dot
555 162 100 100     875 return ($r=~/^[147]$/ && $c=~/^[147]$/) ? $self->{patternProp} : 0;
556             } elsif ($v eq 'a') {
557             # asterisk
558 162         296 my $cmp = $r.$c;
559 162 100       737 return ($cmp =~ /^(14|22|26|41|44|47|62|66|74)$/) ? $self->{patternProp} : 0;
560             } elsif ($v eq 'g') {
561             # girandola
562 162         296 my $cmp = $r.$c;
563 162 100       829 return ($cmp =~ /^(00|08|14|41|44|47|74|80|88)$/) ? $self->{patternProp} : 0;
564             } elsif ($v eq 'c') {
565             # color
566             return {
567 162         701 background_color => $self->{colorPattern}{$r%3 . $c%3}
568             };
569             } else {
570 0         0 die "don't know about pattern '$v'";
571             }
572             }
573              
574             sub _is_grey_block {
575 1270     1270   1933 my ($r, $c) = @_;
576 1270   100     8631 return (($r>2 && $r<6) xor ($c>2 && $c<6));
577             }
578              
579             sub _get_cell_prop {
580 2106     2106   3046 my $self = shift;
581 2106         4097 my ($i, $j, $variant, $grid, $area_colors) = @_;
582              
583 2106 100       3812 if (my $prop = $self->_is_pattern_area($i, $j, $variant)) {
    100          
    100          
584 376         1397 return $prop;
585              
586             } elsif (! $grid) {
587 1270 100       3145 return _is_grey_block($i, $j) ? $self->{greyProp} : $self->{whiteProp};
588              
589             } elsif ($variant eq 'n') {
590             return {
591 81         460 background_color => $area_colors->{$grid->[$i][$j]},
592             };
593             }
594              
595 379         1462 return $self->{whiteProp};
596             }
597              
598             # determining the colors for jigsaw lines puzzles is anything but trivial,
599             # at least if we want to use as little colors as possible.
600             sub _get_area_colors {
601 1     1   3 my $self = shift;
602 1 50       5 my $areas = shift() or return undef;
603 1         3 my $puzzle_number = shift();
604              
605 1         3 my $adjoins_with = {};
606 1         4 my $size = @$areas;
607 1         5 for (my $i = 0; $i < $size; $i++) {
608 9         24 for (my $j = 0; $j < $size; $j++) {
609 81         146 my $a = $areas->[$i][$j];
610              
611 81 100       169 if ($j < $size - 1) {
612 72         167 my $r = $areas->[$i][$j+1];
613 72 100       159 if ($r != $a) {
614 32         79 $adjoins_with->{$a}{$r} = 1;
615 32         66 $adjoins_with->{$r}{$a} = 1;
616             }
617             }
618              
619 81 100       178 if ($i < $size - 1) {
620 72         149 my $d = $areas->[$i+1][$j];
621 72 100       205 if ($d != $a) {
622 26         56 $adjoins_with->{$a}{$d} = 1;
623 26         75 $adjoins_with->{$d}{$a} = 1;
624             }
625             }
626             }
627             }
628              
629 1         31 my $area_to_color = {};
630 1         3 my $assigned_colors = {};
631 1         3 my $max_color = 0;
632 1         2 my $most_neighbours = 0;
633             # sort areas by number of neighbours, descending
634             # secondary sort by area index, else don't use the minimal number of colors
635 1         11 foreach my $area (sort {
636 19 50       47 scalar(keys %{$adjoins_with->{$b}}) <=> scalar(keys %{$adjoins_with->{$a}})
  19         41  
  19         60  
637             || $a <=> $b
638             } keys %$adjoins_with) {
639 9   66     27 $most_neighbours ||= $area;
640              
641 9         20 foreach my $color (0 .. 3) {
642 17         27 my $conflict = 0;
643 17         27 foreach my $neighbour (keys %{$adjoins_with->{$area}}) {
  17         48  
644 45         76 my $c = $area_to_color->{$neighbour};
645 45 100 100     144 if (defined($c) && $c == $color) {
646 8         13 $conflict = 1;
647 8         16 last;
648             }
649             }
650 17 100       42 unless ($conflict) {
651 9         20 $area_to_color->{$area} = $color;
652 9 100       22 if ($color > $max_color) {
653 2         3 $max_color = $color;
654             }
655 9         19 last;
656             }
657             }
658             }
659 1         4 my $colors_used = $max_color + 1;
660 1 50 33     12 if ($colors_used == 2) {
    50          
661 0         0 warn "[puzzle #$puzzle_number] only 2 colors needed: replacing color of area $most_neighbours with 3rd color\n";
662 0         0 $area_to_color->{$most_neighbours} = 2;
663 0         0 $colors_used = 3;
664              
665             } elsif ($colors_used != 3 && $colors_used != 4) {
666 0         0 die "\nfatal: an unprepared number $colors_used of colors are used:\n";
667             }
668              
669 1         5 foreach my $a (keys %$area_to_color) {
670 9         26 $area_to_color->{$a} = $self->{areaColors}{$colors_used}[$area_to_color->{$a}];
671             }
672              
673 1         11 return $area_to_color;
674             }
675            
676             sub _draw_straight_lines {
677 18     18   71 my ($content, $x, $y, , $table_size, $cell_size, $factor) = @_;
678              
679 18 50       51 if ($PDF_API2_IS_OLD) {
680 0         0 $content->linewidth(3 * $factor);
681 0         0 $content->linecap(2);
682             } else {
683 18         99 $content->line_width(3 * $factor);
684 18         903 $content->line_cap(2);
685             }
686              
687             # 4 vertical lines
688 18         988 $content->move($x, $y);
689 18         2001 $content->vline($y-$table_size);
690 18         1564 $content->move($x+($cell_size*3), $y);
691 18         1561 $content->vline($y-$table_size);
692 18         1528 $content->move($x+($cell_size*6), $y);
693 18         1541 $content->vline($y-$table_size);
694 18         1396 $content->move($x+$table_size, $y);
695 18         1554 $content->vline($y-$table_size);
696              
697             # 4 horizontal lines
698 18         1484 $content->move($x, $y);
699 18         1547 $content->hline($x+$table_size);
700 18         1577 $content->move($x, $y-($cell_size*3));
701 18         1560 $content->hline($x+$table_size);
702 18         1502 $content->move($x, $y-($cell_size*6));
703 18         1593 $content->hline($x+$table_size);
704 18         1462 $content->move($x, $y-$table_size);
705 18         1593 $content->hline($x+$table_size);
706              
707 18         1423 $content->stroke();
708             }
709              
710             sub _draw_jigsaw_lines {
711 8     8   29 my ($content, $x, $y, $grid, $table_size, $cell_size, $factor) = @_;
712              
713 8 50       26 if ($PDF_API2_IS_OLD) {
714 0         0 $content->linewidth(3 * $factor);
715 0         0 $content->linecap(2);
716             } else {
717 8         44 $content->line_width(3 * $factor);
718 8         379 $content->line_cap(2);
719             }
720              
721             # outer lines
722 8         376 $content->move($x, $y);
723 8         721 $content->vline($y-$table_size);
724 8         595 $content->move($x+$table_size, $y);
725 8         600 $content->vline($y-$table_size);
726 8         569 $content->move($x, $y);
727 8         638 $content->hline($x+$table_size);
728 8         595 $content->move($x, $y-$table_size);
729 8         629 $content->hline($x+$table_size);
730              
731 8         548 $content->stroke();
732              
733             # jigsaw lines
734 8 50       219 if ($PDF_API2_IS_OLD) {
735 0         0 $content->linecap(0);
736             } else {
737 8         30 $content->line_cap(0);
738             }
739              
740 8         350 for (my $i = 0; $i < 9; $i++) {
741 72         125 my $my = $y - ($i*$cell_size);
742 72         137 for (my $j = 0; $j < 9; $j++) {
743 648         17035 my $mx = $x + ($j*$cell_size);
744 648         1023 my $this_area = $grid->[$i][$j];
745              
746 648 100       1093 if ($i < 8) {
747 576 100       1494 if ($grid->[$i+1][$j] != $this_area) {
748 220         597 $content->move($mx, $my-$cell_size);
749 220         18663 $content->hline($mx+$cell_size);
750             }
751             }
752              
753 648 100       16218 if ($j < 8) {
754 576 100       1531 if ($grid->[$i][$j+1] != $this_area) {
755 236         644 $content->move($mx+$cell_size, $my);
756 236         17953 $content->vline($my-$cell_size);
757             }
758             }
759             }
760             }
761              
762 8         29 $content->stroke();
763             }
764              
765             sub _format_time_w_zone_offset {
766 1   33 1   9 my $epoch = shift() || time();
767              
768 1         15 my $offset_sec = $epoch - timelocal( gmtime $epoch );
769 1 50       191 my $sign = ($offset_sec =~ s/^-//) ? '-' : ($offset_sec > 0 ) ? '+' : 'Z';
    50          
770              
771 1         2 my $offset = '';
772 1 50       5 if ($offset_sec > 0) {
773 0         0 my $offset_hrs = int $offset_sec / 3600;
774 0         0 my $offset_min = int( ($offset_sec % 3600) / 60);
775 0         0 $offset = sprintf '%02d\'%02d', $offset_hrs, $offset_min;
776             # Time offset ought to be: [-+Z] (hh') (mm')
777             # API2 until v2.041 gave a correct format example in the pod, but no validation took place.
778             # From v2.042 - 2.044 introduced a (faulty) date validation which _required_ a _leading_
779             # apostrophe with offset minutes but croaked on the trailing offset mm' apostrophe.
780             # Now both apostrophes are optional since v2.045.
781 0 0 0     0 if ($PDF::API2::VERSION < 2.042 || $PDF::API2::VERSION > 2.044) {
782             # (no validation || 'tolerant' validation) => pass correct format
783 0         0 $offset .= "'";
784             }
785             } # or else just return Z(ulu), to avoid any confusion
786              
787 1         22 my @lt = localtime($epoch);
788 1         20 return sprintf('%d%02d%02d%02d%02d%02d%s%s',
789             $lt[5]+1900, $lt[4]+1, @lt[3,2,1,0], $sign, $offset)
790             }
791              
792             sub _make_version_comparable {
793 4     4   17 my $v = shift;
794 4         58 $v =~ s/^(v?[0-9]+(?:\.[0-9]+)).*$/$1/;
795 4 50       28 if ($v =~ s/^v//) {
796 0         0 $v =~ s/^\./\.0/;
797             }
798 4         37 return $v
799             }
800              
801             1;
802              
803             =encoding UTF-8
804              
805             =head1 NAME
806              
807             Games::Sudoku::Pdf - Produce pdf files from your digital Sudoku sources or collections.
808              
809             =head1 DESCRIPTION
810              
811             An easy way to create pdf files of 9x9 Sudoku puzzles from various sources, which you can give to your friends or print out and pencil solve at the beach.
812             Sixteen variants of 9x9 Sudoku are supported. (See the output of scripts/example.pl.)
813             Just specify how many puzzles (columns x rows) to arrange per page and the positioning and scaling will be adapted automatically.
814              
815             =head1 SYNOPSIS
816              
817             my $writer = Games::Sudoku::Pdf->new(%options);
818              
819             $writer->add_puzzle(
820             clues => '.9.46.....85...4.7..........3.2.8.4...9...1...6.1.4.5..........3.4...92.....49.8.',
821             jigsaw => '111222333141122333141226663441522563745555563745885966744488969777889969777888999',
822             extraRegion => 'g',
823             bottomLine => 'jigsaw #5 from Daisy (very hard!)',
824             );
825              
826             $writer->read_input_file('./all-17-clues-sudoku.txt', slice => [5..9, 7..25]);
827              
828             $writer->close();
829              
830             >sudoku2pdf my_sudokus.txt > my_sudokus.pdf
831              
832             =head1 METHODS
833              
834             =head2 new()
835              
836             Games::Sudoku::Pdf->new( %options )
837              
838             Returns the writer object. The following options are available:
839              
840             =head3 pageFormat default 'A4'
841              
842             Find possible values in L.
843              
844             =head3 pageOrientation
845              
846             Possible values are 'portrait' or 'landscape'.
847             Default is the resp. format's definiton in L.
848              
849             =head3 puzzlesPerPage default 4 (resolved to '2x2')
850              
851             Specifies the number of Sudoku per page, their size and positions.
852             You will probably want to give the WxH notation.
853             If you specify e.g. '1x5', 5 small Sudoku will be placed at the left side, leaving place for your manual notes to the right.
854             Once you specify 2 or more columns, they will get equally distributed from left to right.
855              
856             =head3 appendFile
857              
858             You can specify path and name of any existing pdf file, to which the new pages shall be appended.
859              
860             =head3 title, author, subject, keywords
861              
862             Any of these four optional values will be written to the pdf meta data fields.
863              
864             =head3 extraRegion default none
865              
866             For Sudoku sporting an 'extra region' (X-, Hyper-, Percent- ...) this parameter can be given globally here,
867             or for each puzzle L.
868             The following variants are recognized:
869              
870             =over 4
871              
872             =item x X-Sudoku
873              
874             =item h Hyper Sudoku
875              
876             =item p Percent Sudoku
877              
878             =item c Color Sudoku
879              
880             Just one fixed version is currently possible (colors 1-9):
881              
882             +-------------------+
883             | 1 2 3 1 2 3 1 2 3 |
884             | 4 5 6 4 5 6 4 5 6 |
885             | 7 8 9 7 8 9 7 8 9 |
886             | 1 2 3 1 2 3 1 2 3 |
887             | 4 5 6 4 5 6 4 5 6 |
888             | 7 8 9 7 8 9 7 8 9 |
889             | 1 2 3 1 2 3 1 2 3 |
890             | 4 5 6 4 5 6 4 5 6 |
891             | 7 8 9 7 8 9 7 8 9 |
892             +-------------------+
893              
894             =item d Center Dot Sudoku
895              
896             =item a Asterisk Sudoku
897              
898             =item g Girandola Sudoku
899              
900             =back
901              
902             For an overview take a look at the script and output of L.
903              
904             =head2 add_puzzle()
905              
906             $writer->add_puzzle(%options)
907              
908             Adds the next puzzle to the pdf. The options are:
909              
910             =head3 clues mandatory
911              
912             A string of 81 characters, the common Sudoku exchange format. Given clues as numbers 1-9, empty cells denoted by C<.>, C<0>, C or any non-digit.
913              
914             =head3 jigsaw optional
915              
916             Instead of regular 3x3 boxes with straight sides, the puzzle can be broken up into irregular shapes.
917             These may be provided by a string of 81 characters with the numbers 1-9 whose positions form 9 contiguous areas in the puzzle matrix.
918             Refer to L.
919              
920             =head3 extraRegion default none
921              
922             A single letter of xhpcdag, as described in L above.
923              
924             =head3 bottomLine default none
925              
926             A short string which will be put beneath the puzzle.
927             You may want to provide the puzzle's source, its estimated difficulty, a number or anything else.
928             Because we use the pdf corefonts, only latin1 is supported.
929              
930             It is up to you and your Sudoku sources, that the given clues together with the other optional parameters provided
931             defines a proper puzzle with exactly one solution.
932              
933             =head2 read_input_file()
934              
935             $writer->read_input_file($input_file, %options)
936              
937             A convenience method to slurp an entire text file with your puzzle collection.
938             The $input_file may be a file name or an open handle. The %options are:
939              
940             =head3 slice optional
941              
942             An array reference that contains the numbers of the puzzle lines that will be processed.
943             Only lines starting with a string of 81 givens are counted. Or, if you provided a L, for which the handler returned a hashref.
944              
945             =head3 lineHandler optional
946              
947             A reference to your custom subroutine thats given the chomp-ed line and has to return a hashref that will be fed to L.
948             If the return value is false, the line is skipped.
949              
950             If no custom line handler is provided, lines are expected to follow the format
951              
952             <81 char givens> [ [<81 char jigsaw>] [ [<1 char extraRegion>] [ []]]]
953              
954             Delimiters can be C<'|', ':', ',', ';', EtabE>.
955             For any standard Sudoku, the givens alone are sufficient.
956             See the 4 parameter's descriptions to L.
957              
958             Note: If this somewhat limited input format does not suit you, take a look at the commandline script L.
959              
960             =head2 page_break()
961              
962             $writer->page_break()
963              
964             Continue adding puzzles on a new page with otherwise unchanged settings.
965             There will be B empty pages inserted by repeated calls.
966              
967             =head2 close()
968              
969             $writer->close( $output_file )
970              
971             Writes the pdf to file and frees memory.
972             The $output_file may be omitted if an L<'appendFile'|/"appendFile"> was specified in L and if that file is supposed to be replaced.
973             B
974              
975             =head1 SCRIPTS
976              
977             =head2 sudoku2pdf
978              
979             After installation of Games::Sudoku::Pdf this command line script should be in your path.
980             Flexible input options are available. Invoke Csudoku2pdf -h> for details.
981              
982             =head1 DEPENDENCIES
983              
984             =over 4
985              
986             =item * L
987              
988             =item * L
989              
990             =item * L
991              
992             =back
993              
994             =head1 SEE ALSO
995              
996             L, L
997              
998             =head1 COPYRIGHT AND LICENSE
999              
1000             The following copyright notice applies to all the files provided in
1001             this distribution, including binary files, unless explicitly noted
1002             otherwise.
1003              
1004             This software is copyright (c) 2024 by Steffen Heinrich
1005              
1006             This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
1007              
1008             =cut