File Coverage

blib/lib/Image/Base/Magick.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             # Copyright 2010, 2011, 2012, 2017 Kevin Ryde
2              
3             # This file is part of Image-Base-Magick.
4             #
5             # Image-Base-Magick is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Image-Base-Magick is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Image-Base-Magick. If not, see .
17              
18              
19             # file:///usr/share/doc/imagemagick-doc/www/perl-magick.html
20             # file:///usr/share/doc/imagemagick-doc/www/formats.html
21              
22             require 5;
23             package Image::Base::Magick;
24 2     2   4110 use strict;
  2         5  
  2         47  
25 2     2   8 use Carp;
  2         4  
  2         93  
26 2     2   9 use Fcntl;
  2         6  
  2         346  
27 2     2   1364 use Image::Magick;
  0            
  0            
28             use vars '$VERSION', '@ISA';
29              
30             use Image::Base;
31             @ISA = ('Image::Base');
32              
33             $VERSION = 5;
34              
35             # uncomment this to run the ### lines
36             #use Smart::Comments '###';
37              
38              
39             sub new {
40             my ($class, %params) = @_;
41             ### Image-Base-Magick new(): %params
42             my $err;
43              
44             # $obj->new(...) means make a copy, with some extra settings
45             if (ref $class) {
46             my $self = $class;
47             $class = ref $self;
48             if (! defined $params{'-imagemagick'}) {
49             $params{'-imagemagick'} = $self->get('-imagemagick')->Clone;
50             }
51             # inherit everything else
52             %params = (%$self, %params);
53             ### copy params: \%params
54             }
55              
56             if (! defined $params{'-imagemagick'}) {
57             # Crib: passing attributes to new() is the same as a subsequent set()
58             # except you don't get an error return from new()
59             my $m = $params{'-imagemagick'} = Image::Magick->new;
60              
61             # must apply -width, -height as "size" before ReadImage()
62             if (exists $params{'-width'} || exists $params{'-height'}) {
63             my $width = delete $params{'-width'} || 0;
64             my $height = delete $params{'-height'} || 0;
65             ### Set(size) -width,-height: "${width}x${height}"
66             if ($err = $m->Set (size => "${width}x${height}")) {
67             croak $err;
68             }
69             }
70             ### ReadImage xc-black
71             if ($err = $m->ReadImage('xc:black')) {
72             croak $err;
73             }
74             }
75             my $self = bless {}, $class;
76             $self->set (%params);
77              
78             if (defined $params{'-file'}) {
79             $self->load;
80             }
81              
82             ### new made: $self
83             return $self;
84             }
85              
86             # "size" is the size of the canvas
87             # "width" and "height" are the size of a ReadImage() file, or something
88             # file:///usr/share/doc/imagemagick/www/perl-magick.html#get-attribute
89             #
90             sub _magic_get_width {
91             my ($m, $idx) = @_;
92             my $size;
93             if (defined ($size = $m->Get('size'))) {
94             # ### $size
95             # ### split: [ split /x/, $size ]
96             # ### return: (split /x/, $size)[$idx||0]
97             return (split /x/, $size)[$idx||0];
98             } else {
99             return 0;
100             }
101             }
102             sub _magic_get_height {
103             my ($m) = @_;
104             _magic_get_width ($m, 1);
105             }
106             my %attr_to_get_func = (-width => \&_magic_get_width,
107             -height => \&_magic_get_height,
108             );
109             my %attr_to_GetSet = (-file => 'filename',
110             # these not documented yet ...
111             -ncolours => 'colors',
112             -file_format => 'magick',
113             );
114             sub _get {
115             my ($self, $key) = @_;
116             ### Image-Base-Magick _get(): $key
117              
118             my $m = $self->{'-imagemagick'};
119             {
120             my $func;
121             if ($func = $attr_to_get_func{$key}) {
122             return &$func($m);
123             }
124             }
125             {
126             my $attribute;
127             if ($attribute = $attr_to_GetSet{$key}) {
128             ### Get: $attribute
129             ### is: $m->Get($attribute)
130             return $m->Get($attribute);
131             }
132             }
133             return $self->SUPER::_get ($key);
134             }
135              
136             sub set {
137             my ($self, %params) = @_;
138             ### Image-Base-Magick set(): \%params
139              
140             {
141             my $key;
142             foreach $key ('-ncolours') {
143             if (exists $params{$key}) {
144             croak "Attribute $key is read-only";
145             }
146             }
147             }
148              
149             # apply this first
150             {
151             my $m;
152             if ($m = delete $params{'-imagemagick'}) {
153             $self->{'-imagemagick'} = $m;
154             }
155             }
156              
157             my $m = $self->{'-imagemagick'};
158             my @set;
159              
160             if (exists $params{'-width'} || exists $params{'-height'}) {
161             # FIXME: might prefer a crop on shrink, and some sort of extend-only on
162             # grow
163              
164             my @resize;
165             my $width = delete $params{'-width'};
166             if (defined $width && $width != _magic_get_width($m)) {
167             push @resize, width => $width;
168             }
169             my $height = delete $params{'-height'};
170             if (defined $height && $height != _magic_get_height($m)) {
171             push @resize, height => $height;
172             }
173             # my $width = delete $params{'-width'};
174             # my $height = delete $params{'-height'};
175             if (! defined $width) { $width = _magic_get_width($m); }
176             if (! defined $height) { $height = _magic_get_height($m); }
177             # $m->Resize (width => $width, height => $height);
178              
179             if (@resize) {
180             ### Resize
181             $m->Resize (@resize);
182             }
183             ### Set(size): "${width}x${height}"
184             push @set, size => "${width}x${height}";
185             }
186              
187             {
188             my $key;
189             foreach $key (keys %params) {
190             my $attribute;
191             if ($attribute = $attr_to_GetSet{$key}) {
192             push @set, $attribute, delete $params{$key};
193             }
194             }
195             }
196             if (@set) {
197             ### Set(): @set
198             my $err;
199             if ($err = $m->Set(@set)) {
200             croak $err;
201             }
202             }
203              
204             ### store params: %params
205             %$self = (%$self, %params);
206             }
207              
208             sub load {
209             my ($self, $filename) = @_;
210             ### Image-Base-Magick load()
211             if (@_ > 1) {
212             $self->set('-file', $filename);
213             } else {
214             $filename = $self->get('-file');
215             }
216             ### load filename: $filename
217             ### into m: $self->{'-imagemagick'}
218              
219              
220             # This nonsense seems to be necessary to read from a filehandle to avoid
221             # "%d" interpretation on a named file.
222             #
223             # Must temporary $m->Set(filename=>'') or else Read() seems to prefer the
224             # filename attribute over the Read(file=>), or something.
225             #
226             # sysopen() is used to avoid perl two-arg open() whitespace stripping etc.
227             #
228             # @$m=() clear out existing image, as the Read() adds to the canvas.
229             #
230              
231             sysopen FH, $filename, Fcntl::O_RDONLY()
232             or croak "Cannot open $filename: $!";
233             binmode FH
234             or croak "Cannot set binmode for $filename: $!";
235              
236             my $m = $self->{'-imagemagick'};
237             my $err;
238             if ($err = $m->Set(filename => '')) {
239             close FH;
240             croak 'Oops, cannot temporarily unset filename attribute: ',$err;
241             }
242              
243             my @old_ims = @$m;
244             @$m = ();
245              
246             ### empty before load: $m
247             ### file size: -s \*FH
248             ### width: $m->Get('width')
249             ### height: $m->Get('height')
250             ### size: $m->Get('size')
251             ### filename: $m->Get('filename')
252              
253             my $readerr = $m->Read (file => \*FH);
254              
255             ### load leaves magick: $m
256             ### array: [@$m]
257             ### width: $m->Get('width')
258             ### height: $m->Get('height')
259             ### size: $m->Get('size')
260              
261             if ($err = $m->Set(filename => $filename)) {
262             close FH;
263             @$m = @old_ims;
264             croak 'Oops, cannot restore filename attribute: ',$err;
265             }
266              
267             if (! close FH) {
268             @$m = @old_ims;
269             return "Error closing $filename: $!";
270             }
271              
272             if ($readerr) {
273             @$m = @old_ims;
274             croak $readerr;
275             }
276              
277             if (! scalar(@$m)) {
278             @$m = @old_ims;
279             croak 'ImageMagick Read didn\'t read an image';
280             }
281              
282             # canvas size as size of image loaded
283             my ($width, $height);
284             if (! defined ($width = $m->Get('width'))
285             || ! defined ($height = $m->Get('height'))) {
286             @$m = @old_ims;
287             croak 'ImageMagick Read didn\'t give width,height';
288             }
289             my $size = "${width}x${height}";
290             if ($err = $m->Set (size => $size)) {
291             @$m = @old_ims;
292             croak "Cannot set size $size: $err";
293             }
294             }
295              
296              
297             # my $m = $self->{'-imagemagick'};
298             # my @old_ims = @$m;
299             # @$m = ();
300             # if (my $err = $m->Read ($filename)) {
301             # @$m = @old_ims;
302             # croak $err;
303             # }
304              
305              
306             # not documented ... probably doesn't work
307             sub load_fh {
308             my ($self, $fh) = @_;
309             ### Image-Base-Magick load_fh()
310             my $err;
311             if ($err = $self->{'-imagemagick'}->Read (file => $fh)) {
312             croak $err;
313             }
314             }
315              
316             # not yet documented ... and untested
317             sub load_string {
318             my ($self, $str) = @_;
319             my $err;
320             if ($err = $self->{'-imagemagick'}->Read (blob => $str)) {
321             croak $err;
322             }
323             }
324              
325             sub save {
326             my ($self, $filename) = @_;
327             ### Image-Base-Magick save(): @_
328             if (@_ > 1) {
329             $self->set('-file', $filename);
330             } else {
331             $filename = $self->get('-file');
332             }
333             ### $filename
334             ### _save_options: _save_options($self)
335              
336              
337             # Not using Write(filename=>) because it expands "%d" to a sequence
338             # number, per file:///usr/share/doc/imagemagick/www/perl-magick.html#read
339             #
340             # Use sysopen() so as not to interpret whitespace etc on $filename.
341             #
342             sysopen (FH, $filename,
343             Fcntl::O_WRONLY() | Fcntl::O_TRUNC() | Fcntl::O_CREAT())
344             or croak "Cannot create $filename: $!";
345             binmode FH
346             or croak "Cannot set binmode on $filename: $!";
347             {
348             my $err;
349             if ($err = $self->{'-imagemagick'}->Write (file => \*FH,
350             _save_options($self))) {
351             close FH;
352             croak $err;
353             }
354             }
355             close FH
356             or croak "Error closing $filename: $!";
357              
358             $self->set('-file', $filename);
359             }
360              
361             # if (my $err = $self->{'-imagemagick'}->Write (filename => $filename,
362             # _save_options($self))) {
363             # croak $err;
364             # }
365              
366              
367             # not yet documented ... might not work
368             sub save_fh {
369             my ($self, $fh) = @_;
370             my $err;
371             if ($err = $self->{'-imagemagick'}->Write (file => $fh,
372             _save_options($self))) {
373             croak $err;
374             }
375             }
376              
377             sub _save_options {
378             my ($self) = @_;
379              
380             # For PNG "quality" option is zlib_compression*10. Or for undef or -1
381             # compressionomit the quality parameter. Docs
382             # file:///usr/share/doc/imagemagick/www/command-line-options.html#quality
383             # Code coders/png.c WriteOnePNGImage() doing png_set_compression_level()
384             # of quality/10 with maximum 9
385             #
386             my $m = $self->{'-imagemagick'};
387             my $format = $m->Get('magick');
388             if ($format eq 'png') {
389             my $zlib_compression = $self->{'-zlib_compression'};
390             if (defined $zlib_compression && $zlib_compression >= 0) {
391             return (quality => $zlib_compression * 10);
392             }
393             }
394             # For JPEG and MIFF "quality" option is a percentage 0 to 100
395             # file:///usr/share/doc/imagemagick-doc/www/perl-magick.html#set-attribute
396             my $quality = $self->{'-quality_percent'};
397             if (defined $quality) {
398             return (quality => $quality);
399             }
400             return;
401             }
402              
403             # Circa ImageMagick 6.7.7.10 "pixel[]" such as
404             #
405             # $err = $m->set ("pixel[$x,$y]", $colour);
406             #
407             # when setting a negative X,Y or big positive X,Y somehow gets $err
408             #
409             # Exception 445: pixels are not authentic `black' @ error/cache.c/QueueAuthenticPixelCacheNexus/4387 at t/MyTestImageBase.pm line 326
410             #
411             # Using primitive=>'point' avoids that.
412              
413             sub xy {
414             my ($self, $x, $y, $colour) = @_;
415             ### Image-Base-Magick xy(): $x,$y,$colour
416             my $m = $self->{'-imagemagick'};
417             my $err;
418             if (@_ == 4) {
419             $err = $m->Draw (primitive => 'point',
420             fill => $colour,
421             points => "$x,$y");
422              
423             # Or maybe SetPixel(), but it takes color=>[$r,$g,$b] arrayref, not string
424             # $err = $m->SetPixel (x=>$x, y=>$y, color=>$colour);
425              
426             } else {
427             # cf $m->get("pixel[123,456]") gives a string "$r,$g,$g,$a"
428              
429             # GetPixel() gives list ($r,$g,$b) each in range 0 to 1
430             my @rgb = $m->GetPixel (x => $x, y => $y);
431             ### @rgb
432             if (@rgb == 1) {
433             $err = $rgb[0];
434             } else {
435             return sprintf '#%02X%02X%02X', map {$_*255} @rgb;
436             }
437             }
438             if ($err) {
439             croak $err;
440             }
441             }
442             sub line {
443             my ($self, $x1, $y1, $x2, $y2, $colour) = @_;
444             ### Image-Base-Magick line: @_
445             my $err;
446             if ($err = $self->{'-imagemagick'}->Draw (primitive => 'line',
447             fill => $colour,
448             points => "$x1,$y1 $x2,$y2")) {
449             croak $err;
450             }
451             }
452             sub rectangle {
453             my ($self, $x1, $y1, $x2, $y2, $colour, $fill) = @_;
454             ### Image-Base-Magick rectangle: @_
455             # ### index: $self->colour_to_index($colour)
456              
457             my $m = $self->{'-imagemagick'};
458             my $err;
459             if ($x1==$x2 && $y1==$y2) {
460             # primitive=>rectangle of 1x1 seems to draw nothing
461              
462             ### use set pixel[]
463             $err = $m->set ("pixel[$x1,$y1]", $colour);
464              
465             # $err = $m->Draw (primitive => 'point',
466             # fill => $colour,
467             # points => "$x1,$y1");
468              
469             } else {
470             $err = $m->Draw (primitive => 'rectangle',
471             ($fill ? 'fill' : 'stroke'), $colour,
472             points => "$x1,$y1 $x2,$y2");
473             }
474             if ($err) {
475             croak $err;
476             }
477             }
478              
479             sub ellipse {
480             my ($self, $x1, $y1, $x2, $y2, $colour, $fill) = @_;
481             ### Image-Base-Magick ellipse: "$x1, $y1, $x2, $y2, $colour"
482              
483             my $m = $self->{'-imagemagick'};
484             my $w = $x2 - $x1;
485             my $h = $y2 - $y1;
486             my $err;
487             if ($w || $h) {
488             ### more than 1 pixel wide and/or high, primitive=>ellipse
489             ### ellipse: (($x1+$x2)/2).','.(($y1+$y2)/2).' '.($w/2).','.($h/2).' 0,360'
490             $err = $m->Draw (primitive => 'ellipse',
491             strokewidth => .25,
492             ($fill ? 'fill' : 'stroke') => $colour,
493             points => ((($x1+$x2)/2).','.(($y1+$y2)/2)
494             .' '
495             .($w/2).','.($h/2)
496             .' 0,360'));
497             } else {
498             ### only 1 pixel wide and/or high, primitive=>line
499             $err = $m->Draw (primitive => 'line',
500             fill => $colour,
501             points => "$x1,$y1 $x2,$y2");
502             }
503             if ($err) {
504             croak $err;
505             }
506             }
507              
508             sub diamond {
509             my ($self, $x1, $y1, $x2, $y2, $colour, $fill) = @_;
510             ### Image-Base-Magick diamond() ...
511              
512             my $xh = ($x2 - $x1);
513             my $yh = ($y2 - $y1);
514             my $xeven = ($xh & 1);
515             my $yeven = ($yh & 1);
516             $xh = int($xh / 2);
517             $yh = int($yh / 2);
518             ### x centre: $x1+$xh, $x2-$xh
519             ### assert: $x1+$xh+$xeven == $x2-$xh
520             ### assert: $y1+$yh+$yeven == $y2-$yh
521              
522             my $m = $self->{'-imagemagick'};
523             my $err;
524             if ($x1 == $x2 && $y1 == $y2) {
525             # 1x1 polygon doesn't seem to draw any pixels in imagemagick 6.6, do it
526             # as a single point instead
527             $err = $m->set ("pixel[$x1,$y1]", $colour);
528              
529             } else {
530             $err = $m->Draw (primitive => 'polygon',
531             ($fill ? 'fill' : 'stroke') => $colour,
532             strokewidth => 0,
533             points => (($x1+$xh).' '.$y1 # top centre
534              
535             # left
536             .' '.$x1.' '.($y1+$yh)
537              
538             .($yeven ? ' '.$x1.' '.($y2-$yh) : '')
539              
540             # bottom
541             .' '.($x1+$xh).' '.$y2
542             .($xeven ? ' '.($x2-$xh).' '.$y2 : '')
543              
544             # right
545             .($yeven ? ' '.$x2.' '.($y2-$yh) : '')
546             .' '.$x2.' '.($y1+$yh)
547              
548             .($xeven ? ' '.($x2-$xh).' '.$y1 : '')
549             ));
550             }
551             if ($err) {
552             croak $err;
553             }
554             }
555              
556             # sub add_colours {
557             # my $self = shift;
558             # ### add_colours: @_
559             #
560             # my $m = $self->{'-imagemagick'};
561             # }
562              
563             1;
564             __END__