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.
- checksums.yaml +4 -4
- data/bin/Changes +25 -2
- data/bin/MANIFEST +6 -0
- data/bin/META.json +1 -1
- data/bin/META.yml +1 -1
- data/bin/README +2 -2
- data/bin/exiftool +138 -64
- data/bin/lib/Image/ExifTool/DJI.pm +85 -23
- data/bin/lib/Image/ExifTool/EXE.pm +2 -9
- data/bin/lib/Image/ExifTool/GoPro.pm +28 -0
- data/bin/lib/Image/ExifTool/JSON.pm +5 -1
- data/bin/lib/Image/ExifTool/Nikon.pm +808 -524
- data/bin/lib/Image/ExifTool/Olympus.pm +2 -1
- data/bin/lib/Image/ExifTool/PLIST.pm +57 -32
- data/bin/lib/Image/ExifTool/Plot.pm +492 -0
- data/bin/lib/Image/ExifTool/Protobuf.pm +21 -10
- data/bin/lib/Image/ExifTool/QuickTime.pm +41 -31
- data/bin/lib/Image/ExifTool/QuickTimeStream.pl +56 -22
- data/bin/lib/Image/ExifTool/RIFF.pm +3 -3
- data/bin/lib/Image/ExifTool/TagLookup.pm +4988 -4965
- data/bin/lib/Image/ExifTool/TagNames.pod +155 -65
- data/bin/lib/Image/ExifTool/WritePDF.pl +1 -1
- data/bin/lib/Image/ExifTool/Writer.pl +0 -1
- data/bin/lib/Image/ExifTool/XMP.pm +1 -1
- data/bin/lib/Image/ExifTool.pm +16 -2
- data/bin/lib/Image/ExifTool.pod +27 -0
- data/bin/perl-Image-ExifTool.spec +1 -1
- data/lib/exiftool_vendored/version.rb +1 -1
- metadata +3 -2
@@ -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.
|
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
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
$
|
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/&(#(\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
|
+
|