exiftool_vendored 13.19.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.
@@ -40,7 +40,7 @@ use Image::ExifTool qw(:DataAccess :Utils);
40
40
  use Image::ExifTool::Exif;
41
41
  use Image::ExifTool::APP12;
42
42
 
43
- $VERSION = '2.83';
43
+ $VERSION = '2.84';
44
44
 
45
45
  sub PrintLensInfo($$$);
46
46
 
@@ -442,6 +442,7 @@ my %olympusCameraTypes = (
442
442
  S0089 => 'E-M5MarkIII',
443
443
  S0092 => 'E-M1MarkIII', #IB
444
444
  S0093 => 'E-P7', #IB
445
+ S0094 => 'E-M10MarkIIIS', #forum17050
445
446
  S0095 => 'OM-1', #IB
446
447
  S0101 => 'OM-5', #IB
447
448
  S0121 => 'OM-1MarkII', #forum15652
@@ -132,6 +132,7 @@ my %plistType = (
132
132
  },
133
133
  adjustmentData => { # AAE file
134
134
  Name => 'AdjustmentData',
135
+ CompressedPLIST => 1,
135
136
  SubDirectory => { TagTable => 'Image::ExifTool::PLIST::Main' },
136
137
  },
137
138
  );
@@ -213,8 +214,22 @@ sub FoundTag($$$$;$)
213
214
  $$et{LastPListTag} = $tagInfo;
214
215
  # override file type if applicable
215
216
  $et->OverrideFileType($plistType{$tag}) if $plistType{$tag} and $$et{FILE_TYPE} eq 'XMP';
217
+ # handle compressed PLIST/JSON data
218
+ my $proc;
219
+ if ($$tagInfo{CompressedPLIST} and ref $val eq 'SCALAR' and $$val !~ /^bplist00/) {
220
+ if (eval { require IO::Uncompress::RawInflate }) {
221
+ my $inflated;
222
+ if (IO::Uncompress::RawInflate::rawinflate($val => \$inflated)) {
223
+ $val = \$inflated;
224
+ } else {
225
+ $et->Warn("Error inflating PLIST::$$tagInfo{Name}");
226
+ }
227
+ } else {
228
+ $et->Warn('Install IO::Uncompress to decode compressed PLIST data');
229
+ }
230
+ }
216
231
  # save the tag
217
- $et->HandleTag($tagTablePtr, $tag, $val);
232
+ $et->HandleTag($tagTablePtr, $tag, $val, ProcessProc => $proc);
218
233
 
219
234
  return 1;
220
235
  }
@@ -423,44 +438,54 @@ sub ProcessBinaryPLIST($$;$)
423
438
  }
424
439
 
425
440
  #------------------------------------------------------------------------------
426
- # Extract information from a PLIST file
441
+ # Extract information from a PLIST file (binary, XML or JSON format)
427
442
  # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
428
443
  # Returns: 1 on success, 0 if this wasn't valid PLIST
429
444
  sub ProcessPLIST($$;$)
430
445
  {
431
446
  my ($et, $dirInfo, $tagTablePtr) = @_;
447
+ my $dataPt = $$dirInfo{DataPt};
448
+ my ($result, $notXML);
432
449
 
433
- # process XML PLIST data using the XMP module
434
- $$dirInfo{XMPParseOpts}{FoundProc} = \&FoundTag;
435
- my $result = Image::ExifTool::XMP::ProcessXMP($et, $dirInfo, $tagTablePtr);
436
- delete $$dirInfo{XMPParseOpts};
437
-
438
- unless ($result) {
439
- my $buff;
440
- my $raf = $$dirInfo{RAF};
441
- if ($raf) {
442
- $raf->Seek(0,0) and $raf->Read($buff, 64) or return 0;
443
- } else {
444
- return 0 unless $$dirInfo{DataPt};
445
- $buff = ${$$dirInfo{DataPt}};
446
- }
447
- if ($buff =~ /^bplist0/) {
448
- # binary PLIST file
449
- my $tagTablePtr = GetTagTable('Image::ExifTool::PLIST::Main');
450
- $et->SetFileType('PLIST', 'application/x-plist');
451
- $$et{SET_GROUP1} = 'PLIST';
452
- unless (ProcessBinaryPLIST($et, $dirInfo, $tagTablePtr)) {
453
- $et->Error('Error reading binary PLIST file');
454
- }
455
- delete $$et{SET_GROUP1};
456
- $result = 1;
457
- } elsif ($$et{FILE_EXT} and $$et{FILE_EXT} eq 'PLIST' and
458
- $buff =~ /^\xfe\xff\x00/)
459
- {
460
- # (have seen very old PLIST files encoded as UCS-2BE with leading BOM)
461
- $et->Error('Old PLIST format currently not supported');
462
- $result = 1;
450
+ if ($dataPt) {
451
+ pos($$dataPt) = $$dirInfo{DirStart} || 0;
452
+ $notXML = 1 unless $$dataPt =~ /\G</g;
453
+ }
454
+ unless ($notXML) {
455
+ # process XML PLIST data using the XMP module
456
+ $$dirInfo{XMPParseOpts}{FoundProc} = \&FoundTag;
457
+ $result = Image::ExifTool::XMP::ProcessXMP($et, $dirInfo, $tagTablePtr);
458
+ delete $$dirInfo{XMPParseOpts};
459
+ return $result if $result;
460
+ }
461
+ my $buff;
462
+ my $raf = $$dirInfo{RAF};
463
+ if ($raf) {
464
+ $raf->Seek(0,0) and $raf->Read($buff, 64) or return 0;
465
+ $dataPt = \$buff;
466
+ } else {
467
+ return 0 unless $dataPt;
468
+ }
469
+ if ($$dataPt =~ /^bplist0/) { # binary PLIST
470
+ # binary PLIST file
471
+ my $tagTablePtr = GetTagTable('Image::ExifTool::PLIST::Main');
472
+ $et->SetFileType('PLIST', 'application/x-plist');
473
+ $$et{SET_GROUP1} = 'PLIST';
474
+ unless (ProcessBinaryPLIST($et, $dirInfo, $tagTablePtr)) {
475
+ $et->Error('Error reading binary PLIST file');
463
476
  }
477
+ delete $$et{SET_GROUP1};
478
+ $result = 1;
479
+ } elsif ($$dataPt =~ /^\{"/) { # JSON PLIST
480
+ $raf and $raf->Seek(0);
481
+ require Image::ExifTool::JSON;
482
+ $result = Image::ExifTool::JSON::ProcessJSON($et, $dirInfo);
483
+ } elsif ($$et{FILE_EXT} and $$et{FILE_EXT} eq 'PLIST' and
484
+ $$dataPt =~ /^\xfe\xff\x00/)
485
+ {
486
+ # (have seen very old PLIST files encoded as UCS-2BE with leading BOM)
487
+ $et->Error('Old PLIST format currently not supported');
488
+ $result = 1;
464
489
  }
465
490
  return $result;
466
491
  }
@@ -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
+