File Coverage

blib/lib/Math/Image/CalcResized.pm
Criterion Covered Total %
statement 11 115 9.5
branch 0 110 0.0
condition 0 18 0.0
subroutine 4 7 57.1
pod 2 2 100.0
total 17 252 6.7


line stmt bran cond sub pod time code
1             package Math::Image::CalcResized;
2              
3 1     1   399007 use 5.010001;
  1         4  
4 1     1   6 use strict;
  1         2  
  1         36  
5 1     1   5 use warnings;
  1         2  
  1         73  
6              
7 1     1   6 use Exporter 'import';
  1         3  
  1         3086  
8              
9             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
10             our $DATE = '2024-08-29'; # DATE
11             our $DIST = 'Math-Image-CalcResized'; # DIST
12             our $VERSION = '0.006'; # VERSION
13              
14             our @EXPORT_OK = qw(calc_image_resized_size);
15             our %SPEC;
16              
17             $SPEC{':package'} = {
18             v => 1.1,
19             summary => 'Calculate dimensions of image/video resized by ImageMagick-like geometry specification',
20             };
21              
22             sub _calc_or_human {
23 0     0     my ($action, %args) = @_;
24              
25 0           my $size;
26 0           my ($w, $h);
27 0 0         if ($action eq 'calc') {
28 0 0         $size = $args{size} or return [400, "Please specify image size"];
29 0 0         $size =~ /\A(\d+)x(\d+)\z/ or return [400, "Invalid size format, please use x syntax"];
30 0           ($w, $h) = ($1, $2);
31             }
32 0 0         my $resize = $args{resize}; defined $resize or return [400, "Please specify resize"];
  0            
33 0           my ($w2, $h2) = ($w, $h);
34 0           my $human_general = "no resizing";
35 0           my $human_specific;
36              
37 0 0         goto SKIP unless length $resize;
38              
39             # some instructions are translated to other first
40 0 0         if ($resize =~ /\A(\d+)\^([<>])\z/) {
    0          
41 0 0         $human_general = ($2 eq '>' ? "shrink" : "enlarge") . " shortest side to ${1}px";
42 0 0         goto SKIP unless $action eq 'calc';
43 0 0         if ($w < $h) {
44 0           $resize = "$1$2";
45 0 0         $human_specific = ($2 eq '>' ? "shrink" : "enlarge") . " shortest side (width) to ${1}px";
46             } else {
47 0           $resize = "x$1$2";
48 0 0         $human_specific = ($2 eq '>' ? "shrink" : "enlarge") . " shortest side (height) to ${1}px";
49             }
50             } elsif ($resize =~ /\A\^(\d+)([<>])\z/) {
51 0 0         $human_general = ($2 eq '>' ? "shrink" : "enlarge") . " longest side to ${1}px";
52 0 0         goto SKIP unless $action eq 'calc';
53 0 0         if ($w > $h) {
54 0           $resize = "$1$2";
55 0 0         $human_specific = ($2 eq '>' ? "shrink" : "enlarge") . " longest side (width) to ${1}px";
56             } else {
57 0           $resize = "x$1$2";
58 0 0         $human_specific = ($2 eq '>' ? "shrink" : "enlarge") . " longest side (height) to ${1}px";
59             }
60             }
61              
62 0 0         if ($resize =~ /\A(\d+(?:\.\d*)?)%\z/) {
    0          
    0          
    0          
    0          
63 0           $human_general = "scale to $resize";
64 0 0         goto SKIP unless $action eq 'calc';
65 0           $w2 = $1/100 * $w;
66 0           $h2 = $1/100 * $h;
67 0           $human_specific = "scale to $resize (${w2}px)";
68             } elsif ($resize =~ /\A(\d+(?:\.\d*)?)%?x(\d+(?:\.\d*)?)%\z/) {
69 0           $human_general = "scale width to ${1}%, height to ${2}%";
70 0 0         goto SKIP unless $action eq 'calc';
71 0           $w2 = $1/100 * $w;
72 0           $h2 = $2/100 * $h;
73 0           $human_specific = "scale width to ${1}% (${w2}px), height to ${2}% (${h2}px)";
74             } elsif ($resize =~ /\A(\d+)([>^<]?)\z/) {
75 0           my $which = $2;
76 0 0 0       if ($which eq '>') { # shrink
    0          
77 0           $human_general = "shrink width to ${1}px";
78 0 0         goto SKIP unless $action eq 'calc';
79 0 0         goto SKIP if $w <= $1;
80             } elsif ($which eq '^' || $which eq '<') { # enlarge
81 0           $human_general = "enlarge width to ${1}px";
82 0 0         goto SKIP unless $action eq 'calc';
83 0 0         goto SKIP if $w >= $1;
84             } else {
85 0           $human_general = "set width to ${1}px";
86 0 0         goto SKIP unless $action eq 'calc';
87             }
88              
89 0           $w2 = $1;
90 0           $h2 = ($h/$w) * $w2;
91 0           $human_specific = $human_general;
92             } elsif ($resize =~ /\Ax(\d+)([>^<]?)\z/) {
93 0           my $which = $2;
94 0 0 0       if ($which eq '>') { # shrink
    0          
95 0           $human_general = "shrink height to ${1}px";
96 0 0         goto SKIP unless $action eq 'calc';
97 0 0         goto SKIP if $h <= $1;
98             } elsif ($which eq '^' || $which eq '<') { # enlarge
99 0           $human_general = "enlarge height to ${1}px";
100 0 0         goto SKIP unless $action eq 'calc';
101 0 0         goto SKIP if $h >= $1;
102             } else {
103 0           $human_general = "set height to ${1}px";
104 0 0         goto SKIP unless $action eq 'calc';
105             }
106              
107 0           $h2 = $1;
108 0           $w2 = ($w/$h) * $h2;
109 0           $human_specific = $human_general;
110             } elsif ($resize =~ /\A(\d+)x(\d+)([<>!^]?)\z/) {
111 0           my $which = $3;
112 0 0 0       if ($which eq '' || $which eq '>') {
    0 0        
    0          
113 0 0         if ($which eq '>') {
114 0           $human_general = "shrink image to fit inside ${1}x${2}";
115 0 0         goto SKIP unless $action eq 'calc';
116 0 0 0       goto SKIP if $w <= $1 || $h <= $2;
117             }
118              
119 0           $human_general = "fit image inside ${1}x${2}";
120 0 0         goto SKIP unless $action eq 'calc';
121              
122 0 0         if ($h2 > $2) {
123 0           $h2 = $2;
124 0           $w2 = ($w/$h) * $h2;
125             }
126 0 0         if ($w2 > $1) {
127 0           $h2 = $1/$w2 * $h2;
128 0           $w2 = $1;
129             }
130 0           $human_specific = $human_general;
131             } elsif ($which eq '^' || $which eq '<') {
132 0 0         if ($which eq '<') {
133 0           $human_general = "enlarge image to fit ${1}x${2} inside it";
134 0 0         goto SKIP unless $action eq 'calc';
135 0 0 0       goto SKIP if $w >= $1 || $h >= $2;
136             }
137              
138 0           $human_general = "fit image to fit ${1}x${2} inside it";
139 0 0         goto SKIP unless $action eq 'calc';
140              
141 0 0         if ($h2 < $2) {
142 0           $h2 = $2;
143 0           $w2 = ($w/$h) * $h2;
144             }
145 0 0         if ($w2 < $1) {
146 0           $h2 = $1/$w2 * $h2;
147 0           $w2 = $1;
148             }
149 0           $human_specific = $human_general;
150             } elsif ($which eq '!') {
151 0           $human_general = "set dimension to ${1}x${2}";
152 0 0         goto SKIP unless $action eq 'calc';
153              
154 0           $w2 = $1;
155 0           $h2 = $2;
156 0           $human_specific = $human_general;
157             }
158             } else {
159 0           return [400, "Unrecognized resize instruction '$resize'"];
160             }
161              
162             SKIP:
163 0 0         if ($action eq 'human') {
164 0           [200, "OK", $human_general];
165             } else {
166 0           [200, "OK", sprintf("%dx%d", $w2, $h2), {
167             'func.human_general' => $human_general,
168             'func.human_specific' => $human_specific,
169             }];
170             }
171             }
172              
173             $SPEC{calc_image_resized_size} = {
174             v => 1.1,
175             summary => 'Given size of an image (in WxH, e.g. "2592x1944") and ImageMagick-like resize instruction (e.g. "1024p>"), calculate new resized image',
176             args => {
177             size => {
178             summary => 'Image/video size, in x format, e.g. 2592x1944',
179             schema => ['str*', match=>qr/\A\d+x\d+\z/],
180             req => 1,
181             pos => 0,
182             description => <<'_',
183              
184             _
185             },
186             resize => {
187             summary => 'Resize instruction, follows ImageMagick format',
188             schema => 'str*',
189             req => 1,
190             pos => 1,
191             description => <<'_',
192              
193             Resize instruction can be given in several formats:
194              
195             Syntax Meaning
196             -------------------------- ----------------------------------------------------------------
197             "" No resizing.
198              
199             SCALE"%" Height and width both scaled by specified percentage.
200             SCALEX"%x"SCALEY"%" Height and width individually scaled by specified percentages. (Only one % symbol needed.)
201              
202             WIDTH Width given, height automagically selected to preserve aspect ratio.
203             WIDTH">" Shrink width if larger, height automagically selected to preserve aspect ratio.
204             WIDTH"^" Enlarge width if smaller, height automagically selected to preserve aspect ratio.
205              
206             "x"HEIGHT Height given, width automagically selected to preserve aspect ratio.
207             "x"HEIGHT">" Shrink height if larger, width automagically selected to preserve aspect ratio.
208             "x"HEIGHT"^" Enlarge height if smaller, width automagically selected to preserve aspect ratio.
209              
210             WIDTH"x"HEIGHT Maximum values of height and width given, aspect ratio preserved.
211             WIDTH"x"HEIGHT"^" Minimum values of height and width given, aspect ratio preserved.
212             WIDTH"x"HEIGHT"!" Width and height emphatically given, original aspect ratio ignored.
213             WIDTH"x"HEIGHT">" Shrinks an image with dimension(s) larger than the corresponding width and/or height argument(s).
214             WIDTH"x"HEIGHT"<" Shrinks an image with dimension(s) larger than the corresponding width and/or height argument(s).
215              
216             NUMBER"^>" Shrink shortest side if larger than number, aspect ratio preserved.
217             NUMBER"^<" Enlarge shortest side if larger than number, aspect ratio preserved.
218             "^"NUMBER">" Shrink longer side if larger than number, aspect ratio preserved.
219             "^"NUMBER"<" Enlarge longer side if larger than number, aspect ratio preserved.
220              
221             Currently unsupported:
222              
223             AREA"@" Resize image to have specified area in pixels. Aspect ratio is preserved.
224             X":"Y Here x and y denotes an aspect ratio (e.g. 3:2 = 1.5).
225              
226             Ref:
227              
228             _
229             },
230             },
231             examples => [
232             {args=>{size=>"2592x1944", resize=>""}, naked_result=>"2592x1944", summary=>"no resizing"},
233              
234             {args=>{size=>"2592x1944", resize=>"20%"}, naked_result=>"518x388", summary=>"scale (down) to 20%"},
235              
236             {args=>{size=>"2592x1944", resize=>"20%x40%"}, naked_result=>"518x777", summary=>"scale (down) width to 20% but height to 40%"},
237             {args=>{size=>"2592x1944", resize=>"20x40%"}, naked_result=>"518x777", summary=>"scale (down) width to 20% but height to 40% (first percent sign is optional)"},
238              
239             {args=>{size=>"2592x1944", resize=>"1024"}, naked_result=>"1024x768", summary=>"set width to 1024px"},
240              
241             {args=>{size=>"2592x1944", resize=>"1024>"}, naked_result=>"1024x768", summary=>"shrink width to 1024px"},
242             {args=>{size=>"2592x1944", resize=>"10240>"}, naked_result=>"2592x1944", summary=>"shrink width to 10240px (no effect since width is already less than 10240px)"},
243              
244             {args=>{size=>"2592x1944", resize=>"1024^"}, naked_result=>"2592x1944", summary=>"enlarge width to 1024px (no effect since width is already greater than 1024px"},
245             {args=>{size=>"2592x1944", resize=>"10240^"}, naked_result=>"10240x7680", summary=>"enlarge width to 10240px"},
246              
247             {args=>{size=>"2592x1944", resize=>"x1024"}, naked_result=>"1365x1024", summary=>"set height to 1024px"},
248              
249             {args=>{size=>"2592x1944", resize=>"x768>"}, naked_result=>"1024x768", summary=>"shrink height to 768px"},
250             {args=>{size=>"2592x1944", resize=>"x7680>"}, naked_result=>"2592x1944", summary=>"shrink height to 7680px (no effect since height is already less than 7680px)"},
251              
252             {args=>{size=>"2592x1944", resize=>"x768^"}, naked_result=>"2592x1944", summary=>"enlarge height to 768px (no effect since height is already greater than 768px)"},
253             {args=>{size=>"2592x1944", resize=>"x7680^"}, naked_result=>"10240x7680", summary=>"enlarge height to 7680px"},
254              
255             {args=>{size=>"2592x1944", resize=>"20000x10000"}, naked_result=>"2592x1944", summary=>"fit image inside 20000x10000 (no effect since it already fits)"},
256             {args=>{size=>"2592x1944", resize=>"20000x1000"}, naked_result=>"1333x1000", summary=>"fit image inside 20000x1000 (height is reduced to 1000 to make the image fit)"},
257             {args=>{size=>"2592x1944", resize=>"100x200"}, naked_result=>"100x75", summary=>"fit image inside 100x200"},
258             {args=>{size=>"2592x1944", resize=>"100x100"}, naked_result=>"100x75", summary=>"fit image inside 100x100"},
259              
260             {args=>{size=>"2592x1944", resize=>"10000x5000^"}, naked_result=>"10000x7500", summary=>"fit a 10000x5000 area inside image"},
261             {args=>{size=>"2592x1944", resize=>"5000x10000^"}, naked_result=>"13333x10000", summary=>"fit a 5000x10000 area inside image"},
262             {args=>{size=>"2592x1944", resize=>"100x100^"}, naked_result=>"2592x1944", summary=>"fit a 100x100 area inside image (no effect since the image can already fit that area)"},
263              
264             {args=>{size=>"2592x1944", resize=>"100x100!"}, naked_result=>"100x100", summary=>"set dimension to 100x100"},
265              
266             {args=>{size=>"2592x1944", resize=>"10000x5000>"}, naked_result=>"2592x1944", summary=>"shrink image to fit inside 10000x5000px (no effect since image already fits)"},
267             {args=>{size=>"2592x1944", resize=>"2000x1000>"}, naked_result=>"1333x1000", summary=>"shrink image to fit inside 2000x1000px"},
268             {args=>{size=>"2592x1944", resize=>"100x100>"}, naked_result=>"100x75", summary=>"shrink image to fit inside 100x100px"},
269              
270             {args=>{size=>"2592x1944", resize=>"10000x5000<"}, naked_result=>"10000x7500", summary=>"enlarge image to fit 10000x5000px inside it"},
271             {args=>{size=>"2592x1944", resize=>"5000x10000<"}, naked_result=>"13333x10000", summary=>"enlarge image to fit 5000x10000px inside it"},
272             {args=>{size=>"2592x1944", resize=>"3000x1000<"}, naked_result=>"2592x1944", summary=>"enlarge image to fit 3000x1000px inside it (no effect since image already fits)"},
273              
274             {args=>{size=>"2592x1944", resize=>"1024^>"}, naked_result=>"1365x1024", summary=>"shrink shortest side to 1024px"},
275             {args=>{size=>"2592x1944", resize=>"10240^>"}, naked_result=>"2592x1944", summary=>"shrink shortest side to 10240px (no effect since shortest side 1944px is already less than 10240px)"},
276             {args=>{size=>"2592x1944", resize=>"1024^<"}, naked_result=>"2592x1944", summary=>"enlarge shortest side to 1024px (no effect since shortest side is already greater than 1024px)"},
277             {args=>{size=>"2592x1944", resize=>"10240^<"}, naked_result=>"13653x10240", summary=>"enlarge shortest side to 10240px"},
278              
279             {args=>{size=>"2592x1944", resize=>"^1024>"}, naked_result=>"1024x768", summary=>"shrink longest side to 1024px"},
280             {args=>{size=>"2592x1944", resize=>"^10240>"}, naked_result=>"2592x1944", summary=>"shrink longest side to 10240px (no effect since longest side 2592px is already less than 10240px)"},
281             {args=>{size=>"2592x1944", resize=>"^1024<"}, naked_result=>"2592x1944", summary=>"enlarge longest side to 1024px (no effect since longest side 2592px is already greater than 1024px)"},
282             {args=>{size=>"2592x1944", resize=>"^10240<"}, naked_result=>"10240x7680", summary=>"enlarge longest side to 10240px"},
283             ],
284             links => [
285             {url=>'prog:imgsize'},
286             ],
287             };
288             sub calc_image_resized_size {
289 0     0 1   _calc_or_human('calc', @_);
290             }
291              
292             $SPEC{image_resize_notation_to_human} = {
293             v => 1.1,
294             summary => 'Translate ImageMagick-like resize notation (e.g. "720^>") to human-friendly text (e.g. "shrink shortest side to 720px")',
295             description => <<'_',
296              
297             Resize notation supports most syntax from ImageMagick geometry. See
298             and ImageMagick documentation on geometry for more
299             details.
300              
301             _
302             args => {
303             resize => {
304             schema => 'str*',
305             req => 1,
306             pos => 0,
307             },
308             },
309             examples => [
310             {
311             args => {resize=>''}, naked_result=>'no resizing',
312             },
313              
314             {
315             args => {resize=>'50%'}, naked_result=>'scale to 50%',
316             },
317             {
318             args => {resize=>'50%x50%'}, naked_result=>'scale width to 50%, height to 50%',
319             },
320              
321             {
322             args => {resize=>'720'}, naked_result=>'set width to 720px',
323             },
324             {
325             args => {resize=>'720>'}, naked_result=>'shrink width to 720px',
326             },
327             {
328             args => {resize=>'720^'}, naked_result=>'enlarge width to 720px',
329             },
330              
331             {
332             args => {resize=>'x720'}, naked_result=>'set height to 720px',
333             },
334             {
335             args => {resize=>'x720>'}, naked_result=>'shrink height to 720px',
336             },
337             {
338             args => {resize=>'x720^'}, naked_result=>'enlarge height to 720px',
339             },
340              
341             {
342             args => {resize=>'640x480'}, naked_result=>'fit image inside 640x480',
343             },
344             {
345             args => {resize=>'640x480^'}, naked_result=>'fit image to fit 640x480 inside it',
346             },
347             {
348             args => {resize=>'640x480>'}, naked_result=>'shrink image to fit inside 640x480',
349             },
350             {
351             args => {resize=>'640x480<'}, naked_result=>'enlarge image to fit 640x480 inside it',
352             },
353             {
354             args => {resize=>'640x480!'}, naked_result=>'set dimension to 640x480',
355             },
356              
357             {
358             args => {resize=>'720^>'}, naked_result=>'shrink shortest side to 720px',
359             },
360             {
361             args => {resize=>'720^<'}, naked_result=>'enlarge shortest side to 720px',
362             },
363             {
364             args => {resize=>'^720>'}, naked_result=>'shrink longest side to 720px',
365             },
366             {
367             args => {resize=>'^720<'}, naked_result=>'enlarge longest side to 720px',
368             },
369             ],
370             };
371             sub image_resize_notation_to_human {
372 0     0 1   _calc_or_human('human', @_);
373             }
374              
375             1;
376             # ABSTRACT: Calculate dimensions of image/video resized by ImageMagick-like geometry specification
377              
378             __END__