source: main/trunk/greenstone2/perllib/cpan/Image/ExifTool/Shift.pl@ 34921

Last change on this file since 34921 was 34921, checked in by anupama, 3 years ago

Committing the improvements to EmbeddedMetaPlugin's processing of Keywords vs other metadata fields. Keywords were literally stored as arrays of words rather than phrases in PDFs (at least in Diego's sample PDF), whereas other meta fields like Subjects and Creators stored them as arrays of phrases. To get both to work, Kathy updated EXIF to a newer version, to retrieve the actual EXIF values stored in the PDF. And Kathy and Dr Bainbridge came up with a new option that I added called apply_join_before_split_to_metafields that's a regex which can list the metadata fields to apply the join_before_split to and whcih previously always got applied to all metadata fields. Now it's applied to any *Keywords metafields by default, as that's the metafield we have experience of that behaves differently to the others, as it stores by word instead of phrases. Tested on Diego's sample PDF. Diego has double-checked it to works on his sample PDF too, setting the split char to ; and turning on the join_before_split and leaving apply_join_before_split_to_metafields at its default of .*Keywords. File changes are strings.properties for the tooltip, the plugin introducing the option and working with it and Kathy's EXIF updates affecting cpan/File and cpan/Image.

File size: 23.2 KB
Line 
1#------------------------------------------------------------------------------
2# File: Shift.pl
3#
4# Description: ExifTool time shifting routines
5#
6# Revisions: 10/28/2005 - P. Harvey Created
7# 03/13/2019 - PH Added single-argument form of ShiftTime()
8#------------------------------------------------------------------------------
9
10package Image::ExifTool;
11
12use strict;
13
14sub ShiftTime($;$$$);
15
16#------------------------------------------------------------------------------
17# apply shift to value in new value hash
18# Inputs: 0) ExifTool ref, 1) shift type, 2) shift string, 3) raw date/time value,
19# 4) new value hash ref
20# Returns: error string or undef on success and updates value in new value hash
21sub ApplyShift($$$$;$)
22{
23 my ($self, $func, $shift, $val, $nvHash) = @_;
24
25 # get shift direction from first character in shift string
26 my $pre = ($shift =~ s/^(\+|-)//) ? $1 : '+';
27 my $dir = ($pre eq '+') ? 1 : -1;
28 my $tagInfo = $$nvHash{TagInfo};
29 my $tag = $$tagInfo{Name};
30 my $shiftOffset;
31 if ($$nvHash{ShiftOffset}) {
32 $shiftOffset = $$nvHash{ShiftOffset};
33 } else {
34 $shiftOffset = $$nvHash{ShiftOffset} = { };
35 }
36
37 # initialize handler for eval warnings
38 local $SIG{'__WARN__'} = \&SetWarning;
39 SetWarning(undef);
40
41 # shift is applied to ValueConv value, so we must ValueConv-Shift-ValueConvInv
42 my ($type, $err);
43 foreach $type ('ValueConv','Shift','ValueConvInv') {
44 if ($type eq 'Shift') {
45 #### eval ShiftXxx function
46 $err = eval "Shift$func(\$val, \$shift, \$dir, \$shiftOffset)";
47 } elsif ($$tagInfo{$type}) {
48 my $conv = $$tagInfo{$type};
49 if (ref $conv eq 'CODE') {
50 $val = &$conv($val, $self);
51 } else {
52 return "Can't handle $type for $tag in ApplyShift()" if ref $$tagInfo{$type};
53 #### eval ValueConv/ValueConvInv ($val, $self)
54 $val = eval $$tagInfo{$type};
55 }
56 } else {
57 next;
58 }
59 # handle errors
60 $err and return $err;
61 $@ and SetWarning($@);
62 GetWarning() and return CleanWarning();
63 }
64 # update value in new value hash
65 $nvHash->{Value} = [ $val ];
66 return undef; # success
67}
68
69#------------------------------------------------------------------------------
70# Check date/time shift
71# Inputs: 0) shift type, 1) shift string (without sign)
72# Returns: updated shift string, or undef on error (and may update shift)
73sub CheckShift($$)
74{
75 my ($type, $shift) = @_;
76 my $err;
77 if ($type eq 'Time') {
78 return "No shift direction" unless $shift =~ s/^(\+|-)//;
79 # do a test shift to validate the shift string
80 my $testTime = '2005:11:02 09:00:13.25-04:00';
81 $err = ShiftTime($testTime, $shift, $1 eq '+' ? 1 : -1);
82 } else {
83 $err = "Unknown shift type ($type)";
84 }
85 return $err;
86}
87
88#------------------------------------------------------------------------------
89# return the number of days in a month
90# Inputs: 0) month number (Jan=1, may be outside range), 1) year
91# Returns: number of days in month
92sub DaysInMonth($$)
93{
94 my ($mon, $year) = @_;
95 my @days = (31,28,31,30,31,30,31,31,30,31,30,31);
96 # adjust to the range [0,11]
97 while ($mon < 1) { $mon += 12; --$year; }
98 while ($mon > 12) { $mon -= 12; ++$year; }
99 # return standard number of days unless february on a leap year
100 return $days[$mon-1] unless $mon == 2 and not $year % 4;
101 # leap years don't occur on even centuries except every 400 years
102 return 29 if $year % 100 or not $year % 400;
103 return 28;
104}
105
106#------------------------------------------------------------------------------
107# split times into corresponding components: YYYY mm dd HH MM SS tzh tzm
108# Inputs: 0) date/time or shift string 1) reference to list for returned components
109# 2) optional reference to list of time components (if shift string)
110# Returns: true on success
111# Returned components are 0-Y, 1-M, 2-D, 3-hr, 4-min, 5-sec, 6-tzhr, 7-tzmin
112sub SplitTime($$;$)
113{
114 my ($val, $vals, $time) = @_;
115 # insert zeros if missing in shift string
116 if ($time) {
117 $val =~ s/(^|[-+:\s]):/${1}0:/g;
118 $val =~ s/:([:\s]|$)/:0$1/g;
119 }
120 # change dashes to colons in date (for XMP dates)
121 if ($val =~ s/^(\d{4})-(\d{2})-(\d{2})/$1:$2:$3/) {
122 $val =~ tr/T/ /; # change 'T' separator to ' '
123 }
124 # add space before timezone to split it into a separate word
125 $val =~ s/(\+|-)/ $1/;
126 my @words = split ' ', $val;
127 my $err = 1;
128 my @v;
129 for (;;) {
130 my $word = shift @words;
131 last unless defined $word;
132 # split word into separate numbers (allow decimal points but no signs)
133 my @vals = $word =~ /(?=\d|\.\d)\d*(?:\.\d*)?/g or last;
134 if ($word =~ /^(\+|-)/) {
135 # this is the timezone
136 (defined $v[6] or @vals > 2) and $err = 1, last;
137 my $sign = ($1 ne '-') ? 1 : -1;
138 # apply sign to both minutes and seconds
139 $v[6] = $sign * shift(@vals);
140 $v[7] = $sign * (shift(@vals) || 0);
141 } elsif ((@words and $words[0] =~ /^\d+/) or # there is a time word to follow
142 (not $time and $vals[0] =~ /^\d{3}/) or # first value is year (3 or more digits)
143 ($time and not defined $$time[3] and not defined $v[0])) # we don't have a time
144 {
145 # this is a date (must come first)
146 (@v or @vals > 3) and $err = 1, last;
147 not $time and @vals != 3 and $err = 1, last;
148 $v[2] = pop(@vals); # take day first if only one specified
149 $v[1] = pop(@vals) || 0;
150 $v[0] = pop(@vals) || 0;
151 } else {
152 # this is a time (can't come after timezone)
153 (defined $v[3] or defined $v[6] or @vals > 3) and $err = 1, last;
154 not $time and @vals != 3 and @vals != 2 and $err = 1, last;
155 $v[3] = shift(@vals); # take hour first if only one specified
156 $v[4] = shift(@vals) || 0;
157 $v[5] = shift(@vals) || 0;
158 }
159 $err = 0;
160 }
161 return 0 if $err or not @v;
162 if ($time) {
163 # zero any required shift entries which aren't yet defined
164 $v[0] = $v[1] = $v[2] = 0 if defined $$time[0] and not defined $v[0];
165 $v[3] = $v[4] = $v[5] = 0 if defined $$time[3] and not defined $v[3];
166 $v[6] = $v[7] = 0 if defined $$time[6] and not defined $v[6];
167 }
168 @$vals = @v; # return split time components
169 return 1;
170}
171
172#------------------------------------------------------------------------------
173# shift date/time by components
174# Inputs: 0) split date/time list ref, 1) split shift list ref,
175# 2) shift direction, 3) reference to output list of shifted components
176# 4) number of decimal points in seconds
177# 5) reference to return time difference due to rounding
178# Returns: error string or undef on success
179sub ShiftComponents($$$$$;$)
180{
181 my ($time, $shift, $dir, $toTime, $dec, $rndPt) = @_;
182 # min/max for Y, M, D, h, m, s
183 my @min = ( 0, 1, 1, 0, 0, 0);
184 my @max = (10000,12,28,24,60,60);
185 my $i;
186#
187# apply the shift
188#
189 my $c = 0;
190 for ($i=0; $i<@$time; ++$i) {
191 my $v = ($$time[$i] || 0) + $dir * ($$shift[$i] || 0) + $c;
192 # handle fractional values by propagating remainders downwards
193 if ($v != int($v) and $i < 5) {
194 my $iv = int($v);
195 $c = ($v - $iv) * $max[$i+1];
196 $v = $iv;
197 } else {
198 $c = 0;
199 }
200 $$toTime[$i] = $v;
201 }
202 # round off seconds to the required number of decimal points
203 my $sec = $$toTime[5];
204 if (defined $sec and $sec != int($sec)) {
205 my $mult = 10 ** $dec;
206 my $rndSec = int($sec * $mult + 0.5 * ($sec <=> 0)) / $mult;
207 $rndPt and $$rndPt = $sec - $rndSec;
208 $$toTime[5] = $rndSec;
209 }
210#
211# handle overflows, starting with least significant number first (seconds)
212#
213 $c = 0;
214 for ($i=5; $i>=0; $i--) {
215 defined $$time[$i] or $c = 0, next;
216 # apply shift and adjust for previous overflow
217 my $v = $$toTime[$i] + $c;
218 $c = 0; # set carry to zero
219 # adjust for over/underflow
220 my ($min, $max) = ($min[$i], $max[$i]);
221 if ($v < $min) {
222 if ($i == 2) { # 2 = day of month
223 do {
224 # add number of days in previous month
225 --$c;
226 my $mon = $$toTime[$i-1] + $c;
227 $v += DaysInMonth($mon, $$toTime[$i-2]);
228 } while ($v < 1);
229 } else {
230 my $fc = ($v - $min) / $max;
231 # carry ($c) must be largest integer equal to or less than $fc
232 $c = int($fc);
233 --$c if $c > $fc;
234 $v -= $c * $max;
235 }
236 } elsif ($v >= $max + $min) {
237 if ($i == 2) {
238 for (;;) {
239 # test against number of days in current month
240 my $mon = $$toTime[$i-1] + $c;
241 my $days = DaysInMonth($mon, $$toTime[$i-2]);
242 last if $v <= $days;
243 $v -= $days;
244 ++$c;
245 last if $v <= 28;
246 }
247 } else {
248 my $fc = ($v - $max - $min) / $max;
249 # carry ($c) must be smallest integer greater than $fc
250 $c = int($fc);
251 ++$c if $c <= $fc;
252 $v -= $c * $max;
253 }
254 }
255 $$toTime[$i] = $v; # save the new value
256 }
257 # handle overflows in timezone
258 if (defined $$toTime[6]) {
259 my $m = $$toTime[6] * 60 + $$toTime[7];
260 $m += 0.5 * ($m <=> 0); # avoid round-off errors
261 $$toTime[6] = int($m / 60);
262 $$toTime[7] = int($m - $$toTime[6] * 60);
263 }
264 return undef; # success
265}
266
267#------------------------------------------------------------------------------
268# Shift an integer or floating-point number
269# Inputs: 0) date/time string, 1) shift string, 2) shift direction (+1 or -1)
270# 3) (unused)
271# Returns: undef and updates input value
272sub ShiftNumber($$$;$)
273{
274 my ($val, $shift, $dir) = @_;
275 $_[0] = $val + $shift * $dir; # return shifted value
276 return undef; # success!
277}
278
279#------------------------------------------------------------------------------
280# Shift date/time string
281# Inputs: 0) date/time string, 1) shift string, 2) shift direction (+1 or -1),
282# or 0 or undef to take shift direction from sign of shift,
283# 3) reference to ShiftOffset hash (with Date, DateTime, Time, Timezone keys)
284# or 0) shift string (and operates on $_)
285# Returns: error string or undef on success and date/time string is updated
286sub ShiftTime($;$$$)
287{
288 my ($val, $shift, $dir, $shiftOffset);
289 my (@time, @shift, @toTime, $mode, $needShiftOffset, $dec);
290
291 if (@_ == 1) { # single argument form of ShiftTime()?
292 $val = $_;
293 $shift = $_[0];
294 } else {
295 ($val, $shift, $dir, $shiftOffset) = @_;
296 }
297 $dir or $dir = ($shift =~ s/^(\+|-)// and $1 eq '-') ? -1 : 1;
298#
299# figure out what we are dealing with (time, date or date/time)
300#
301 SplitTime($val, \@time) or return "Invalid time string ($val)";
302 if (defined $time[0]) {
303 return "Can't shift from year 0000" if $time[0] eq '0000';
304 $mode = defined $time[3] ? 'DateTime' : 'Date';
305 } elsif (defined $time[3]) {
306 $mode = 'Time';
307 }
308 # get number of digits after the seconds decimal point
309 if (defined $time[5] and $time[5] =~ /\.(\d+)/) {
310 $dec = length($1);
311 } else {
312 $dec = 0;
313 }
314 if ($shiftOffset) {
315 $needShiftOffset = 1 unless defined $$shiftOffset{$mode};
316 $needShiftOffset = 1 if defined $time[6] and not defined $$shiftOffset{Timezone};
317 } else {
318 $needShiftOffset = 1;
319 }
320 if ($needShiftOffset) {
321#
322# apply date/time shift the hard way
323#
324 SplitTime($shift, \@shift, \@time) or return "Invalid shift string ($shift)";
325
326 # change 'Z' timezone to '+00:00' only if necessary
327 if (@shift > 6 and @time <= 6) {
328 $time[6] = $time[7] = 0 if $val =~ s/Z$/\+00:00/;
329 }
330 my $rndDiff;
331 my $err = ShiftComponents(\@time, \@shift, $dir, \@toTime, $dec, \$rndDiff);
332 $err and return $err;
333#
334# calculate and save the shift offsets for next time
335#
336 if ($shiftOffset) {
337 if (defined $time[0] or defined $time[3]) {
338 my @tm1 = (0, 0, 0, 1, 0, 2000);
339 my @tm2 = (0, 0, 0, 1, 0, 2000);
340 if (defined $time[0]) {
341 @tm1[3..5] = reverse @time[0..2];
342 @tm2[3..5] = reverse @toTime[0..2];
343 --$tm1[4]; # month should start from 0
344 --$tm2[4];
345 }
346 my $diff = 0;
347 if (defined $time[3]) {
348 @tm1[0..2] = reverse @time[3..5];
349 @tm2[0..2] = reverse @toTime[3..5];
350 # handle fractional seconds separately
351 $diff = $tm2[0] - int($tm2[0]) - ($tm1[0] - int($tm1[0]));
352 $diff += $rndDiff if defined $rndDiff; # un-do rounding
353 $tm1[0] = int($tm1[0]);
354 $tm2[0] = int($tm2[0]);
355 }
356 eval q{
357 require Time::Local;
358 $diff += Time::Local::timegm(@tm2) - Time::Local::timegm(@tm1);
359 };
360 # not a problem if we failed here since we'll just try again next time,
361 # so don't return error message
362 unless (@$) {
363 my $mode;
364 if (defined $time[0]) {
365 $mode = defined $time[3] ? 'DateTime' : 'Date';
366 } else {
367 $mode = 'Time';
368 }
369 $$shiftOffset{$mode} = $diff;
370 }
371 }
372 if (defined $time[6]) {
373 $$shiftOffset{Timezone} = ($toTime[6] - $time[6]) * 60 +
374 $toTime[7] - $time[7];
375 }
376 }
377
378 } else {
379#
380# apply shift from previously calculated offsets
381#
382 if ($$shiftOffset{Timezone} and @time <= 6) {
383 # change 'Z' timezone to '+00:00' only if necessary
384 $time[6] = $time[7] = 0 if $val =~ s/Z$/\+00:00/;
385 }
386 # apply the previous date/time shift if necessary
387 if ($mode) {
388 my @tm = (0, 0, 0, 1, 0, 2000);
389 if (defined $time[0]) {
390 @tm[3..5] = reverse @time[0..2];
391 --$tm[4]; # month should start from 0
392 }
393 @tm[0..2] = reverse @time[3..5] if defined $time[3];
394 # save fractional seconds
395 my $frac = $tm[0] - int($tm[0]);
396 $tm[0] = int($tm[0]);
397 my $tm;
398 eval q{
399 require Time::Local;
400 $tm = Time::Local::timegm(@tm) + $frac;
401 };
402 $@ and return CleanWarning($@);
403 $tm += $$shiftOffset{$mode}; # apply the shift
404 $tm < 0 and return 'Shift results in negative time';
405 # save fractional seconds in shifted time
406 $frac = $tm - int($tm);
407 if ($frac) {
408 $tm = int($tm);
409 # must account for any rounding that could occur
410 $frac + 0.5 * 10 ** (-$dec) >= 1 and ++$tm, $frac = 0;
411 }
412 @tm = gmtime($tm);
413 @toTime = reverse @tm[0..5];
414 $toTime[0] += 1900;
415 ++$toTime[1];
416 $toTime[5] += $frac; # add the fractional seconds back in
417 }
418 # apply the previous timezone shift if necessary
419 if (defined $time[6]) {
420 my $m = $time[6] * 60 + $time[7];
421 $m += $$shiftOffset{Timezone};
422 $m += 0.5 * ($m <=> 0); # avoid round-off errors
423 $toTime[6] = int($m / 60);
424 $toTime[7] = int($m - $toTime[6] * 60);
425 }
426 }
427#
428# insert shifted time components back into original string
429#
430 my $i;
431 for ($i=0; $i<@toTime; ++$i) {
432 next unless defined $time[$i] and defined $toTime[$i];
433 my ($v, $d, $s);
434 if ($i != 6) { # not timezone hours
435 last unless $val =~ /((?=\d|\.\d)\d*(\.\d*)?)/g;
436 next if $toTime[$i] == $time[$i];
437 $v = $1; # value
438 $d = $2; # decimal part of value
439 $s = ''; # no sign
440 } else {
441 last if $time[$i] == $toTime[$i] and $time[$i+1] == $toTime[$i+1];
442 last unless $val =~ /((?:\+|-)(?=\d|\.\d)\d*(\.\d*)?)/g;
443 $v = $1;
444 $d = $2;
445 if ($toTime[6] >= 0 and $toTime[7] >= 0) {
446 $s = '+';
447 } else {
448 $s = '-';
449 $toTime[6] = -$toTime[6];
450 $toTime[7] = -$toTime[7];
451 }
452 }
453 my $nv = $toTime[$i];
454 my $pos = pos $val;
455 my $len = length $v;
456 my $sig = $len - length $s;
457 my $dec = $d ? length($d) - 1 : 0;
458 my $newNum = sprintf($dec ? "$s%0$sig.${dec}f" : "$s%0${sig}d", $nv);
459 substr($val, $pos - $len, $len) = $newNum;
460 pos($val) = $pos + length($newNum) - $len;
461 }
462 if (@_ == 1) {
463 $_ = $val; # set $_ to the returned value
464 } else {
465 $_[0] = $val; # return shifted value
466 }
467 return undef; # success!
468}
469
470
4711; # end
472
473__END__
474
475=head1 NAME
476
477Image::ExifTool::Shift.pl - ExifTool time shifting routines
478
479=head1 DESCRIPTION
480
481This module contains routines used by ExifTool to shift date and time
482values.
483
484=head1 METHODS
485
486=head2 ShiftTime
487
488Shift date/time value
489
490 use Image::ExifTool;
491 $err = Image::ExifTool::ShiftTime($dateTime, $shift);
492
493=over 4
494
495=item Inputs:
496
4970) Date/time string in EXIF format (eg. C<2016:01:30 11:45:00>).
498
4991) Shift string (see below) with optional leading sign for shift direction.
500
5012) [optional] Direction of shift (-1 or +1), or 0 or undef to use the sign
502from the shift string.
503
5043) [optional] Reference to time-shift hash -- filled in by first call to
505B<ShiftTime>, and used in subsequent calls to shift date/time values by the
506same relative amount (see L</TRICKY> section below).
507
508or
509
5100) Shift string (and $_ contains the input date/time string).
511
512=item Return value:
513
514Error string, or undef on success and the input date/time string is shifted
515by the specified amount.
516
517=back
518
519=head1 SHIFT STRING
520
521Time shifts are applied to standard EXIF-formatted date/time values (eg.
522C<2005:03:14 18:55:00>). Date-only and time-only values may also be
523shifted, and an optional timezone (eg. C<-05:00>) is also supported. Here
524are some general rules and examples to explain how shift strings are
525interpreted:
526
527Date-only values are shifted using the following formats:
528
529 'Y:M:D' - shift date by 'Y' years, 'M' months and 'D' days
530 'M:D' - shift months and days only
531 'D' - shift specified number of days
532
533Time-only values are shifted using the following formats:
534
535 'h:m:s' - shift time by 'h' hours, 'm' minutes and 's' seconds
536 'h:m' - shift hours and minutes only
537 'h' - shift specified number of hours
538
539Timezone shifts are specified in the following formats:
540
541 '+h:m' - shift timezone by 'h' hours and 'm' minutes
542 '-h:m' - negative shift of timezone hours and minutes
543 '+h' - shift timezone hours only
544 '-h' - negative shift of timezone hours only
545
546A valid shift value consists of one or two arguments, separated by a space.
547If only one is provided, it is assumed to be a time shift when applied to a
548time-only or a date/time value, or a date shift when applied to a date-only
549value. For example:
550
551 '1' - shift by 1 hour if applied to a time or date/time
552 value, or by one day if applied to a date value
553 '2:0' - shift 2 hours (time, date/time), or 2 months (date)
554 '5:0:0' - shift 5 hours (time, date/time), or 5 years (date)
555 '0:0:1' - shift 1 s (time, date/time), or 1 day (date)
556
557If two arguments are given, the date shift is first, followed by the time
558shift:
559
560 '3:0:0 0' - shift date by 3 years
561 '0 15:30' - shift time by 15 hours and 30 minutes
562 '1:0:0 0:0:0+5:0' - shift date by 1 year and timezone by 5 hours
563
564A date shift is simply ignored if applied to a time value or visa versa.
565
566Numbers specified in shift fields may contain a decimal point:
567
568 '1.5' - 1 hour 30 minutes (time, date/time), or 1 day (date)
569 '2.5 0' - 2 days 12 hours (date/time), 12 hours (time) or
570 2 days (date)
571
572And to save typing, a zero is assumed for any missing numbers:
573
574 '1::' - shift by 1 hour (time, date/time) or 1 year (date)
575 '26:: 0' - shift date by 26 years
576 '+:30' - shift timezone by 30 minutes
577
578Below are some specific examples applied to real date and/or time values
579('Dir' is the applied shift direction: '+' is positive, '-' is negative):
580
581 Original Value Shift Dir Shifted Value
582 --------------------- ------- --- ---------------------
583 '20:30:00' '5' + '01:30:00'
584 '2005:01:27' '5' + '2005:02:01'
585 '2005:01:27 20:30:00' '5' + '2005:01:28 01:30:00'
586 '11:54:00' '2.5 0' - '23:54:00'
587 '2005:11:02' '2.5 0' - '2005:10:31'
588 '2005:11:02 11:54:00' '2.5 0' - '2005:10:30 23:54:00'
589 '2004:02:28 08:00:00' '1 1.3' + '2004:02:29 09:18:00'
590 '07:00:00' '-5' + '07:00:00'
591 '07:00:00+01:00' '-5' + '07:00:00-04:00'
592 '07:00:00Z' '+2:30' - '07:00:00-02:30'
593 '1970:01:01' '35::' + '2005:01:01'
594 '2005:01:01' '400' + '2006:02:05'
595 '10:00:00.00' '::1.33' - '09:59:58.67'
596
597=head1 NOTES
598
599The format of the original date/time value is not changed when the time
600shift is applied. This means that the length of the date/time string will
601not change, and only the numbers in the string will be modified. The only
602exception to this rule is that a 'Z' timezone is changed to '+00:00'
603notation if a timezone shift is applied. A timezone will not be added to
604the date/time string.
605
606=head1 TRICKY
607
608This module is perhaps more complicated than it needs to be because it is
609designed to be very flexible in the way time shifts are specified and
610applied...
611
612The ability to shift dates by Y years, M months, etc, conflicts with the
613design goal of maintaining a constant shift for all time values when
614applying a batch shift. This is because shifting by 1 month can be
615equivalent to anything from 28 to 31 days, and 1 year can be 365 or 366
616days, depending on the starting date.
617
618The inconsistency is handled by shifting the first tag found with the actual
619specified shift, then calculating the equivalent time difference in seconds
620for this shift and applying this difference to subsequent tags in a batch
621conversion. So if it works as designed, the behaviour should be both
622intuitive and mathematically correct, and the user shouldn't have to worry
623about details such as this (in keeping with Perl's "do the right thing"
624philosophy).
625
626=head1 BUGS
627
628Due to the use of the standard time library functions, dates are typically
629limited to the range 1970 to 2038 on 32-bit systems.
630
631=head1 AUTHOR
632
633Copyright 2003-2021, Phil Harvey (philharvey66 at gmail.com)
634
635This library is free software; you can redistribute it and/or modify it
636under the same terms as Perl itself.
637
638=head1 SEE ALSO
639
640L<Image::ExifTool(3pm)|Image::ExifTool>
641
642=cut
Note: See TracBrowser for help on using the repository browser.