File Coverage

lib/Data/URIID/Barcode.pm
Criterion Covered Total %
statement 57 223 25.5
branch 13 116 11.2
condition 9 69 13.0
subroutine 10 17 58.8
pod 9 9 100.0
total 98 434 22.5


line stmt bran cond sub pod time code
1             # Copyright (c) 2025 Philipp Schafft
2              
3             # licensed under Artistic License 2.0 (see LICENSE file)
4              
5             # ABSTRACT: Extractor for identifiers from URIs
6              
7             package Data::URIID::Barcode;
8              
9 2     2   378866 use v5.16;
  2         7  
10 2     2   11 use strict;
  2         2  
  2         87  
11 2     2   14 use warnings;
  2         2  
  2         117  
12              
13 2     2   9 use Carp;
  2         4  
  2         209  
14 2     2   12 use Scalar::Util qw(weaken);
  2         2  
  2         122  
15              
16             our $VERSION = v0.20;
17              
18 2     2   9 use parent 'Data::URIID::Base';
  2         3  
  2         926  
19              
20 2     2   251 use constant {map {$_ => []} qw(TYPE_UNKNOWN TYPE_OTHER TYPE_QRCODE TYPE_EAN13 TYPE_EAN8)};
  2         4  
  2         13  
  10         5661  
21              
22             my %_type_info = (
23             TYPE_UNKNOWN() => {
24             type => TYPE_UNKNOWN,
25             special => 1,
26             },
27             TYPE_OTHER() => {
28             type => TYPE_OTHER,
29             special => 1,
30             },
31             TYPE_QRCODE() => {
32             type => TYPE_QRCODE,
33             aliases => [qw(qrcode qr-code)],
34             },
35             TYPE_EAN13() => {
36             type => TYPE_EAN13,
37             aliases => [qw(ean13 ean-13)],
38             },
39             TYPE_EAN8() => {
40             type => TYPE_EAN8,
41             aliases => [qw(ean8 ean-8)],
42             },
43             );
44              
45              
46              
47             sub sheet {
48 0     0 1 0 my ($pkg, %opts) = @_;
49 0         0 my $from = delete $opts{from};
50 0         0 my $filename = delete $opts{filename};
51 0         0 my $template = delete $opts{template};
52 0         0 my $values = delete $opts{values};
53 0         0 my $filter_type = delete $opts{filter_type};
54 0         0 my $filter_data = delete $opts{filter_data};
55 0         0 my %pass_opts;
56             my @res;
57 0         0 my $done;
58              
59 0         0 foreach my $key (qw(extractor type)) {
60 0   0     0 $pass_opts{$key} = delete $opts{$key} // next;;
61             }
62              
63 0 0 0     0 if (!defined($from) && defined($values)) {
    0 0        
64 0         0 @res = map {{barcode => $_, quality => 0.001}}
65 0 0 0     0 map {$pkg->new(%pass_opts, ref($_) ? (from => $_) : (data => sprintf($template // '%s', $_)))}
66 0         0 @{$values};
  0         0  
67 0         0 $done = 1;
68             } elsif (!defined($from) && defined($filename)) {
69 0         0 require Image::Magick;
70 0         0 $from = Image::Magick->new();
71 0 0       0 $from->Read($filename) && croak 'Cannot read file';
72             }
73              
74 0 0       0 croak 'Stray options passed' if scalar keys %opts;
75              
76 0 0       0 unless ($done) {
77 0 0       0 croak 'No from given' unless defined $from;
78              
79 0 0       0 if ($from->isa('Image::Magick')) {
80 0         0 require Barcode::ZBar;
81              
82 0         0 my $raw = $from->ImageToBlob(magick => 'GRAY', depth => 8);
83 0         0 my ($col, $rows) = $from->Get(qw(columns rows));
84 0         0 my $scanner = Barcode::ZBar::ImageScanner->new();
85              
86 0         0 $from = Barcode::ZBar::Image->new();
87 0         0 $from->set_format('Y800');
88 0         0 $from->set_size($col, $rows);
89 0         0 $from->set_data($raw);
90              
91 0         0 $scanner->parse_config("enable");
92              
93 0         0 $scanner->scan_image($from);
94             }
95              
96 0 0       0 if ($from->isa('Barcode::ZBar::Image')) {
97 0         0 my $max_quality;
98              
99 0         0 foreach my $symbol ($from->get_symbols()) {
100 0         0 my $raw_type = $symbol->get_type;
101 0         0 my $raw_data = $symbol->get_data;
102 0         0 my $raw_quality = $symbol->get_quality;
103 0         0 my $type;
104              
105 0 0       0 if ($raw_type eq $symbol->QRCODE) {
    0          
    0          
106 0         0 $type = TYPE_QRCODE;
107             } elsif ($raw_type eq $symbol->EAN13) {
108 0         0 $type = TYPE_EAN13;
109             } elsif ($raw_type eq $symbol->EAN8) {
110 0         0 $type = TYPE_EAN8;
111             }
112              
113 0   0     0 $type //= TYPE_OTHER;
114              
115 0 0 0     0 $max_quality = $raw_quality if !defined($max_quality) || $max_quality < $raw_quality;
116 0         0 push(@res, {
117             barcode => $pkg->new(%pass_opts, type => $type, data => $raw_data),
118             _raw_ => $symbol,
119             });
120             }
121              
122 0         0 foreach my $res (@res) {
123 0         0 my $symbol = delete $res->{_raw_};
124 0         0 $res->{quality} = $symbol->get_quality / $max_quality;
125             }
126             } else {
127 0         0 croak 'From of invalid/unsupported type';
128             }
129             }
130              
131 0 0       0 if (defined $filter_type) {
132 0         0 @res = grep {$_->{barcode}->has_type($filter_type)} @res;
  0         0  
133             }
134              
135 0 0       0 if (defined $filter_data) {
136 0 0       0 if (ref($filter_data) eq 'CODE') {
137 0         0 @res = grep {$filter_data->($_->{barcode}->{data})} @res;
  0         0  
138             } else {
139 0         0 @res = grep {$_->{barcode}->{data} =~ $filter_data} @res;
  0         0  
140             }
141             }
142              
143 0 0       0 if (wantarray) {
144 0         0 return map {$_->{barcode}} @res;
  0         0  
145             } else {
146 0         0 my $max_length;
147              
148 0 0       0 croak 'No code found' unless scalar @res;
149              
150 0         0 foreach my $res (@res) {
151 0         0 my $barcode = $res->{barcode};
152 0         0 my $length = length($barcode->data);
153 0 0 0     0 $max_length = $length if !defined($max_length) || $max_length < $length;
154             }
155              
156 0         0 foreach my $res (@res) {
157 0         0 my $barcode = $res->{barcode};
158 0         0 $res->{quality} *= $barcode->_quality_by_type * (length($barcode->data) / $max_length);
159             }
160              
161 0         0 return (sort {$b->{quality} <=> $a->{quality}} @res)[0]{barcode};
  0         0  
162             }
163             }
164              
165              
166             sub new {
167 1     1 1 6 my ($pkg, %opts) = @_;
168 1         2 my __PACKAGE__ $self;
169              
170 1 50       5 if (defined(my $from = delete($opts{from}))) {
171 0         0 $self = eval {$pkg->sheet(from => $from)};
  0         0  
172 0 0       0 return $self if defined $self;
173              
174 0 0       0 if (eval {$from->isa('Data::URIID::Base')}) {
  0         0  
175 0   0     0 $opts{extractor} //= $from->extractor(default => undef);
176             }
177              
178 0 0       0 if (eval {$from->isa('Data::URIID::Result')}) {
  0 0       0  
    0          
    0          
179 0   0     0 $opts{data} //= $from->url->as_string;
180 0   0     0 $opts{type} //= TYPE_QRCODE;
181 0         0 } elsif (eval {$from->isa('Data::URIID::Base')}) {
182 0   0     0 $opts{data} //= $from->ise;
183 0   0     0 $opts{type} //= TYPE_QRCODE;
184 0         0 } elsif (eval {$from->isa('Data::Identifier')}) {
185 0   0     0 $opts{data} //= $from->ise;
186 0   0     0 $opts{type} //= TYPE_QRCODE;
187 0         0 } elsif (eval {$from->isa('URI')}) {
188 0   0     0 $opts{data} //= $from->as_string;
189 0   0     0 $opts{type} //= TYPE_QRCODE;
190             } else {
191 0         0 croak 'Unsupported/invalid from type';
192             }
193             }
194              
195 1 50       4 croak 'No type given' unless defined $opts{type};
196 1 50       5 croak 'No data given' unless defined $opts{data};
197              
198 1         3 weaken($opts{extractor});
199              
200 1         4 $self = bless \%opts, $pkg;
201              
202 1         4 return $self;
203             }
204              
205              
206             sub data {
207 0     0 1 0 my ($self, %opts) = @_;
208 0         0 delete $opts{default};
209 0         0 delete $opts{no_defaults};
210              
211 0 0       0 croak 'Stray options passed' if scalar keys %opts;
212              
213 0         0 return $self->{data};
214             }
215              
216              
217             sub type {
218 2     2 1 506 my ($self, %opts) = @_;
219 2         5 delete $opts{default};
220 2         3 delete $opts{no_defaults};
221              
222 2 50       7 croak 'Stray options passed' if scalar keys %opts;
223              
224 2         14 return $self->{type};
225             }
226              
227              
228             sub has_type {
229 0     0 1 0 my ($self, $type, %opts) = @_;
230 0         0 delete $opts{default};
231 0         0 delete $opts{no_defaults};
232              
233 0 0       0 croak 'Stray options passed' if scalar keys %opts;
234 0 0       0 croak 'No type passed' unless defined $type;
235              
236 0 0 0     0 if (ref($type) && !exists $_type_info{$type}) {
237 0         0 foreach my $t (@{$type}) {
  0         0  
238 0 0       0 return 1 if $self->{type} == $t;
239             }
240             }
241              
242 0         0 return $self->{type} == $type;
243             }
244              
245              
246             sub render {
247 0     0 1 0 my ($self, %opts) = @_;
248 0         0 my $filename = delete $opts{filename};
249 0         0 my $success;
250              
251 0 0       0 croak 'Stray options passed' if scalar keys %opts;
252              
253 0         0 eval {
254 0 0       0 if ($self->has_type(TYPE_QRCODE)) {
255 0         0 require Imager::QRCode;
256              
257 0         0 my $qrcode = Imager::QRCode->new(level => 'H');
258 0         0 my $img = $qrcode->plot($self->data);
259 0         0 $img->write(file => $filename, type => 'png');
260 0         0 $success = 1;
261             }
262             };
263              
264 0 0       0 unless ($success) {
265 0         0 eval {
266 0         0 require GD::Barcode;
267              
268 0         0 my $type;
269              
270 0 0       0 if ($self->has_type(TYPE_QRCODE)) {
    0          
    0          
271 0         0 $type = 'QRcode';
272             } elsif ($self->has_type(TYPE_EAN13)) {
273 0         0 $type = 'EAN13';
274             } elsif ($self->has_type(TYPE_EAN8)) {
275 0         0 $type = 'EAN8';
276             }
277              
278 0 0       0 if (defined $type) {
279 0         0 my $code = GD::Barcode->new($type => $self->data);
280 0         0 my $plot = $code->plot;
281 0         0 my ($width, $height) = $plot->getBounds();
282 0         0 my $image = GD::Image->new($width * 3, $height * 3, 0);
283              
284 0         0 $image->copyResized($plot, 0, 0, 0, 0, $width * 3, $height * 3, $width, $height);
285              
286 0 0       0 open(my $out, '>', $filename) or croak $!;
287 0         0 $out->binmode;
288 0         0 $out->print($image->png);
289 0         0 $success = 1;
290             }
291             };
292             }
293              
294 0 0       0 unless ($success) {
295 0         0 croak 'Code not supported';
296             }
297             }
298              
299              
300             sub type_info {
301 5     5 1 763 my ($self, @args) = @_;
302 5         14 state $aliases;
303 5         9 my @ret;
304              
305 5 100       15 unless (defined $aliases) {
306 1         2 $aliases = {};
307 1         4 foreach my $info (values %_type_info) {
308 5         9 my $type = $info->{type};
309 5   100     14 my $list = $info->{aliases} //= [];
310 5         6 $aliases->{fc($_)} = $type foreach @{$list};
  5         16  
311              
312 5   100     15 $info->{special} //= undef;
313             }
314             }
315              
316 5 100 100     18 if (!scalar(@args) && ref($self)) {
317 1         4 @args = ($self->type);
318             }
319              
320 5 100       12 if (!scalar(@args)) {
321 1         3 @args = keys %_type_info;
322             }
323              
324 5         10 foreach my $arg (@args) {
325 11   33     29 my $info = $_type_info{$arg} // $aliases->{fc($arg)} // croak 'No such type: '.$arg;
      33        
326 11         14 push(@ret, {%{$info}});
  11         40  
327             }
328              
329 5 100       10 if (wantarray) {
330 2         17 return @ret;
331             } else {
332 3 50       8 croak 'Wrong number of results for scalar context' unless scalar(@ret) == 1;
333 3         25 return $ret[0];
334             }
335             }
336              
337              
338             # --- Overrides for Data::URIID::Base ---
339             sub ise {
340 0     0 1   my ($self, @args) = @_;
341              
342 0 0         unless (exists $self->{ise}) {
343 0   0       $self->{result} //= eval {$self->_as_lookup([$self])};
  0            
344              
345 0 0         if (defined $self->{result}) {
346 0           $self->{ise} = $self->{result}->ise;
347             } else {
348             # we have no extractor, still try a few basic things:
349 0 0         if ($self->has_type(TYPE_QRCODE)) {
350 0           my $data = $self->data;
351              
352 0 0 0       if ($data =~ /^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$/ || $data =~ /^[1-3](?:\.(?:0|[1-9][0-9]*))+$/) {
    0          
353 0           $self->{ise} = lc($data);
354             } elsif ($data =~ m#^https://uriid\.org/#) {
355 0           $self->{ise} = eval {Data::Identifier->new(ise => $data)->ise};
  0            
356             }
357             }
358             }
359             }
360              
361 0           return $self->SUPER::ise(@args);
362             }
363              
364             sub displayname {
365 0     0 1   my ($self, %args) = @_;
366              
367 0 0         unless (exists $self->{displayname}) {
368 0           $self->{displayname} = undef; # break any loops.
369              
370 0           eval { $self->ise(%args) }; # preload objects.
  0            
371              
372 0 0 0       $self->{displayname} //= $self->{result}->displayname(%args) if defined $self->{result};
373 0 0 0       $self->{displayname} //= $self->as('Data::Identifier')->displayname(%args) if defined $self->{ise};
374             }
375              
376 0 0         return $self->{displayname} if defined $self->{displayname};
377              
378 0           return $self->SUPER::displayname(%args);
379             }
380              
381             # ---- Private helpers ----
382              
383             sub _quality_by_type {
384 0     0     my ($self) = @_;
385              
386 0 0         return 1 if $self->has_type(TYPE_QRCODE);
387 0           return 0.01;
388             }
389              
390             1;
391              
392             __END__
393              
394             =pod
395              
396             =encoding UTF-8
397              
398             =head1 NAME
399              
400             Data::URIID::Barcode - Extractor for identifiers from URIs
401              
402             =head1 VERSION
403              
404             version v0.20
405              
406             =head1 SYNOPSIS
407              
408             use Data::URIID::Barcode;
409              
410             my Data::URIID::Barcode $barcode = Data::URIID::Barcode->new(type => ..., data => ..., [ %opts ] );
411             # or:
412             my Data::URIID::Barcode $barcode = Data::URIID::Barcode->new(from => ..., [ %opts ] );
413              
414             This module represents a single barcode.
415              
416             This package inherits from L<Data::URIID::Base>.
417              
418             =head1 METHODS
419              
420             =head2 sheet
421              
422             my @barcodes = Data::URIID::Barcode->sheet(%opts);
423             # or:
424             my $barcode = Data::URIID::Barcode->sheet(%opts);
425              
426             # e.g.:
427             my @barcodes = Data::URIID::Barcode->sheet(filename => 'bla.jpg');
428             # or:
429             my @barcodes = Data::URIID::Barcode->sheet(type => Data::URIID::Barcode->TYPE_QRCODE, template => 'IDX-%03u', values => [0 .. 9]);
430              
431             Creates a set of barcode objects from a sheet.
432              
433             When called in scalar context returns the best result (best for a metric not further defined, which may also change in later versions)
434             or C<die>s if none was found.
435              
436             B<Experimental:>
437             This method is currently experimental. It might change at any time.
438              
439             The following options are supported:
440              
441             =over
442              
443             =item C<filename>
444              
445             A file to read from.
446              
447             =item C<filter_type>
448              
449             Filters the barcodes based on their type.
450             This takes the same values as L</has_type>.
451              
452             This value might also be used to hint any scanners.
453              
454             =item C<filter_data>
455              
456             Filters the barcodes based on their data.
457             This is a regex or a function (coderef).
458              
459             If it's a function the data of the barcode is passed as first argument.
460             All other arguments are undefined by this version and later versions may define values for them.
461              
462             =item C<from>
463              
464             A perl object to use.
465             If given C<values> must be C<undef>.
466              
467             =item C<values>
468              
469             A list (arrayref) of values to be used as data barcodes to be generated.
470             If given C<from> must be C<undef>.
471              
472             =item C<template>
473              
474             A template (see L<perlfunc/sprintf>) that is applied to each value in C<values>.
475              
476             Defaults to no transformation.
477             If defined, must not be used with values that are references.
478              
479             =item C<type>
480              
481             The type of the barcode to be used with C<values>.
482              
483             =back
484              
485             =head2 new
486              
487             my Data::URIID::Barcode $barcode = Data::URIID::Barcode->new(type => ..., data => ..., [ %opts ] );
488             # or:
489             my Data::URIID::Barcode $barcode = Data::URIID::Barcode->new(from => ..., [ %opts ] );
490              
491             This method creates a new barcode object.
492              
493             The following options are supported:
494              
495             =over
496              
497             =item C<data>
498              
499             The raw data of the barcode.
500              
501             =item C<extractor>
502              
503             optionally, an instance of L<Data::URIID>.
504              
505             =item C<from>
506              
507             optionally, an instance of another object to read the values from.
508             Depending on the given object the non-optional values might become optional.
509              
510             Currently the following types are supported:
511             L<Data::URIID::Base>,
512             L<Data::Identifier>,
513             L<URI>.
514             Other types might be supported as well.
515              
516             =item C<type>
517              
518             The type of the barcode. One of C<TYPE_*>.
519             Future versions of this module might improve this definition.
520              
521             =back
522              
523             =head2 data
524              
525             my $data = $barcode->data;
526              
527             Returns the data of the barcode.
528              
529             The returned value might differ from the value passed to L</new> as it might have been normalised, decoded (character set), or otherwise altered.
530              
531             No options are supported. However the options C<default>, and C<no_defaults> are ignored.
532              
533             =head2 type
534              
535             my $type = $barcode->type;
536              
537             Returns the type of the barcode.
538              
539             The returned value might differ from the value passed to L</new> as it might have been normalised, replaced with a cached reference, or otherwise altered.
540              
541             No options are supported. However the options C<default>, and C<no_defaults> are ignored.
542              
543             See L</has_type> for a more convenient method.
544              
545             =head2 has_type
546              
547             my $bool = $barcode->has_type(Data::URIID::Barcode->TYPE_*);
548             # or:
549             my $bool = $barcode->has_type([Data::URIID::Barcode->TYPE_*, ...]);
550              
551             Returns whether or not this barcode is of the given type.
552              
553             If the type is given as an arrayref then it is checked if the type matches any of the elements.
554              
555             No options are supported. However the options C<default>, and C<no_defaults> are ignored.
556              
557             =head2 render
558              
559             $barcode->render(filename => ...);
560              
561             Render the barcode as a image file.
562              
563             B<Experimental:>
564             This method is experimental. It may change completly or may be removed on future versions.
565              
566             B<Note:>
567             Currently this method exports as PNG. Later versions might support other formats.
568              
569             =head2 type_info
570              
571             my @info = Data::URIID::Barcode->type_info;
572             # or:
573             my @info = Data::URIID::Barcode->type_info($type0, $type1, ...);
574             # or:
575             my $info = Data::URIID::Barcode->type_info($type);
576             # or:
577             my $info = $barcode->type_info;
578              
579             Returns information on a barcode type.
580             If called in list context returns a list.
581             If called in scalar context returns the only one result (or C<die>s if there is not exactly one result).
582              
583             Takes a list of C<TYPE_*> constants as arguments.
584             If the provided value is not a C<TYPE_*> constant the value is checked against an internal alias list.
585             If no types are given, returns information for all known types (if called on the package) or
586             for the type of the current barcode (if called on an instance).
587              
588             Each element returned is an hash reference containing the following keys:
589              
590             =over
591              
592             =item C<type>
593              
594             The value of the C<TYPE_*> constant.
595              
596             =item C<special>
597              
598             Whether the type is a special one (not a real barcode type).
599             Such include C<TYPE_OTHER>, and C<TYPE_UNKNOWN>.
600             Later versions of this module might add more special types.
601              
602             =item C<aliases>
603              
604             An arrayref with alias names for the given type.
605             B<Note:> This list might be empty.
606              
607             =back
608              
609             =head1 TYPES
610              
611             This module supports a number of types of barcodes.
612              
613             B<Note:>
614             This module does not define the type or value the C<TYPE_*> constants have.
615             Future versions of this module might change this at any release.
616             Always use the type constants.
617              
618             =head2 TYPE_UNKNOWN
619              
620             The type of the barcode is unknown.
621             This might be used if e.g. the scanner software does not tell.
622             However this limits the set of features this module can provide.
623              
624             =head2 TYPE_OTHER
625              
626             The type of barcode is known, but not supported by this module.
627             Future versions of this module might implement the given type.
628              
629             =head2 TYPE_QRCODE
630              
631             A QR-Code.
632              
633             =head2 TYPE_EAN13
634              
635             A EAN-13 code commonly found on products.
636              
637             =head2 TYPE_EAN8
638              
639             A EAN-8 code commonly found on products in small packages.
640              
641             =head1 AUTHOR
642              
643             Philipp Schafft <lion@cpan.org>
644              
645             =head1 COPYRIGHT AND LICENSE
646              
647             This software is Copyright (c) 2023-2025 by Philipp Schafft <lion@cpan.org>.
648              
649             This is free software, licensed under:
650              
651             The Artistic License 2.0 (GPL Compatible)
652              
653             =cut