exiftool_vendored 13.18.0 → 13.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,492 @@
1
+ #------------------------------------------------------------------------------
2
+ # File: Plot.pm
3
+ #
4
+ # Description: Plot tag values in SVG format
5
+ #
6
+ # Revisions: 2025-02-14 - P. Harvey Created
7
+ #------------------------------------------------------------------------------
8
+
9
+ package Image::ExifTool::Plot;
10
+
11
+ use strict;
12
+ use vars qw($VERSION);
13
+
14
+ $VERSION = '1.01';
15
+
16
+ # default plot settings (lower-case settings may be overridden by the user)
17
+ my %defaults = (
18
+ size => [ 800, 600 ], # width,height of output image
19
+ margin => [ 60, 15, 15, 30 ], # left,top,right,bottom margins around plot area
20
+ legend => [ 0, 0 ], # top,right offset for legend
21
+ txtPad => [ 10, 10 ], # padding between text and x,y scale
22
+ lineSpacing => 20, # text line spacing
23
+ # colors for plot lines
24
+ cols => [ qw(red green blue black orange gray purple cyan brown pink
25
+ goldenrod lightsalmon seagreen goldenrod cadetblue plum
26
+ deepskyblue mediumpurple royalblue tomato) ],
27
+ grid => 'darkgray', # grid color
28
+ text => 'black', # text and plot frame color
29
+ type => 'line', # plot type, 'line' or 'scatter'
30
+ style => 'line', # 'line', 'marker' or 'line+marker'
31
+ xlabel => '', # x axis label
32
+ ylabel => '', # y axis label
33
+ title => '', # plot title
34
+ nbins => 20, # number of histogram bins
35
+ # xmin, xmax # x axis minimum,maximum
36
+ # ymin, ymax # y axis minimum,maximum
37
+ # split # split list of numbers into separate plot lines
38
+ # bkg # background color
39
+ Data => { }, # data arrays for each variable
40
+ Name => [ ], # variable names
41
+ XMax => 0, # number of points in plot so far
42
+ );
43
+
44
+ my @markerName = qw(circle square triangle diamond triangle2 triangle3 triangle4);
45
+ my @markerData = (
46
+ '<circle cx="6" cy="6" r="4" stroke-width="1.5" stroke="context-stroke" fill="none" />',
47
+ '<path stroke-width="1.5" stroke="context-stroke" fill="none" d="M2.5 2.5 l7 0 0 7 -7 0 z"/>',
48
+ '<path stroke-width="1.5" stroke="context-stroke" fill="none" d="M6 1.2 l4 8 -8 0 z"/>',
49
+ '<path stroke-width="1.5" stroke="context-stroke" fill="none" d="M6 1.5 l4.5 4.5 -4.5 4.5 -4.5 -4.5 z"/>',
50
+ '<path stroke-width="1.5" stroke="context-stroke" fill="none" d="M1.2 6 l8 4 0 -8 z"/>',
51
+ '<path stroke-width="1.5" stroke="context-stroke" fill="none" d="M6 10.8 l4 -8 -8 0 z"/>',
52
+ '<path stroke-width="1.5" stroke="context-stroke" fill="none" d="M10.8 6 l-8 4 0 -8 z"/>',
53
+ );
54
+ # optimal number grid lines in X and Y for a 800x600 plot and nominal character width
55
+ my ($nx, $ny, $wch) = (15, 12, 8);
56
+
57
+ #------------------------------------------------------------------------------
58
+ # Create new plot object
59
+ sub new
60
+ {
61
+ my $that = shift;
62
+ my $class = ref($that) || $that || 'Image::ExifTool::Plot';
63
+ my $self = bless { }, $class;
64
+ foreach (keys %defaults) {
65
+ ref $defaults{$_} eq 'HASH' and $$self{$_} = { %{$defaults{$_}} }, next;
66
+ ref $defaults{$_} eq 'ARRAY' and $$self{$_} = [ @{$defaults{$_}} ], next;
67
+ $$self{$_} = $defaults{$_};
68
+ }
69
+ return $self;
70
+ }
71
+
72
+ #------------------------------------------------------------------------------
73
+ # Set plot settings
74
+ # Inputs: 0) Plot ref, 1) comma-separated options
75
+ sub Settings($$)
76
+ {
77
+ my ($self, $set) = @_;
78
+ return unless $set;
79
+ foreach (split /,\s*/, $set) {
80
+ next unless /^([a-z].*?)(=(.*))?$/i;
81
+ my ($name, $val) = ($1, $3);
82
+ if (ref $$self{$name} eq 'ARRAY') {
83
+ next unless defined $val;
84
+ $$self{lc $name} = [ split /[\s\/]+/, $val ]; # split on space or slash
85
+ } else {
86
+ $val = 1 unless defined $val; # default to 1 if no "="
87
+ my %charName = ('&'=>'amp', '<'=>'lt', '>'=>'gt');
88
+ # escape necessary XML characters, but allow numerical entities
89
+ $val =~ s/([&><])/&$charName{$1};/sg and $val =~ s/&amp;(#(\d+|x[0-9a-fA-F]+);)/&$1/;
90
+ undef $val unless length $val;
91
+ $$self{lc $name} = $val;
92
+ }
93
+ }
94
+ }
95
+
96
+ #------------------------------------------------------------------------------
97
+ # Add points to SVG plot
98
+ # Inputs: 0) Plot object ref, 1) tag value hash ref, 2) tag ID list ref
99
+ sub AddPoints($$$)
100
+ {
101
+ my ($self, $info, $tags) = @_;
102
+ my ($tag, $name, %num, $index, $mod, $val, @vals);
103
+ my ($ee, $docNum, $data, $xmin, $xmax) = @$self{qw(EE DocNum Data XMin XMax)};
104
+ $$self{type} or $$self{type} = 'line';
105
+ my $scat = $$self{type} =~ /^s/i;
106
+ my $xname = $$self{Name}[0]; # (x-axis name if using scatter plot)
107
+ my $maxLines = $$self{type} =~ /^h/i ? 1 : 20;
108
+ for (;;) {
109
+ if (@vals) {
110
+ $val = shift @vals;
111
+ } else {
112
+ $tag = shift @$tags or last;
113
+ # ignore non-floating-point values
114
+ $val = $$info{$tag};
115
+ ($name) = $tag =~ /^(\S+)/g; # remove index number
116
+ if (ref $val) {
117
+ if (ref $val eq 'ARRAY') {
118
+ $index = defined $index ? $index + 1 : 0;
119
+ $val = $$val[$index];
120
+ defined $val or undef($index), undef($mod), next;
121
+ $name .= $mod ? '['.($index % $mod).']' : "[$index]";
122
+ unshift @$tags, $tag; # will continue with this tag later
123
+ } elsif (ref $val eq 'SCALAR') {
124
+ $val = $$val; # handle binary values
125
+ } else {
126
+ next;
127
+ }
128
+ }
129
+ }
130
+ next unless defined $val and $val =~ /^[+-]?(?=\.?\d)\d*\.?\d*(?:e[+-]?\d+)?([ ,;\t\n\r]?|\z)/i;
131
+ if ($1) {
132
+ # split a string of numbers into separate plot points (eg. histogram tags)
133
+ if ($$self{'split'}) {
134
+ # make an array
135
+ $$info{$tag} = [ split /[ ,;\t\n\r][\n\r]? */, $val ];
136
+ unshift @$tags, $tag;
137
+ # split into lists of 'split' elements if split > 1
138
+ $mod = $$self{'split'} if $$self{'split'} > 1;
139
+ next;
140
+ } else {
141
+ @vals = split /[ ,;\t\n\r][\n\r]? */, $val;
142
+ $val = shift @vals;
143
+ }
144
+ }
145
+ my $docNum = $docNum ? $$docNum{$tag} || 0 : 0;
146
+ next if $docNum and not $ee;
147
+ unless ($$data{$name}) {
148
+ if (@{$$self{Name}} >= $maxLines) {
149
+ unless ($$self{MaxTags}) {
150
+ if ($$self{type} =~ /^h/i) {
151
+ $$self{Warn} = 'A histogram can only plot one variable';
152
+ } else {
153
+ $$self{Warn} = 'Too many variables to plot all of them';
154
+ }
155
+ $$self{MaxTags} = 1;
156
+ }
157
+ next;
158
+ }
159
+ push @{$$self{Name}}, $name;
160
+ $xname or $xname = $name; # x-axis data for scatter plot
161
+ unless (defined $$self{Min}) {
162
+ $$self{Min} = $$self{Max} = $val unless $scat and $name eq $xname;
163
+ $xmin = $xmax = $docNum unless defined $xmin;
164
+ }
165
+ $num{$name} = $xmax;
166
+ $$data{$name}[$xmax - $xmin] = $val if $xmax >= $xmin;
167
+ next;
168
+ }
169
+ if ($docNum and $num{$name} < $docNum) {
170
+ $num{$name} = $docNum; # keep documents synchronized if some tags are missing
171
+ } else {
172
+ $num{$name} = $xmax unless defined $num{$name};
173
+ ++$num{$name};
174
+ }
175
+ $$data{$name}[$num{$name} - $xmin] = $val if $num{$name} >= $xmin;
176
+ unless ($scat and $name eq $xname) {
177
+ $$self{Max} = $val if $val > $$self{Max};
178
+ $$self{Min} = $val if $val < $$self{Min};
179
+ }
180
+ }
181
+ # start next file at x value so far
182
+ $xmax < $num{$_} and $xmax = $num{$_} foreach keys %num;
183
+ $$self{XMin} = $xmin;
184
+ $$self{XMax} = $xmax;
185
+ }
186
+
187
+ #------------------------------------------------------------------------------
188
+ # Calculate a nice round number for grid spacing
189
+ # Inputs: 0) nominal spacing (must be positive)
190
+ # Returns: spacing rounded to an even number
191
+ sub GetGridSpacing($)
192
+ {
193
+ my $nom = shift;
194
+ my $rounded;
195
+ my $div = sprintf('%.3e', $nom);
196
+ my $num = substr($div, 0, 1);
197
+ my $exp = $div =~ s/.*e// ? $div : 0;
198
+ # look for nearest factor to 1, 1.5, 2 or 5 * 10^x
199
+ ($num, $exp) = $num < 8 ? (5, $exp) : (1, $exp+1) if $num > 2;
200
+ return $exp >= 0 ? $num . ('0' x $exp) : '.' . ('0' x (-$exp - 1)) . $num;
201
+ }
202
+
203
+ #------------------------------------------------------------------------------
204
+ # Draw SVG plot
205
+ # Inputs: 0) Plot ref, 1) Output file reference
206
+ sub Draw($$)
207
+ {
208
+ my ($self, $fp) = @_;
209
+ my ($min, $max, $xmin, $xmax, $name, $style) = @$self{qw(Min Max XMin XMax Name style)};
210
+
211
+ if (not defined $min or not defined $xmin or not $style) {
212
+ $$self{Error} = 'Nothing to plot';
213
+ return;
214
+ }
215
+ my ($data, $title, $xlabel, $ylabel, $cols) = @$self{qw(Data title xlabel ylabel cols)};
216
+ my ($i, $n, %col, %class, $dx, $dy, $dx2, $xAxis, $x, $y, $px, $py);
217
+ my ($grid, $lastLen, $noLegend, $xname, $xdat, $xdiff, $diff);
218
+ my $scat = $$self{type} =~ /^s/i ? 1 : 0;
219
+ my $hist = $$self{type} =~ /^h/i ? [ ] : 0;
220
+ my @name = @$name;
221
+ my @margin = ( @{$$self{margin}} );
222
+
223
+ # set reasonable default titles and labels
224
+ $xname = shift @name if $scat;
225
+ $title = "$name[0] vs $xname" if $scat and defined $title and not $title and @name == 1;
226
+ $xlabel = $$name[0] if $scat || $hist and defined $xlabel and not $xlabel;
227
+ $ylabel = ($hist ? 'Count' : $name[0]) and $noLegend=1 if defined $ylabel and not $ylabel and @name == 1;
228
+
229
+ # make room for title/labels
230
+ $margin[1] += $$self{lineSpacing} * 1.5 if $title;
231
+ $margin[3] += $$self{lineSpacing} if $xlabel;
232
+ $margin[0] += $$self{lineSpacing} if $ylabel;
233
+
234
+ if ($scat) {
235
+ $xdat = $$self{Data}{$xname};
236
+ unless (defined $$self{xmin} and defined $$self{xmax}) {
237
+ my $set;
238
+ foreach (@$xdat) {
239
+ next unless defined;
240
+ $set or $xmin = $xmax = $_, $set = 1, next;
241
+ $xmin = $_ if $xmin > $_;
242
+ $xmax = $_ if $xmax < $_;
243
+ }
244
+ my $dnx2 = ($xmax - $xmin) / ($nx * 2);
245
+ # leave a bit of a left/right margin, but don't pass 0
246
+ $xmin = ($xmin >= 0 and $xmin < $dnx2) ? 0 : $xmin - $dnx2;
247
+ $xmax = ($xmax <= 0 and -$xmax < $dnx2) ? 0 : $xmax + $dnx2;
248
+ }
249
+ $xmin = $$self{xmin} if defined $$self{xmin};
250
+ $xmax = $$self{xmax} if defined $$self{xmax};
251
+ } else {
252
+ # shift x range to correspond with index in data list
253
+ $xmax -= $xmin;
254
+ $xmin = 0;
255
+ }
256
+ if ($hist) {
257
+ $$self{nbins} > 0 or $$self{Error} = 'Invalid number of histogram bins', return;
258
+ $noLegend = 1;
259
+ # y axis becomes histogram x axis after binning
260
+ $min = $$self{xmin} if defined $$self{xmin};
261
+ $max = $$self{xmax} if defined $$self{xmax};
262
+ } else {
263
+ # leave a bit of a margin above/below data when autoscaling but don't pass 0
264
+ my $dny2 = ($max - $min) / ($ny * 2);
265
+ $min = ($min >= 0 and $min < $dny2) ? 0 : $min - $dny2;
266
+ $max = ($max <= 0 and -$max < $dny2) ? 0 : $max + $dny2;
267
+ # adjust to user-defined range if specified
268
+ $min = $$self{ymin} if defined $$self{ymin};
269
+ $max = $$self{ymax} if defined $$self{ymax};
270
+ }
271
+ # generate random colors if we need more
272
+ while (@$cols < @$name) {#138
273
+ $$self{seeded} or srand(141), $$self{seeded} = 1;
274
+ push @$cols, sprintf("#%.2x%.2x%.2x",int(rand(220)),int(rand(220)),int(rand(220)));
275
+ }
276
+ $diff = $max - $min || 1;
277
+ $xdiff = $xmax - $xmin || 1;
278
+
279
+ # determine y grid spacing (nice even numbers)
280
+ $dy = GetGridSpacing($diff / ($hist ? $$self{nbins} : $ny));
281
+ # expand plot min/max to the nearest even multiple of our grid spacing
282
+ $min = ($min > 0 ? int($min/$dy) : int($min/$dy-0.9999)) * $dy;
283
+ $max = ($max > 0 ? int($max/$dy+0.9999) : int($max/$dy)) * $dy;
284
+
285
+ # bin histogram
286
+ if ($hist) {
287
+ my $dat = $$data{$name[0]};
288
+ my $nmax = int(($max - $min) / $dy + 0.5);
289
+ @$hist = (0) x $nmax;
290
+ foreach (@$dat) {
291
+ next unless defined;
292
+ $n = ($_ - $min) / $dy;
293
+ next if $n < 0 or $n > $nmax + 0.00001;
294
+ $n = int($n);
295
+ ++$$hist[$n < $nmax ? $n : $nmax - 1];
296
+ }
297
+ ($xmin, $xmax, $min, $max) = ($min, $max, 0, 0);
298
+ if ($$self{ymax}) {
299
+ $max = $$self{ymax};
300
+ } else {
301
+ $max < $_ and $max = $_ foreach @$hist; # find max count
302
+ }
303
+ $diff = $max - $min || 1;
304
+ $dx = $dy;
305
+ $dy = GetGridSpacing($diff / $ny);
306
+ $max = ($max > 0 ? int($max/$dy+0.9999) : int($max/$dy)) * $dy;
307
+ $$data{$name[0]} = $hist;
308
+ } else {
309
+ $dx = GetGridSpacing($xdiff / $nx);
310
+ }
311
+ if ($scat) {
312
+ $xmin = ($xmin > 0 ? int($xmin/$dx) : int($xmin/$dx-0.9999)) * $dx;
313
+ $xmax = ($xmax > 0 ? int($xmax/$dx+0.9999) : int($xmax/$dx)) * $dx;
314
+ }
315
+ $diff = $max - $min || 1;
316
+ $xdiff = $xmax - $xmin || 1;
317
+ # width/height of plot area
318
+ my $width = $$self{size}[0] - $margin[0] - $margin[2];
319
+ my $height = $$self{size}[1] - $margin[1] - $margin[3];
320
+ my $yscl = $height / $diff;
321
+ my $xscl = $width / $xdiff;
322
+ my $px0 = $margin[0] - $xmin * $xscl;
323
+ my $py0 = $margin[1] + $height + $min * $yscl;
324
+ my $tmp = $title || "Plot by ExifTool $Image::ExifTool::VERSION";
325
+ print $fp qq{<?xml version="1.0" standalone="no"?>
326
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
327
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="$$self{size}[0]" height="$$self{size}[1]"
328
+ preserveAspectRatio="xMidYMid meet" viewBox="0 0 $$self{size}[0] $$self{size}[1]">
329
+ <title>$tmp</title>};
330
+ print $fp "<rect x='0' y='0' width='$$self{size}[0]' height='$$self{size}[1]' fill='$$self{bkg}'/>" if $$self{bkg};
331
+ print $fp "\n<!-- X axis -->";
332
+ print $fp "\n<g dominant-baseline='hanging' text-anchor='middle'>";
333
+ $py = int(($margin[1] + $height + $$self{txtPad}[1]) * 10 + 0.5) / 10;
334
+ $px = int(($margin[0] + $width / 2) * 10 + 0.5) / 10;
335
+ if ($title) {
336
+ print $fp "\n<text x='${px}' y='14' font-size='150%'>$title</text>";
337
+ }
338
+ if ($xlabel) {
339
+ $y = $py + $$self{lineSpacing};
340
+ print $fp "\n<text x='${px}' y='${y}'>$xlabel</text>";
341
+ }
342
+ if ($ylabel) {
343
+ $y = $margin[1] + $height / 2;
344
+ print $fp "\n<text x='10' y='${y}' transform='rotate(-90,10,$y)'>$ylabel</text>";
345
+ }
346
+ # check to be sure the X labels will fit
347
+ my $len = 0;
348
+ for ($i=0, $x=$xmax; $i<3; ++$i, $x-=$dx) {
349
+ $n = length sprintf('%g', $x);
350
+ $len = $n if $len < $n;
351
+ }
352
+ my $n = $wch * $len * $xdiff / $dx; # conservative length of all x-axis text
353
+ $dx2 = GetGridSpacing($dx * $n * 1.5 / 500) if $n > 500; # use larger label spacing
354
+ ($grid, $lastLen) = ('', 0);
355
+ for ($x=int($xmin/$dx-1)*$dx; ; $x+=$dx) {
356
+ $px = int(($margin[0] + ($x - $xmin) * $width / $xdiff) * 10 + 0.5) / 10;
357
+ next if $px < $margin[0] - 0.5;
358
+ last if $px > $margin[0] + $width + 0.5;
359
+ if (not $dx2 or $x/$dx2 - int($x/$dx2) < 0.1) {
360
+ printf $fp "\n<text x='${px}' y='${py}'>%g</text>", $x;
361
+ }
362
+ length($grid) - $lastLen > 80 and $grid .= "\n", $lastLen = length($grid);
363
+ $grid .= sprintf("M$px $margin[1] v$height ");
364
+ }
365
+ print $fp "\n<path stroke='$$self{grid}' stroke-width='0.5' d='\n${grid}'/>";
366
+ print $fp "\n</g>\n<!-- Y axis -->\n<g dominant-baseline='middle' text-anchor='end'>";
367
+ $px = int(($margin[0] - $$self{txtPad}[0]) * 10 + 0.5) / 10;
368
+ ($grid, $lastLen) = ('', 0);
369
+ for ($y=$min; ; $y+=$dy) {
370
+ $py = int(($margin[1] + $height - ($y - $min) * $yscl) * 10 + 0.5) / 10;
371
+ last if $py < $margin[1] - 0.5;
372
+ $y = 0 if $y < $dy/2 and $y > -$dy/2; # (avoid round-off errors)
373
+ printf $fp "\n<text x='${px}' y='${py}'>%g</text>", $y;
374
+ $y < $dy/2 and $y > -$dy/2 and $xAxis = 1, next; # draw x axis later
375
+ length($grid) - $lastLen > 80 and $grid .= "\n", $lastLen = length($grid);
376
+ $grid .= "M$margin[0] $py h$width ";
377
+ }
378
+ if ($xAxis and $min!=0) {
379
+ $py = $margin[1] + $height + $min * $yscl;
380
+ print $fp "\n<path stroke='$$self{text}' d='M$margin[0] $py h${width}'/>";
381
+ }
382
+ print $fp "\n<path stroke='$$self{grid}' stroke-width='0.5' d='\n${grid}'/>";
383
+ print $fp "\n</g>\n<!-- Plot box and legend-->\n<g dominant-baseline='middle' text-anchor='start'>";
384
+ print $fp "\n<path stroke='$$self{text}' fill='none' d='M$margin[0] $margin[1] l0 $height $width 0 0 -$height z'/>";
385
+ for ($i=0; $i<@name and not $noLegend; ++$i) {
386
+ next if $scat and not $i;
387
+ $x = $margin[0] + $$self{legend}[0] + 550;
388
+ $y = $margin[1] + $$self{legend}[1] + 15 + $$self{lineSpacing} * ($i - $scat + 0.5);
389
+ my $col = $$cols[$i];
390
+ my $mark = '';
391
+ if ($style =~ /\b[mp]/i) { # 'm' for 'marker' or 'p' for 'point' (undocumented)
392
+ my $id = $markerName[$i % @markerName];
393
+ $mark = " marker-end='url(#$id)' fill='none'";
394
+ }
395
+ my $line = ($style =~ /\bl/i) ? ' l-20 0' : '';
396
+ print $fp "\n<path$mark stroke-width='2' stroke='${col}' d='M$x $y m-7 -1${line}'/>";
397
+ print $fp "\n<text x='${x}' y='${y}'>$name[$i]</text>";
398
+ }
399
+ my @clip = ($margin[0]-6, $margin[1]-6, $width+12, $height+12);
400
+ print $fp "\n</g>\n<!-- Definitions -->\n<defs>";
401
+ print $fp "\n<clipPath id='plot-area'><rect x='$clip[0]' y='$clip[1]' width='$clip[2]' height='$clip[3]' /></clipPath>";
402
+ if ($style =~ /\b[mp]/i) {
403
+ for ($i=0; $i<@markerName and $i<@name; ++$i) {
404
+ print $fp "\n<marker id='@markerName[$i]' markerWidth='12' markerHeight='12' refX='6' refY='6' markerUnits='userSpaceOnUse'>";
405
+ my $mark = $markerData[$i];
406
+ $mark =~ s/"none"/"$$cols[$i]"/ if $style =~ /\bf/i;
407
+ print $fp "\n$mark\n</marker>";
408
+ }
409
+ print $fp "\n</defs>\n<style>";
410
+ for ($i=0; $i<@markerName and $i<@name; ++$i) {
411
+ print $fp "\n path.$markerName[$i] { marker: url(#$markerName[$i]) }";
412
+ }
413
+ print $fp "\n text { fill: $$self{text}] }";
414
+ print $fp "\n</style>";
415
+ } else {
416
+ print $fp "\n</defs><style>\n text { fill: $$self{text} }\n</style>";
417
+ }
418
+ print $fp "\n<g fill='none' clip-path='url(#plot-area)' stroke-linejoin='round' stroke-linecap='round' stroke-width='1.5'>";
419
+ foreach (0..$#name) {
420
+ $col{$name[$_]} = $$cols[$_];
421
+ $class{$name[$_]} = $style =~ /\b[mp]/i ? ' class="' . $markerName[$_ % @markerName] . '"' : '';
422
+ }
423
+ my ($i0, $i1, $xsclr);
424
+ my $fill = '';
425
+ if ($scat) {
426
+ ($i0, $i1) = (0, $#$xdat);
427
+ } elsif ($hist) {
428
+ ($i0, $i1) = (0, $#$hist);
429
+ $xscl = $width / @$hist;
430
+ $px0 = $margin[0];
431
+ $xsclr = int($xscl * 100 + 0.5) / 100;
432
+ $fill = qq( fill="$$cols[0]" style="fill-opacity: .20") if $$self{style} =~ /\bf/i;
433
+ } else {
434
+ $i0 = int($xmin) - 1;
435
+ $i0 = 0 if $i0 < 0;
436
+ $i1 = int($xmax) + 1;
437
+ }
438
+ foreach (@name) {
439
+ my $dat = $$data{$_};
440
+ my $doLines = $style =~ /\bl/i;
441
+ print $fp "\n<!-- $_ -->";
442
+ print $fp "\n<path$class{$_}$fill stroke='$col{$_}' d='";
443
+ print $fp 'M' if $doLines;
444
+ my $m = $doLines ? '' : ' M';
445
+ for ($i=$i0; $i<=$i1; ++$i) {
446
+ next unless defined $$dat[$i];
447
+ $y = int(($py0 - $$dat[$i] * $yscl) * 10 + 0.5) / 10;
448
+ if ($scat) {
449
+ next unless defined $$xdat[$i];
450
+ $x = int(($px0 + $$xdat[$i] * $xscl) * 10 + 0.5) / 10;
451
+ } else {
452
+ $x = int(($px0 + $i * $xscl) * 10 + 0.5) / 10;
453
+ if ($hist) {
454
+ print $fp $m, ($i % 5 ? ' ' : "\n"), "$x $y h$xsclr";
455
+ $m = ' L'; # (draw lines after the first point)
456
+ next;
457
+ }
458
+ }
459
+ print $fp $m, ($i % 20 ? ' ' : "\n"), "$x $y";
460
+ }
461
+ print $fp ' V', $margin[1]+$height, " H$margin[0] z" if $hist and $fill;
462
+ print $fp "'/>";
463
+ }
464
+ print $fp "\n</g></svg>\n";
465
+ }
466
+
467
+ 1; # end
468
+
469
+ __END__
470
+
471
+ =head1 NAME
472
+
473
+ Image::ExifTool::Plot - Plot tag values in SVG format
474
+
475
+ =head1 SYNOPSIS
476
+
477
+ This module is used by Image::ExifTool
478
+
479
+ =head1 DESCRIPTION
480
+
481
+ This module contains definitions required by Image::ExifTool to plot tag
482
+ values in SVG format.
483
+
484
+ =head1 AUTHOR
485
+
486
+ Copyright 2003-2025, Phil Harvey (philharvey66 at gmail.com)
487
+
488
+ This library is free software; you can redistribute it and/or modify it
489
+ under the same terms as Perl itself.
490
+
491
+ =cut
492
+
@@ -18,7 +18,7 @@ use strict;
18
18
  use vars qw($VERSION);
19
19
  use Image::ExifTool qw(:DataAccess :Utils);
20
20
 
21
- $VERSION = '1.02';
21
+ $VERSION = '1.03';
22
22
 
23
23
  sub ProcessProtobuf($$$;$);
24
24
 
@@ -174,11 +174,11 @@ sub ProcessProtobuf($$$;$)
174
174
  my $dir = { DataPt => \$buff, Pos => 0 };
175
175
  my $num = VarInt($dir);
176
176
  my $den = VarInt($dir);
177
- $val = (defined $num and $den) ? "$num/$den" : 'err';
177
+ $val = (defined $num and $den) ? $num/$den : 'err';
178
178
  } else {
179
179
  $val = ReadValue(\$buff, 0, $$tagInfo{Format}, undef, length($buff));
180
180
  }
181
- } elsif ($type == 0) {
181
+ } elsif ($type == 0) { # varInt
182
182
  $val = $buff;
183
183
  my $hex = sprintf('%x', $val);
184
184
  if (length($hex) == 16 and $hex =~ /^ffffffff/) {
@@ -188,9 +188,9 @@ sub ProcessProtobuf($$$;$)
188
188
  my $signed = ($val & 1) ? -($val >> 1)-1 : ($val >> 1);
189
189
  $val .= " (0x$hex, signed $signed)";
190
190
  }
191
- } elsif ($type == 1) {
191
+ } elsif ($type == 1) { # 64-bit number
192
192
  $val = '0x' . unpack('H*', $buff) . ' (double ' . GetDouble(\$buff,0) . ')';
193
- } elsif ($type == 2) {
193
+ } elsif ($type == 2) { # string, bytes or protobuf
194
194
  if ($$tagInfo{SubDirectory}) {
195
195
  # (fall through to process known SubDirectory)
196
196
  } elsif ($$tagInfo{IsProtobuf}) {
@@ -203,14 +203,25 @@ sub ProcessProtobuf($$$;$)
203
203
  ProcessProtobuf($et, \%subdir, $tagTbl, "$prefix$id-");
204
204
  $$et{INDENT} = substr($$et{INDENT}, 0, -2);
205
205
  next;
206
- } elsif ($buff !~ /[^\x20-\x7e]/) {
207
- $val = $buff; # assume this is an ASCII string
208
- } elsif (length($buff) % 4) {
209
- $val = '0x' . unpack('H*', $buff);
210
206
  } else {
211
- $val = '0x' . join(' ', unpack('(H8)*', $buff)); # (group in 4-byte blocks)
207
+ # check for rational value (2 varInt values)
208
+ my $rat;
209
+ my %dir = ( DataPt => \$buff, Pos => 0 );
210
+ my $num = VarInt(\%dir);
211
+ if (defined $num) {
212
+ my $denom = VarInt(\%dir);
213
+ $rat = " (rational $num/$denom)" if $denom and $dir{Pos} == length($buff);
214
+ }
215
+ if ($buff !~ /[^\x20-\x7e]/) {
216
+ $val = $buff; # assume this is an ASCII string
217
+ } elsif (length($buff) % 4) {
218
+ $val = '0x' . unpack('H*', $buff);
219
+ } else {
220
+ $val = '0x' . join(' ', unpack('(H8)*', $buff)); # (group in 4-byte blocks)
221
+ }
222
+ $val .= $rat if $rat;
212
223
  }
213
- } elsif ($type == 5) {
224
+ } elsif ($type == 5) { # 32-bit number
214
225
  $val = '0x' . unpack('H*', $buff) . ' (int32u ' . Get32u(\$buff, 0);
215
226
  $val .= ', int32s ' . Get32s(\$buff, 0) if ord(substr($buff,3,1)) & 0x80;
216
227
  $val .= ', float ' . GetFloat(\$buff, 0) . ')';