File Coverage

blib/lib/Mojolicious/Command/Author/generate/obrazi.pm
Criterion Covered Total %
statement 24 256 9.3
branch 0 68 0.0
condition 0 46 0.0
subroutine 8 25 32.0
pod 4 5 80.0
total 36 400 9.0


line stmt bran cond sub pod time code
1             package Mojolicious::Command::Author::generate::obrazi;
2 1     1   1066 use feature ':5.26';
  1         3  
  1         133  
3 1     1   8 use Mojo::Base Mojolicious::Command => -signatures;
  1         13  
  1         14  
4 1     1   5085 use Mojo::File 'path';
  1         3  
  1         62  
5 1     1   11 use Mojo::Util qw(url_escape punycode_decode punycode_encode getopt encode decode dumper);
  1         2  
  1         85  
6 1     1   7 use Mojo::Collection 'c';
  1         2  
  1         59  
7 1     1   1229 use Text::CSV_XS qw( csv );
  1         13722  
  1         78  
8 1     1   8 use Mojo::Loader qw(data_section);
  1         3  
  1         53  
9 1     1   1109 use Imager;
  1         40105  
  1         10  
10              
11             my $_formats = join '|', map { $_ =~ /jpe?g/i ? 'jpe?g' : $_ } sort keys %Imager::formats;
12             my $FILETYPES = qr/\.(?:$_formats)$/i;
13 0     0     my sub _U {'UTF-8'}
14             has description => 'Generate a gallery from a directory structure with images';
15              
16             # our own log, used instead of 'say'.
17             has log => sub {
18             Mojo::Log->new(format => sub { "[$$] [$_[1]] " . join(' ', @_[2 .. $#_]) . $/ });
19             };
20              
21             has usage => sub { shift->extract_usage . $/ . 'Supported formats: ' . $FILETYPES . $/ };
22             has from_dir => sub { path('./')->to_abs };
23             has images => sub {
24             $_[0]->matrix->grep(sub { $_->[1] =~ $FILETYPES });
25             };
26             has _linked_images => sub {
27             my $c = 0;
28             my $images = $_[0]->images;
29             return {map { $_->[1] => {self => $_, prev => $images->[$c - 1], next => $images->[$c + 1], id => ++$c} } @$images};
30             };
31             has categories => sub {
32             $_[0]->matrix->grep(sub { !$_->[-1] && !$_->[-2] && $_->[1] =~ /$_->[0]$/ });
33             };
34             has files_per_subproc => sub {
35             return int(scalar(@{$_[0]->images}) / $_[0]->subprocs_num) + 1;
36             };
37             has csv_filename => 'index.csv';
38             has subprocs_num => 4;
39             has template_file => '';
40             has obrazec_file => '';
41             has to_dir => sub { $_[0]->app->home->child('public') };
42             has publish_url => '/obrazi.html';
43              
44             # Default titles and descriptions
45             has defaults => sub { {
46             author => 'Марио Беров',
47             category_title => 'Заглавие на категорията',
48             category_description => 'Описание на категорията',
49             image_title => 'Заглавие на изображението',
50             image_description => 'Описание на изображението'
51             . $/
52             . ' Материали, размери,какво, защо - според каквото мислиш, че е важно.',
53             } };
54              
55             # An empty Imager instance on which the read() method will be called for every
56             # image we work with.
57             has imager => sub { Imager->new };
58              
59             my @header = qw(category path title description author image thumbnail);
60              
61             # csv file contents
62             has matrix => sub { c([@header]) };
63              
64             # resized images
65             has _processed => sub { c() };
66              
67             # '1000x1000'
68             sub max {
69 0 0   0 1   if ($_[1]) {
70 0 0 0       $_[0]->{max} = $_[1] && return $_[0] if ref $_[1];
71 0           ($_[0]->{max}{width}, $_[0]->{max}{height}) = $_[1] =~ /(\d+)x(\d+)/;
72 0           return $_[0];
73             }
74 0   0       return $_[0]->{max} //= {width => 1000, height => 1000};
75             }
76              
77             # '100x100'
78             sub thumbs {
79 0 0   0 1   if ($_[1]) {
80 0 0 0       $_[0]->{thumbs} = $_[1] && return $_[0] if ref $_[1];
81 0           ($_[0]->{thumbs}{width}, $_[0]->{thumbs}{height}) = $_[1] =~ /(\d+)x(\d+)/;
82 0           return $_[0];
83             }
84 0   0       return $_[0]->{thumbs} //= {width => 100, height => 100};
85             }
86              
87 0     0 1   sub run ($self, @args) {
  0            
  0            
  0            
88 0           getopt \@args,
89             'f|from=s' => \(my $from_dir = $self->from_dir),
90             'to=s' => \(my $to_dir = $self->to_dir),
91             'x|max=s' => \(my $max = $self->max),
92             's|thumbs=s' => \(my $thumbs = $self->thumbs),
93             'i|index=s' => \(my $csv_filename = $self->csv_filename),
94             't|template=s' => \(my $template = $self->template_file),
95             'o|obrazec=s' => \(my $obrazec = $self->obrazec_file),
96             'u|url=s' => \(my $url = $self->publish_url),
97             ;
98 0 0 0       if ($template ne $self->template_file and not -f $template) {
99 0           Carp::croak("Template $template does not exist. " . "Please provide an existing template file.");
100             }
101 0 0         $self->template_file($template) if $template;
102              
103 0 0 0       if ($obrazec ne $self->obrazec_file and not -f $obrazec) {
104 0           Carp::croak("Single image template $obrazec does not exist. " . "Please provide an existing template file.");
105             }
106 0 0         $self->obrazec_file($obrazec) if $obrazec;
107              
108 0           $self->from_dir(path($from_dir)->to_abs)->to_dir(path($to_dir)->to_abs)->max($max)->thumbs($thumbs)
109             ->csv_filename($csv_filename)->publish_url(decode _U, $url);
110 0           return $self->_do_csv->_resize_and_copy_to_dir->_do_html;
111             }
112              
113             # Calculates the resized image dimensions according to the C<$self-Emax>
114             # and C<$self-Ethumbs> gallery contraints. Accepts the utf8 decoded path
115             # and the raw path to the file to be worked on. Returns two empty strings if
116             # there is error reading the image and warns about the error. Returns filenames
117             # for the resized image and the thumbnail image.
118 0     0 1   sub calculate_max_and_thumbs ($self, $path, $raw_path) {
  0            
  0            
  0            
  0            
119 0           state $imager = $self->imager;
120 0           my $log = $self->log;
121 0           my $img;
122 0           my $image = [$raw_path->to_array->[-1] =~ /^(.+?)\.(.\w+)$/];
123 0           $log->info('Inspecting image ', $path);
124              
125 0           my $max = $self->max;
126 0           my $thumbs = $self->thumbs;
127 0           my %size = %$max;
128 0           my %thumb_size = %$thumbs;
129 0           my $fh = $raw_path->open();
130 0 0         unless ($fh->binmode) {
131 0           $log->warn(" !!! Skipping $path. Error: " . $!);
132 0           return ('', '');
133             }
134 0 0         if (not eval { $img = $imager->read(fh => $fh) }) {
  0            
135 0           $log->warn(" !!! Skipping $path. Image error: " . $imager->errstr());
136 0           return ('', '');
137             }
138             else {
139 0           $image->[0] = decode _U, $image->[0];
140 0           %size = (width => $img->getwidth, height => $img->getheight);
141 0           %thumb_size = %size;
142 0 0 0       if ($size{width} > $max->{width} || $size{height} > $max->{height}) {
143             @size{qw(x_scale y_scale width height)}
144 0           = $img->scale_calculate(xpixels => $max->{width}, ypixels => $max->{height}, type => 'min');
145             }
146              
147 0 0 0       if ($thumb_size{width} > $thumbs->{width} || $thumb_size{height} > $thumbs->{height}) {
148             @thumb_size{qw(x_scale y_scale width height)}
149 0           = $img->scale_calculate(xpixels => $thumbs->{width}, ypixels => $thumbs->{height}, type => 'min');
150             }
151             }
152              
153             return (
154 0           punycode_encode($image->[0]) . "_$size{width}x$size{height}.$image->[1]",
155             punycode_encode($image->[0]) . "_$thumb_size{width}x$thumb_size{height}.$image->[1]"
156             );
157             }
158              
159             # Reads the `from_dir` and dumps a csv file named after the from_dir folder.
160             # The file contains a table with paths and default titles and descriptions for
161             # the pictures. This file can be given to the painter to add titles and
162             # descriptions for the pictures using an application like LibreOffice Calc or
163             # M$ Excel.
164 0     0     sub _do_csv ($self, $root = $self->from_dir) {
  0            
  0            
  0            
165 0           my $csv_filepath = decode _U, $root->child($self->csv_filename);
166 0           my $log = $self->log;
167 0 0         if (-f $csv_filepath) {
168 0           $log->info("$csv_filepath already exists.$/"
169             . "\tContinuing with resizing and copying images.$/"
170             . "\tSome rows may be updated with filenames for resized images and thumbnails...$/");
171 0           return $self;
172             }
173 0           my $category = '';
174 0           my $defaults = $self->defaults;
175 0           my $matrix = $self->matrix;
176 0           $root->list_tree({dir => 1})->sort->each(
177 0     0     sub ($e, $num) {
  0            
  0            
178 0           $self->_make_matrix_row($root, $e, \$category, $defaults, $matrix, $log);
179             }
180 0           );
181 0           csv(in => $matrix->to_array, enc => _U, out => \my $data, binary => 1, sep_char => ",");
182 0           path($csv_filepath)->spurt($data);
183              
184 0           return $self;
185             }
186              
187 0     0     sub _make_matrix_row ($self, $root, $raw_path, $category, $defaults, $matrix, $log) {
  0            
  0            
  0            
  0            
  0            
  0            
  0            
  0            
188 0           my $path = decode(_U, $_->to_string =~ s|$root/||r);
189 0 0         if (-d $raw_path) {
    0          
190 0           $log->info("Inspecting category $path");
191 0           $$category = decode(_U, $_->to_array->[-1]);
192             push @$matrix,
193             [
194             $$category, $path,
195             "$defaults->{category_title} – $$category",
196             $defaults->{category_description},
197 0           $defaults->{author}, '', '',
198             ];
199             }
200             elsif (-f $raw_path) {
201 0 0         if ($_ !~ $FILETYPES) {
202 0           $log->warn("Skipping unsupported file $path…");
203 0           return;
204             }
205              
206             # for images without category - which are in the $root folder
207 0 0         $$category = '' unless $path =~ /$$category/;
208             push @$matrix,
209             [
210             $$category, $path,
211             "$defaults->{image_title} – " . path($path)->basename,
212             $defaults->{image_description},
213 0           $defaults->{author}, $self->calculate_max_and_thumbs($path, $raw_path)
214             ];
215             }
216             }
217              
218             # Scales and resizes images to maximum width and height and generates thumbnails.
219 0     0     sub _resize_and_copy_to_dir($self) {
  0            
  0            
220 0           my $matrix = $self->matrix;
221 0           my $csv_filepath = decode _U, $self->from_dir->child($self->csv_filename);
222 0 0         if (@$matrix == 1) {
223              
224             # read the CSV file from disk to get calculated dimensions
225 0           $matrix = c @{csv(in => $csv_filepath, enc => _U, binary => 1, sep_char => ",")};
  0            
226 0           $self->matrix($matrix);
227             }
228              
229 0           my @subprocs;
230 0           my $chunk_size = $self->files_per_subproc;
231 0     0     my $images = $self->images->map(sub { [@$_] }); #copy
  0            
232 0           while (my @chunk = splice @$images, 0, $chunk_size) {
233 0           push @subprocs, $self->_process_chunk_of_files(\@chunk);
234             }
235              
236 0           foreach my $s (@subprocs) {
237              
238             # Wait for the subprocess to finish
239 0           $s->wait;
240             }
241 0           $self->log->info('All subprocesses finished.');
242              
243             # Update image and thumbnail columns and write the new csv file
244 0           my $_processed = $self->_processed;
245 0           my $updated = 0;
246             $self->matrix->each(sub {
247 0 0   0     return unless $_->[1] =~ $FILETYPES;
248 0           for my $i (0 .. @$_processed - 1) {
249 0 0 0       if ($_->[1] eq $_processed->[$i][1] && ($_->[-1] ne $_processed->[$i][-1] || $_->[-2] ne $_processed->[$i][-2])) {
      0        
250 0           ($_) = splice @$_processed, $i, 1;
251 0           $updated++;
252 0           last;
253             }
254             }
255 0           });
256 0 0         if ($updated) {
257 0           csv(in => $matrix->to_array, enc => _U, out => \my $data, binary => 1, sep_char => ",");
258 0           path($csv_filepath)->spurt($data);
259 0           my $copied = path($csv_filepath)->copy_to($self->to_dir);
260 0           $self->log->info("$csv_filepath *updated* and copied to $copied");
261             }
262             else {
263 0           my $copied = path($csv_filepath)->copy_to($self->to_dir);
264 0           $self->log->info("$csv_filepath copied to $copied");
265             }
266 0           return $self;
267             }
268              
269 0     0     sub _process_chunk_of_files ($self, $files = []) {
  0            
  0            
  0            
270 0           my $log = $self->log;
271 0           my $from_dir = $self->from_dir;
272 0           my $to_dir = $self->to_dir;
273 0           my $imager = $self->imager;
274 0     0     Mojo::IOLoop->subprocess->run_p(sub($sub) {
  0            
  0            
275 0           my $processed = [];
276 0           for my $row (@$files) {
277 0           my $raw_path = $from_dir->child(encode _U, $row->[1]);
278              
279             # Check for calculated dimensions and calculate them if missing.
280 0 0 0       if (!$row->[-2] || !$row->[-1]) {
281 0           ($row->[-2], $row->[-1]) = $self->calculate_max_and_thumbs($row->[1], $raw_path);
282             }
283 0           my $to_path = $to_dir->child($row->[1])->dirname;
284 0           my $sized_path = $to_path->child($row->[-2])->to_string;
285 0           my $thumb_path = $to_path->child($row->[-1])->to_string;
286 0           my $html_path = "$sized_path.html";
287 0 0 0       if (-s $sized_path && -s $thumb_path && -s $html_path) {
      0        
288 0           $log->info("$row->[-2].html, $row->[-2] and $row->[-1] were already produced. Skipping ...");
289 0           push @$processed, $row;
290 0           next;
291             }
292 0           my (%sized, %thumb);
293 0           @sized{qw(xpixels ypixels)} = $row->[-2] =~ /_(\d+)x(\d+)\./;
294 0           @thumb{qw(xpixels ypixels)} = $row->[-1] =~ /_(\d+)x(\d+)\./;
295 0           my $img;
296 0 0         unless (-s $sized_path) {
297 0 0         unless (eval { $img = $imager->read(file => $raw_path) }) {
  0            
298 0           $log->warn(" !!! Skipping $row->[1]. Image error: " . $imager->errstr());
299 0           next;
300             }
301 0 0         unless (eval { $to_path->make_path({mode => 0711}); 1 }) {
  0            
  0            
302 0           $log->warn("!!! Skipping $row->[1]. Error: $@");
303 0           next;
304             }
305 0           my $maxi = $img->scale(%sized);
306 0           $maxi->settag(name => 'i_xres', value => 96);
307 0           $maxi->settag(name => 'i_yres', value => 96);
308 0 0         unless ($maxi->write(file => $sized_path)) {
309 0           $log->warn("!!! Cannot write image $sized_path!\nError:" . $maxi->errstr);
310             }
311             else {
312 0           $log->info("Written $sized_path");
313             }
314             }
315 0 0         unless (-s $thumb_path) {
316 0           my $thumbi = $img->scale(%thumb);
317 0           $thumbi->settag(name => 'i_xres', value => 96);
318 0           $thumbi->settag(name => 'i_yres', value => 96);
319 0 0         unless ($thumbi->write(file => $thumb_path)) {
320 0           $log->warn("!!! Cannot write image $thumb_path!\nError:" . $thumbi->errstr);
321             }
322             else {
323 0           $log->info("Written $thumb_path");
324             }
325             }
326              
327             # Generate single image html file for sharing on social medias
328 0           $self->render_obrazec_to_file($row, $html_path);
329 0           push @$processed, $row;
330             }
331 0           return $$, $processed;
332 0           })->then(
333 0     0     sub ($pid, $processed) {
  0            
  0            
334              
335             # Executed in the parent process where we can collect the results and write the
336             # new csv file, which can be saved in the $to_dir.
337             # TODO: think if this is needed or we can just copy the initially produced
338             # csv file to $to_dir.
339 0           $log->info("PID $pid processed " . (scalar @$processed) . ' images!');
340 0           push @{$self->_processed}, @$processed;
  0            
341 0           return;
342             }
343 0     0     )->catch(sub ($err) {
  0            
  0            
344 0 0         $log->warn("Subprocess error: $err") if $err;
345 0           });
346             }
347              
348 0     0     sub _do_html($self) {
  0            
  0            
349 0           state $app = $self->app;
350 0           my $categories = $self->categories;
351 0           my $processed = $self->_processed;
352 0           my $template_file = $self->template_file;
353 0   0       my $tpl = $template_file || 'obrazi.html';
354 0           my $css_file = 'obrazi.css';
355 0           my $js_file = 'obrazi.js';
356 0           my $vars = {
357             generator => __PACKAGE__,
358             categories => $categories,
359             processed => $processed,
360             app => $app,
361             thumbs => $self->thumbs,
362             css_file => $css_file,
363             js_file => $js_file,
364             linked => $self->_linked_images,
365             self => $self,
366             };
367              
368 0           $self->write_file($self->to_dir->child($css_file), $self->render_data($css_file => $vars));
369 0           $self->write_file($self->to_dir->child($js_file), $self->render_data($js_file => $vars));
370 0 0         if ($template_file) {
371              
372 0           my $html = Mojo::Template->new($self->template)->name($template_file)->render_file($template_file => $vars);
373 0           $self->to_dir->child(path($template_file)->basename =~ s/\.ep$//r)->spurt(encode _U, $html);
374             }
375             else {
376 0           $self->write_file($self->to_dir->child($tpl), encode _U, $self->render_data($tpl => $vars));
377             }
378              
379 0           return $self;
380             }
381              
382 0     0 0   sub render_obrazec_to_file ($self, $img, $html_path) {
  0            
  0            
  0            
  0            
383 0           state $obrazec_file = $self->obrazec_file;
384 0           state $obrazec_data = 'obrazec.html';
385 0   0       state $mt = Mojo::Template->new(vars => 1, name => $obrazec_file || $obrazec_data);
386 0           state $parsed = 0;
387 0           my $categories = $self->categories;
388              
389 0 0 0       if ($obrazec_file && !$parsed) {
    0 0        
390 0           $mt->parse(decode _U, path($obrazec_file)->slurp);
391 0           $parsed = 1;
392             }
393             elsif (!$obrazec_file && !$parsed) {
394 0           $mt->parse(data_section(ref $self, $obrazec_data));
395 0           $parsed = 1;
396             }
397              
398 0           my $html = $mt->process(
399             {
400             generator => __PACKAGE__,
401             self => $self,
402             app => $self->app,
403             img => $img,
404             categories => $categories,
405             linked => $self->_linked_images
406             }
407             );
408 0 0         path($html_path)->spurt(encode _U, $html) && $self->log->info("Written $html_path");
409 0           return;
410             }
411             1;
412              
413             =encoding utf8
414              
415             =head1 NAME
416              
417             Mojolicious::Command::Author::generate::obrazi - Обраꙁи for your site – a
418             gallery generator command
419              
420             =head1 SYNOPSIS
421              
422             Usage: APPLICATION generate obrazi [OPTIONS]
423              
424             Examples:
425             ./myapp.pl help generate obrazi # This help
426              
427             mojo generate obrazi --from ~/Pictures/summer-2021 \
428             --to /opt/myapp/public/summer-2021
429              
430             mojo generate obrazi --from ~/Pictures/summer-2021 \
431             --to /opt/some/static/site/albums/summer-2021 \
432             -x 800x600 -s 96x96 -t ./some/custom_template.html.ep
433              
434             Options:
435             -h, --help Show this summary of available options
436             -f, --from Root of directory structure from which the images
437             will be taken. Defaults to ./.
438             --to Root directory where the gallery will be put. Defaults to ./.
439             -x, --max Maximum image dimesnions in pixels in format 'widthxheight'.
440             Defaults to 1000x1000.
441             -s, --thumbs Thumbnails maximal dimensions. Defaults to 100x100 pixels.
442             -i, --index Name of the CSV index file to be generated and then read
443             in the --from directory.
444             -t, --template Path to template file. Defaults to embedded template
445             obrazi.html.
446             -o, --obrazec Path to single image template file for sharing on social
447             media. Defaults to embedded template obrazec.html
448             -u, --url Url where the gallery will be published.
449             Defaults to '/obrazi.html'.
450              
451             =head1 DESCRIPTION
452              
453             L generates a gallery from a
454             directory structure, containing images. The produced gallery is a static html
455             file which body content can be easily taken, modified, and embedded into a page
456             in any site.
457              
458             In addition the command generates a csv file on the first traversal of the
459             directory structure, describing the images. This file can be edited. Titles and
460             descriptions can be added for each image and then the command can be run again
461             to regenerate the gallery with the modified titles and descriptions. This file
462             can be further used by a helper — L to
463             produce and embed the HTML for the galery into a L application.
464             Please note that the helper is not yet implemented.
465              
466             The word B<обраꙁъ>(singular) means L
467             template, etc.|https://histdict.uni-sofia.bg/dictionary/show/d_05616>
468             in OCS/OS/OBG language. The name of the plugin is the plural variant in
469             nominative case (обраꙁи).
470              
471             =head1 WORKFLOW
472              
473             1. Images' owner and producer gives the direcory (probably zipped) to the
474             command runner (a human yet).
475              
476             2. The runner runs the command as shown in the SYNOPSIS.
477              
478             3. The runner gives back the produced csv file to the images' producer. Fixes
479             problems with ICC profiles etc. Notifies the producer for eventual naming
480             convetions, possible problems. The producer fills in the description and titles
481             in the comfort of L
482             Calc|https://www.libreoffice.org/discover/calc/> or MS Excel and returns the
483             file to the command-runner. This may take some time.
484              
485             4. The runner runs again the command with the modified csv file, reviews the
486             produced file. Takes the HTML and puts it in a page on the Web.
487              
488             5. The images' owner/producer enjoys the gallery, prise him/herself with it or
489             goes back to the runner to report problems.
490              
491             6. DONE or go to some of the previous steps.
492              
493             =head1 FEATURES
494              
495             Recursively traverses subdirectories and scales images (four at a time) to
496             given width and height and produces thumbnails for those images. Thumbnail
497             sizes can also be set.
498              
499             Produces an index CSV file which can be edited to add titles and descriptions
500             for the images.
501              
502             Produces an HTML file with fully functional lightbox-like gallery, implemented
503             only using jQuery and CSS – no jQ-plugins. Left/right keyboard buttons navigation
504             to next and previous image.
505              
506             =head1 ATTRIBUTES
507              
508             L inherits all attributes from
509             L and implements the following new ones.
510              
511             =head2 categories
512              
513             my $cat = $self->categories->first(sub { $_->[0] eq $img->[0] });
514              
515             A L instance, containing rows from the CSV file which are
516             categories (directories). For this attribute to return meaningful data,
517             C<$self-Ematrix> must be already filled in from CSV file.
518              
519              
520             =head2 csv_filename
521              
522             my $filename = $self->csv_filename; # index.csv
523             my $обраꙁи = $self->csv_filename('gallery.csv');
524              
525             The name of the CSV file which will be created in L. This file,
526             after being edited and after the images are processed, will be copied together
527             with the images to L. Defaults to C. Can be passed on the
528             command-line via the C<--index> argument.
529              
530             =head2 defaults
531              
532             my $defaults_hashref = $обраꙁи->defaults;
533             $обраꙁи->defaults->{category_title} = 'Def. Cat. title';
534             $обраꙁи->defaults->{category_description} = 'Def. Cat. description.';
535             $обраꙁи->defaults->{image_title} = 'Def. Image Title';
536             $обраꙁи->defaults->{image_description} = 'Def. Image description.';
537             $обраꙁи->defaults->{author} = 'John Smith';
538              
539             These values go to the folowing columns in the produced CSV file. C
540             description, author>. They are supposed to be replaced by editing the produced
541             file. TODO: Maybe allow these to be passed on the command line via an argument
542             C<--defaults>.
543              
544             =head2 description
545              
546             my $description = $обраꙁи->description;
547             $self = $обраꙁи->description('Foo');
548              
549             Short description of this command, used for the application's command list.
550              
551             =head2 files_per_subproc
552              
553             my $files_num = $обраꙁи->files_per_subproc;
554             $self = $обраꙁи->files_per_subproc(10);
555              
556             Number of files to be processed by one subprocess. Defaults to
557             Csubprocs_num) +1>. The last chunk of files is
558             the remainder — usually smaller than the previous chunks.
559              
560             =head2 from_dir
561              
562             $self = $обраꙁи->from_dir(path('./'));
563             my $root_folder_abs_path = $обраꙁи->from_dir;
564              
565             Holds a L instance — absolute path to the directory from which the
566             pictures will be taken. This is where the CSV file describing the directory
567             structure will be generated too. The value is taken from the commandline
568             argument C<--from_dir>. Defaults to C<./> — current directory — where the
569             command is executed.
570              
571             =head2 imager
572              
573             my $img = $обраꙁи->imager->read(file=>'path/to/image.jpg')
574             || die $обраꙁи->imager->errstr;
575              
576             my $self = $обраꙁи->imager(Imager->new);
577              
578             An L instance. This is the images-processing engine, used by the
579             command.
580              
581             =head2 images
582              
583             my $images = $self->images->map(sub { [@$_] }); #copy
584              
585             A L instance, containing rows from the CSV file which are
586             image files, supported by L and will be processed. For this attribute
587             to return meaningful data, C<$self-Ematrix> must be already filled in from
588             CSV file.
589              
590             =head2 log
591              
592             my $log = $self->log;
593             my $self = $self->log(Mojo::Log->new)
594              
595             A L instance. By default it is not the same as
596             C<$self-Eapp-Elog>. Used to output info, warnings and errors to the
597             terminal or the application log.
598              
599             =head2 matrix
600              
601             my $matrix = $self->matrix;
602              
603             # add an image
604             push @$matrix,
605             [
606             $category, $path,
607             $defaults->{image_title}, $defaults->{image_description},
608             $defaults->{author}, $self->calculate_max_and_thumbs($path, $raw_path)
609             ];
610              
611             # add a category
612             push @$matrix, [
613             $category, $path, "$defaults->{category_title} – $category",
614             $defaults->{category_description}, $defaults->{author}, '', ''
615             ];
616              
617             $matrix->each(sub{...});
618              
619              
620             csv(in => $matrix->to_array, enc => 'UTF-8', out => \my $data, binary => 1, sep_char => ",");
621             path($csv_filepath)->spurt($data);
622              
623             A L instance. First row contains the headers. This matrix is
624             filled in while recursively searching in the L for images. Then it
625             is dumped into the index CSV file. If the CSV file is already present, the data
626             is read directly from it.
627              
628             =head2 max
629              
630             my $max_sizes = $self->max; # {width => 1000, height => 1000}
631             $self = $self->max({width => 1000, height => 1000});
632             $self = $self->max('1000x1000');
633              
634             A hash reference with keys C and C. Defaults to C<{width =>
635             1000, height => 1000}>. Can be changed via the command line argument C<--max>.
636              
637             =head2 obrazec_file
638              
639             Path to template file for single html pages. Defaults to embedded template
640             obrazec.html. Can be passed as argument on the command-line via C<--obrazec>.
641              
642             =head2 publish_url
643              
644             $обраꙁи->publish_url($string); # $self
645             $обраꙁи->publish_url; # $string
646              
647             String. Url path or preferably full url, where the gallery will reside. Needed
648             for link from individual images to the common gallery page and OpenGraph meta
649             data. Can be passed as argument on the command-line via C<--url>. Defaults
650             to C – rarely what you need.
651              
652             =head2 subprocs_num
653              
654             $self->subprocs_num; #4
655             $self = $self->subprocs_num(5);
656              
657             Integer, used to split the number of files found into equal chunks, each of
658             which will be processed in a separate subprocess in parallel. Defaults to 4.
659             See also L.
660              
661             =head2 template_file
662              
663             my $self = $self->template_file('path/to/template.html.ep');
664             my $tpl = $self->template_file;
665              
666             Path to template file to be used for generating the HTML for the gallery.
667             Defaults to embedded template obrazi.html. Can be passed as argument on the
668             command-line via C<--template>.
669              
670             =head2 thumbs
671              
672             my $thumbs_sizes = $self->thumbs; # {width => 100, height => 100}
673             $self = $self->thumbs({width => 100, height => 100});
674             $self = $self->thumbs('1000x1000');
675              
676             A hash reference with keys C and C. Defaults to C<{width =>
677             100, height => 100}>. Can be changed via the command line argument
678             C<--thumbs>.
679              
680             =head2 to_dir
681              
682             $self->to_dir # path($app/public)
683             $self = $self->to_dir(path('/some/folder'));
684              
685             A L instance. Directory, where the folder with the processed images
686             will be put. Defaults to the C forlder of the current application.
687             Can be changed via the command line argument C<--to_dir>. It is recommended to
688             pass this value unless all your images are into one root folder. The L
689             directory is the root of your images' galery.
690              
691             =head2 usage
692              
693             my $usage = $обраꙁи->usage;
694             $self = $обраꙁи->usage('Foo');
695              
696             Usage information for this command, used for the help screen. At the bottom are
697             shown the supported by L image formats.
698              
699             =head1 METHODS
700              
701             L inherits all methods from
702             L and implements the following new ones.
703              
704             =head2 calculate_max_and_thumbs
705              
706             # img_1000x1000.jpg, img_100x100.jpg
707             my ($img_filename, $thumb_filename) = $self->calculate_max_and_thumbs($decoded_path, $raw_path);
708              
709             Calculates the resized image dimensions according to the C<$self-Emax>
710             and C<$self-Ethumbs> gallery constraints. Accepts the utf8 decoded path
711             and the raw path to the file to be worked on. Returns two empty strings if
712             there is error reading the image and warns about the error. Returns filenames
713             for the resized image and the thumbnail image. See also
714             L.
715              
716             =head2 run
717              
718             $обраꙁи = $обраꙁи->run(@ARGV);
719              
720             Run this command.
721              
722             =head2 TEMPLATES
723              
724             L contains four embedded
725             templates:
726              
727             obrazi.html — template for a single page gallery, containing all images
728             obrazec.html — template for single image html pages
729             obrazi.css — template for CSS rules used in both html templates
730             obrazi.js — JavaScript (jQuery) code for handling navigation and effecs
731             in obrazi.html
732              
733             TODO: Maybe make the templates inflatable.
734              
735             =head1 DEMOS
736              
737             Here is a hopefully growing list of URLs of galleries produced with this
738             software.
739              
740             L<Творби на Марио Беров|https://слово.бг/хѫдожьство/mario_berov.bg.html> — a
741             gallery, presenting works of the Bulgarian Orthodox icon painter Mario Berov.
742              
743             =head1 SEE ALSO
744              
745             L, L
746             L, L, L,
747              
748             L,
749              
750             L.
751              
752             =cut
753              
754             __DATA__