File Coverage

blib/lib/Image/ExifTool/FlashPix.pm
Criterion Covered Total %
statement 396 694 57.0
branch 194 434 44.7
condition 56 178 31.4
subroutine 15 20 75.0
pod 0 14 0.0
total 661 1340 49.3


line stmt bran cond sub pod time code
1             #------------------------------------------------------------------------------
2             # File: FlashPix.pm
3             #
4             # Description: Read FlashPix meta information
5             #
6             # Revisions: 05/29/2006 - P. Harvey Created
7             #
8             # References: 1) http://www.exif.org/Exif2-2.PDF
9             # 2) http://www.graphcomp.com/info/specs/livepicture/fpx.pdf
10             # 3) http://search.cpan.org/~jdb/libwin32/
11             # 4) http://msdn.microsoft.com/en-us/library/aa380374.aspx
12             # 5) http://www.cpan.org/modules/by-authors/id/H/HC/HCARVEY/File-MSWord-0.1.zip
13             # 6) https://msdn.microsoft.com/en-us/library/cc313153(v=office.12).aspx
14             # 7) https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-oshared/3ef02e83-afef-4b6c-9585-c109edd24e07
15             #------------------------------------------------------------------------------
16              
17             package Image::ExifTool::FlashPix;
18              
19 6     6   7229 use strict;
  6         12  
  6         330  
20 6     6   36 use vars qw($VERSION);
  6         14  
  6         404  
21 6     6   32 use Image::ExifTool qw(:DataAccess :Utils);
  6         13  
  6         1832  
22 6     6   1527 use Image::ExifTool::Exif;
  6         16  
  6         342  
23 6     6   4647 use Image::ExifTool::ASF; # for GetGUID()
  6         27  
  6         386  
24 6     6   4550 use Image::ExifTool::Microsoft; # for %codePage
  6         31  
  6         67624  
25              
26             $VERSION = '1.51';
27              
28             sub ProcessFPX($$);
29             sub ProcessFPXR($$$);
30             sub ProcessProperties($$$);
31             sub ReadFPXValue($$$$$;$$);
32             sub ProcessHyperlinks($$);
33             sub ProcessContents($$$);
34             sub ProcessWordDocument($$$);
35             sub ProcessDocumentTable($);
36             sub ProcessCommentBy($$$);
37             sub ProcessLastSavedBy($$$);
38             sub SetDocNum($$;$$$);
39             sub ConvertDTTM($);
40              
41             # sector type constants
42             sub HDR_SIZE () { 512; }
43             sub DIF_SECT () { 0xfffffffc; }
44             sub FAT_SECT () { 0xfffffffd; }
45             sub END_OF_CHAIN () { 0xfffffffe; }
46             sub FREE_SECT () { 0xffffffff; }
47              
48             # format flags
49             sub VT_VECTOR () { 0x1000; }
50             sub VT_ARRAY () { 0x2000; }
51             sub VT_BYREF () { 0x4000; }
52             sub VT_RESERVED () { 0x8000; }
53              
54             # other constants
55             sub VT_VARIANT () { 12; }
56             sub VT_LPSTR () { 30; }
57              
58             # list of OLE format codes (unsupported codes commented out)
59             my %oleFormat = (
60             0 => undef, # VT_EMPTY
61             1 => undef, # VT_NULL
62             2 => 'int16s', # VT_I2
63             3 => 'int32s', # VT_I4
64             4 => 'float', # VT_R4
65             5 => 'double', # VT_R8
66             6 => undef, # VT_CY
67             7 => 'VT_DATE', # VT_DATE (double, number of days since Dec 30, 1899)
68             8 => 'VT_BSTR', # VT_BSTR (int32u count, followed by binary string)
69             # 9 => 'VT_DISPATCH',
70             10 => 'int32s', # VT_ERROR
71             11 => 'int16s', # VT_BOOL
72             12 => 'VT_VARIANT', # VT_VARIANT
73             # 13 => 'VT_UNKNOWN',
74             # 14 => 'VT_DECIMAL',
75             16 => 'int8s', # VT_I1
76             17 => 'int8u', # VT_UI1
77             18 => 'int16u', # VT_UI2
78             19 => 'int32u', # VT_UI4
79             20 => 'int64s', # VT_I8
80             21 => 'int64u', # VT_UI8
81             # 22 => 'VT_INT',
82             # 23 => 'VT_UINT',
83             # 24 => 'VT_VOID',
84             # 25 => 'VT_HRESULT',
85             # 26 => 'VT_PTR',
86             # 27 => 'VT_SAFEARRAY',
87             # 28 => 'VT_CARRAY',
88             # 29 => 'VT_USERDEFINED',
89             30 => 'VT_LPSTR', # VT_LPSTR (int32u count, followed by string)
90             31 => 'VT_LPWSTR', # VT_LPWSTR (int32u word count, followed by Unicode string)
91             64 => 'VT_FILETIME',# VT_FILETIME (int64u, 100 ns increments since Jan 1, 1601)
92             65 => 'VT_BLOB', # VT_BLOB
93             # 66 => 'VT_STREAM',
94             # 67 => 'VT_STORAGE',
95             # 68 => 'VT_STREAMED_OBJECT',
96             # 69 => 'VT_STORED_OBJECT',
97             # 70 => 'VT_BLOB_OBJECT',
98             71 => 'VT_CF', # VT_CF
99             72 => 'VT_CLSID', # VT_CLSID
100             );
101              
102             # OLE flag codes (high nibble of property type)
103             my %oleFlags = (
104             0x1000 => 'VT_VECTOR',
105             0x2000 => 'VT_ARRAY', # not yet supported
106             0x4000 => 'VT_BYREF', # ditto
107             0x8000 => 'VT_RESERVED',
108             );
109              
110             # byte sizes for supported VT_* format and flag types
111             my %oleFormatSize = (
112             VT_DATE => 8,
113             VT_BSTR => 4, # (+ string length)
114             VT_VARIANT => 4, # (+ data length)
115             VT_LPSTR => 4, # (+ string length)
116             VT_LPWSTR => 4, # (+ string character length)
117             VT_FILETIME => 8,
118             VT_BLOB => 4, # (+ data length)
119             VT_CF => 4, # (+ data length)
120             VT_CLSID => 16,
121             VT_VECTOR => 4, # (+ vector elements)
122             );
123              
124             # names for each type of directory entry
125             my @dirEntryType = qw(INVALID STORAGE STREAM LOCKBYTES PROPERTY ROOT);
126              
127             # test for file extensions which may be variants of the FPX format
128             # (have seen one password-protected DOCX file that is FPX-like, so assume
129             # that all the rest could be as well)
130             my %fpxFileType = (
131             DOC => 1, DOCX => 1, DOCM => 1,
132             DOT => 1, DOTX => 1, DOTM => 1,
133             POT => 1, POTX => 1, POTM => 1,
134             PPS => 1, PPSX => 1, PPSM => 1,
135             PPT => 1, PPTX => 1, PPTM => 1, THMX => 1,
136             XLA => 1, XLAM => 1,
137             XLS => 1, XLSX => 1, XLSM => 1, XLSB => 1,
138             XLT => 1, XLTX => 1, XLTM => 1,
139             # non MSOffice types
140             FLA => 1, VSD => 1,
141             );
142              
143             %Image::ExifTool::FlashPix::Main = (
144             PROCESS_PROC => \&ProcessFPXR,
145             GROUPS => { 2 => 'Image' },
146             VARS => { LONG_TAGS => 0 },
147             NOTES => q{
148             The FlashPix file format, introduced in 1996, was developed by Kodak,
149             Hewlett-Packard and Microsoft. Internally the FPX file structure mimics
150             that of an old DOS disk with fixed-sized "sectors" (usually 512 bytes) and a
151             "file allocation table" (FAT). No wonder this image format never became
152             popular. However, some of the structures used in FlashPix streams are part
153             of the EXIF specification, and are still being used in the APP2 FPXR segment
154             of JPEG images by some digital cameras from manufacturers such as FujiFilm,
155             Hewlett-Packard, Kodak and Sanyo.
156              
157             ExifTool extracts FlashPix information from both FPX images and the APP2
158             FPXR segment of JPEG images. As well, FlashPix information is extracted
159             from DOC, PPT, XLS (Microsoft Word, PowerPoint and Excel) documents, VSD
160             (Microsoft Visio) drawings, and FLA (Macromedia/Adobe Flash project) files
161             since these are based on the same file format as FlashPix (the Windows
162             Compound Binary File format). Note that ExifTool identifies any
163             unrecognized Windows Compound Binary file as a FlashPix (FPX) file. See
164             L for the FlashPix
165             specification.
166              
167             Note that Microsoft is not consistent with the time zone used for some
168             date/time tags, and it may be either UTC or local time depending on the
169             software used to create the file.
170             },
171             "\x05SummaryInformation" => {
172             Name => 'SummaryInfo',
173             SubDirectory => {
174             TagTable => 'Image::ExifTool::FlashPix::SummaryInfo',
175             },
176             },
177             "\x05DocumentSummaryInformation" => {
178             Name => 'DocumentInfo',
179             Multi => 1, # flag to process UserDefined information after this
180             SubDirectory => {
181             TagTable => 'Image::ExifTool::FlashPix::DocumentInfo',
182             },
183             },
184             "\x01CompObj" => {
185             Name => 'CompObj',
186             SubDirectory => {
187             TagTable => 'Image::ExifTool::FlashPix::CompObj',
188             DirStart => 0x1c, # skip stream header
189             },
190             },
191             "\x05Image Info" => {
192             Name => 'ImageInfo',
193             SubDirectory => {
194             TagTable => 'Image::ExifTool::FlashPix::ImageInfo',
195             },
196             },
197             "\x05Image Contents" => {
198             Name => 'Image',
199             SubDirectory => {
200             TagTable => 'Image::ExifTool::FlashPix::Image',
201             },
202             },
203             "Contents" => {
204             Name => 'Contents',
205             Notes => 'found in FLA files; may contain XMP',
206             SubDirectory => {
207             TagTable => 'Image::ExifTool::XMP::Main',
208             ProcessProc => \&ProcessContents,
209             },
210             },
211             "ICC Profile 0001" => {
212             Name => 'ICC_Profile',
213             SubDirectory => {
214             TagTable => 'Image::ExifTool::ICC_Profile::Main',
215             DirStart => 0x1c, # skip stream header
216             },
217             },
218             "\x05Extension List" => {
219             Name => 'Extensions',
220             SubDirectory => {
221             TagTable => 'Image::ExifTool::FlashPix::Extensions',
222             },
223             },
224             'Subimage 0000 Header' => {
225             Name => 'SubimageHdr',
226             SubDirectory => {
227             TagTable => 'Image::ExifTool::FlashPix::SubimageHdr',
228             DirStart => 0x1c, # skip stream header
229             },
230             },
231             # 'Subimage 0000 Data'
232             "\x05Data Object" => { # plus instance number (eg. " 000000")
233             Name => 'DataObject',
234             SubDirectory => {
235             TagTable => 'Image::ExifTool::FlashPix::DataObject',
236             },
237             },
238             # "\x05Data Object Store" => { # plus instance number (eg. " 000000")
239             "\x05Transform" => { # plus instance number (eg. " 000000")
240             Name => 'Transform',
241             SubDirectory => {
242             TagTable => 'Image::ExifTool::FlashPix::Transform',
243             },
244             },
245             "\x05Operation" => { # plus instance number (eg. " 000000")
246             Name => 'Operation',
247             SubDirectory => {
248             TagTable => 'Image::ExifTool::FlashPix::Operation',
249             },
250             },
251             "\x05Global Info" => {
252             Name => 'GlobalInfo',
253             SubDirectory => {
254             TagTable => 'Image::ExifTool::FlashPix::GlobalInfo',
255             },
256             },
257             "\x05Screen Nail" => { # plus class ID (eg. "_bd0100609719a180")
258             Name => 'ScreenNail',
259             Groups => { 2 => 'Other' },
260             # strip off stream header
261             ValueConv => 'length($val) > 0x1c and $val = substr($val, 0x1c); \$val',
262             },
263             "\x05Audio Info" => {
264             Name => 'AudioInfo',
265             SubDirectory => {
266             TagTable => 'Image::ExifTool::FlashPix::AudioInfo',
267             },
268             },
269             'Audio Stream' => { # plus instance number (eg. " 000000")
270             Name => 'AudioStream',
271             Groups => { 2 => 'Audio' },
272             # strip off stream header
273             ValueConv => 'length($val) > 0x1c and $val = substr($val, 0x1c); \$val',
274             },
275             'Current User' => { #PH
276             Name => 'CurrentUser',
277             # not sure what the rest of this data is, but extract ASCII name from it - PH
278             ValueConv => q{
279             return undef if length $val < 12;
280             my ($size,$pos) = unpack('x4VV', $val);
281             my $len = $size - $pos - 4;
282             return undef if $len < 0 or length $val < $size + 8;
283             return substr($val, 8 + $pos, $len);
284             },
285             },
286             'WordDocument' => {
287             Name => 'WordDocument',
288             SubDirectory => { TagTable => 'Image::ExifTool::FlashPix::WordDocument' },
289             },
290             # save these tables until after the WordDocument was processed
291             '0Table' => {
292             Name => 'Table0',
293             Hidden => 2, # (used only as temporary storage until table is processed)
294             Binary => 1,
295             },
296             '1Table' => {
297             Name => 'Table1',
298             Hidden => 2, # (used only as temporary storage until table is processed)
299             Binary => 1,
300             },
301             Preview => {
302             Name => 'PreviewImage',
303             Groups => { 2 => 'Preview' },
304             Binary => 1,
305             Notes => 'written by some FujiFilm models',
306             # skip 47-byte Fuji header
307             RawConv => q{
308             return undef unless length $val > 47;
309             $val = substr($val, 47);
310             return $val =~ /^\xff\xd8\xff/ ? $val : undef;
311             },
312             },
313             Property => {
314             Name => 'PreviewInfo',
315             SubDirectory => {
316             TagTable => 'Image::ExifTool::FlashPix::PreviewInfo',
317             ByteOrder => 'BigEndian',
318             },
319             },
320             # recognize Autodesk Revit files by looking at BasicFileInfo
321             # (but don't yet support reading their metatdata)
322             BasicFileInfo => {
323             Name => 'BasicFileInfo',
324             Binary => 1,
325             RawConv => q{
326             $val =~ tr/\0//d; # brute force conversion to ASCII
327             if ($val =~ /\.(rfa|rft|rte|rvt)/) {
328             $self->OverrideFileType(uc($1), "application/$1", $1);
329             }
330             return $val;
331             },
332             },
333             IeImg => {
334             Name => 'EmbeddedImage',
335             Notes => q{
336             embedded images in Scene7 vignette VNT files. The EmbeddedImage Class and
337             Rectangle are also extracted for applicable images, and may be associated
338             with the corresponding EmbeddedImage via the family 3 group name
339             },
340             Groups => { 2 => 'Preview' },
341             Binary => 1,
342             },
343             IeImg_class => {
344             Name => 'EmbeddedImageClass',
345             Notes => q{
346             not a real tag. This information is extracted if available for the
347             corresponding EmbeddedImage from the Contents of a VNT file
348             },
349             # eg. "Cache", "Mask"
350             },
351             IeImg_rect => { #
352             Name => 'EmbeddedImageRectangle',
353             Notes => q{
354             not a real tag. This information is extracted if available for the
355             corresponding EmbeddedImage from the Contents of a VNT file
356             },
357             },
358             _eeJPG => {
359             Name => 'EmbeddedImage',
360             Notes => q{
361             Not a real tag. Extracted from stream content when the ExtractEmbedded
362             option is used
363             },
364             Groups => { 2 => 'Preview' },
365             Binary => 1,
366             },
367             _eePNG => {
368             Name => 'EmbeddedPNG',
369             Notes => q{
370             Not a real tag. Extracted from stream content when the ExtractEmbedded
371             option is used
372             },
373             Groups => { 2 => 'Preview' },
374             Binary => 1,
375             },
376             _eeLink => {
377             Name => 'LinkedFileName',
378             Notes => q{
379             Not a real tag. Extracted from stream content when the ExtractEmbedded
380             option is used
381             },
382             },
383             );
384              
385             # Summary Information properties
386             %Image::ExifTool::FlashPix::SummaryInfo = (
387             PROCESS_PROC => \&ProcessProperties,
388             GROUPS => { 2 => 'Document' },
389             NOTES => q{
390             The Dictionary, CodePage and LocalIndicator tags are common to all FlashPix
391             property tables, even though they are only listed in the SummaryInfo table.
392             },
393             0x00 => { Name => 'Dictionary', Groups => { 2 => 'Other' }, Binary => 1 },
394             0x01 => {
395             Name => 'CodePage',
396             Groups => { 2 => 'Other' },
397             SeparateTable => 'Microsoft CodePage',
398             PrintConv => \%Image::ExifTool::Microsoft::codePage,
399             },
400             0x02 => 'Title',
401             0x03 => 'Subject',
402             0x04 => { Name => 'Author', Groups => { 2 => 'Author' } },
403             0x05 => 'Keywords',
404             0x06 => 'Comments',
405             0x07 => 'Template',
406             0x08 => { Name => 'LastModifiedBy', Groups => { 2 => 'Author' } },
407             0x09 => 'RevisionNumber',
408             0x0a => { Name => 'TotalEditTime', PrintConv => 'ConvertTimeSpan($val)' }, # (in sec)
409             0x0b => { Name => 'LastPrinted', Groups => { 2 => 'Time' } },
410             0x0c => {
411             Name => 'CreateDate',
412             Groups => { 2 => 'Time' },
413             PrintConv => '$self->ConvertDateTime($val)',
414             },
415             0x0d => {
416             Name => 'ModifyDate',
417             Groups => { 2 => 'Time' },
418             PrintConv => '$self->ConvertDateTime($val)',
419             },
420             0x0e => 'Pages',
421             0x0f => 'Words',
422             0x10 => 'Characters',
423             0x11 => {
424             Name => 'ThumbnailClip',
425             # (not a displayable format, so not in the "Preview" group)
426             Binary => 1,
427             },
428             0x12 => {
429             Name => 'Software',
430             RawConv => '$$self{Software} = $val', # (use to determine file type)
431             },
432             0x13 => {
433             Name => 'Security',
434             # see http://msdn.microsoft.com/en-us/library/aa379255(VS.85).aspx
435             PrintConv => {
436             0 => 'None',
437             BITMASK => {
438             0 => 'Password protected',
439             1 => 'Read-only recommended',
440             2 => 'Read-only enforced',
441             3 => 'Locked for annotations',
442             },
443             },
444             },
445             0x22 => { Name => 'CreatedBy', Groups => { 2 => 'Author' } }, #PH (guess) (MAX files)
446             0x23 => 'DocumentID', # PH (guess) (MAX files)
447             # 0x25 ? seen values 1.0-1.97 (MAX files)
448             0x80000000 => { Name => 'LocaleIndicator', Groups => { 2 => 'Other' } },
449             );
450              
451             # Document Summary Information properties (ref 4)
452             %Image::ExifTool::FlashPix::DocumentInfo = (
453             PROCESS_PROC => \&ProcessProperties,
454             GROUPS => { 2 => 'Document' },
455             NOTES => q{
456             The DocumentSummaryInformation property set includes a UserDefined property
457             set for which only the Hyperlinks and HyperlinkBase tags are pre-defined.
458             However, ExifTool will also extract any other information found in the
459             UserDefined properties.
460             },
461             # 0x01 => 'CodePage', #7
462             0x02 => 'Category',
463             0x03 => 'PresentationTarget',
464             0x04 => 'Bytes',
465             0x05 => 'Lines',
466             0x06 => 'Paragraphs',
467             0x07 => 'Slides',
468             0x08 => 'Notes',
469             0x09 => 'HiddenSlides',
470             0x0a => 'MMClips',
471             0x0b => {
472             Name => 'ScaleCrop',
473             PrintConv => { 0 => 'No', 1 => 'Yes' },
474             },
475             0x0c => 'HeadingPairs',
476             0x0d => {
477             Name => 'TitleOfParts',
478             # look for "3ds Max" software name at beginning of TitleOfParts
479             RawConv => q{
480             (ref $val eq 'ARRAY' ? $$val[0] : $val) =~ /^(3ds Max)/ and $$self{Software} = $1;
481             return $val;
482             }
483             },
484             0x0e => 'Manager',
485             0x0f => 'Company',
486             0x10 => {
487             Name => 'LinksUpToDate',
488             PrintConv => { 0 => 'No', 1 => 'Yes' },
489             },
490             0x11 => 'CharCountWithSpaces',
491             # 0x12 ? seen -32.1850395202637,-386.220672607422,-9.8100004196167,-9810,...
492             0x13 => { #PH (unconfirmed)
493             Name => 'SharedDoc',
494             PrintConv => { 0 => 'No', 1 => 'Yes' },
495             },
496             # 0x14 ? seen -1
497             # 0x15 ? seen 1
498             0x16 => {
499             Name => 'HyperlinksChanged',
500             PrintConv => { 0 => 'No', 1 => 'Yes' },
501             },
502             0x17 => { #PH (unconfirmed handling of lower 16 bits, not valid for MAX files)
503             Name => 'AppVersion',
504             ValueConv => 'sprintf("%d.%.4d",$val >> 16, $val & 0xffff)',
505             },
506             # 0x18 ? seen -1 (DigitalSignature, VtDigSig format, ref 7)
507             # 0x19 ? seen 0
508             # 0x1a ? seen 0
509             # 0x1b ? seen 0
510             # 0x1c ? seen 0,1
511             # 0x1d ? seen 1
512             0x1a => 'ContentType', #7, github#217
513             0x1b => 'ContentStatus', #7, github#217
514             0x1c => 'Language', #7, github#217
515             0x1d => 'DocVersion', #7, github#217
516             # 0x1e ? seen 1
517             # 0x1f ? seen 1,5
518             # 0x20 ? seen 0,5
519             # 0x21 ? seen -1
520             # 0x22 ? seen 0
521             '_PID_LINKBASE' => {
522             Name => 'HyperlinkBase',
523             ValueConv => '$self->Decode($val, "UCS2","II")',
524             },
525             '_PID_HLINKS' => {
526             Name => 'Hyperlinks',
527             RawConv => \&ProcessHyperlinks,
528             },
529             );
530              
531             # Image Information properties
532             %Image::ExifTool::FlashPix::ImageInfo = (
533             PROCESS_PROC => \&ProcessProperties,
534             GROUPS => { 2 => 'Image' },
535             0x21000000 => {
536             Name => 'FileSource',
537             PrintConv => {
538             1 => 'Film Scanner',
539             2 => 'Reflection Print Scanner',
540             3 => 'Digital Camera',
541             4 => 'Video Capture',
542             5 => 'Computer Graphics',
543             },
544             },
545             0x21000001 => {
546             Name => 'SceneType',
547             PrintConv => {
548             1 => 'Original Scene',
549             2 => 'Second Generation Scene',
550             3 => 'Digital Scene Generation',
551             },
552             },
553             0x21000002 => 'CreationPathVector',
554             0x21000003 => 'SoftwareRelease',
555             0x21000004 => 'UserDefinedID',
556             0x21000005 => 'SharpnessApproximation',
557             0x22000000 => { Name => 'Copyright', Groups => { 2 => 'Author' } },
558             0x22000001 => { Name => 'OriginalImageBroker', Groups => { 2 => 'Author' } },
559             0x22000002 => { Name => 'DigitalImageBroker', Groups => { 2 => 'Author' } },
560             0x22000003 => { Name => 'Authorship', Groups => { 2 => 'Author' } },
561             0x22000004 => { Name => 'IntellectualPropertyNotes', Groups => { 2 => 'Author' } },
562             0x23000000 => {
563             Name => 'TestTarget',
564             PrintConv => {
565             1 => 'Color Chart',
566             2 => 'Gray Card',
567             3 => 'Grayscale',
568             4 => 'Resolution Chart',
569             5 => 'Inch Scale',
570             6 => 'Centimeter Scale',
571             7 => 'Millimeter Scale',
572             8 => 'Micrometer Scale',
573             },
574             },
575             0x23000002 => 'GroupCaption',
576             0x23000003 => 'CaptionText',
577             0x23000004 => 'People',
578             0x23000007 => 'Things',
579             0x2300000A => {
580             Name => 'DateTimeOriginal',
581             Description => 'Date/Time Original',
582             Groups => { 2 => 'Time' },
583             PrintConv => '$self->ConvertDateTime($val)',
584             },
585             0x2300000B => 'Events',
586             0x2300000C => 'Places',
587             0x2300000F => 'ContentDescriptionNotes',
588             0x24000000 => { Name => 'Make', Groups => { 2 => 'Camera' } },
589             0x24000001 => {
590             Name => 'Model',
591             Description => 'Camera Model Name',
592             Groups => { 2 => 'Camera' },
593             },
594             0x24000002 => { Name => 'SerialNumber', Groups => { 2 => 'Camera' } },
595             0x25000000 => {
596             Name => 'CreateDate',
597             Groups => { 2 => 'Time' },
598             PrintConv => '$self->ConvertDateTime($val)',
599             },
600             0x25000001 => {
601             Name => 'ExposureTime',
602             PrintConv => 'Image::ExifTool::Exif::PrintExposureTime($val)',
603             },
604             0x25000002 => {
605             Name => 'FNumber',
606             PrintConv => 'sprintf("%.1f",$val)',
607             },
608             0x25000003 => {
609             Name => 'ExposureProgram',
610             Groups => { 2 => 'Camera' },
611             # use PrintConv of corresponding EXIF tag
612             PrintConv => $Image::ExifTool::Exif::Main{0x8822}->{PrintConv},
613             },
614             0x25000004 => 'BrightnessValue',
615             0x25000005 => 'ExposureCompensation',
616             0x25000006 => {
617             Name => 'SubjectDistance',
618             Groups => { 2 => 'Camera' },
619             PrintConv => 'sprintf("%.3f m", $val)',
620             },
621             0x25000007 => {
622             Name => 'MeteringMode',
623             Groups => { 2 => 'Camera' },
624             PrintConv => $Image::ExifTool::Exif::Main{0x9207}->{PrintConv},
625             },
626             0x25000008 => {
627             Name => 'LightSource',
628             Groups => { 2 => 'Camera' },
629             PrintConv => $Image::ExifTool::Exif::Main{0x9208}->{PrintConv},
630             },
631             0x25000009 => {
632             Name => 'FocalLength',
633             Groups => { 2 => 'Camera' },
634             PrintConv => 'sprintf("%.1f mm",$val)',
635             },
636             0x2500000A => {
637             Name => 'MaxApertureValue',
638             Groups => { 2 => 'Camera' },
639             ValueConv => '2 ** ($val / 2)',
640             PrintConv => 'sprintf("%.1f",$val)',
641             },
642             0x2500000B => {
643             Name => 'Flash',
644             Groups => { 2 => 'Camera' },
645             PrintConv => {
646             1 => 'No Flash',
647             2 => 'Flash Fired',
648             },
649             },
650             0x2500000C => {
651             Name => 'FlashEnergy',
652             Groups => { 2 => 'Camera' },
653             },
654             0x2500000D => {
655             Name => 'FlashReturn',
656             Groups => { 2 => 'Camera' },
657             PrintConv => {
658             1 => 'Subject Outside Flash Range',
659             2 => 'Subject Inside Flash Range',
660             },
661             },
662             0x2500000E => {
663             Name => 'BackLight',
664             PrintConv => {
665             1 => 'Front Lit',
666             2 => 'Back Lit 1',
667             3 => 'Back Lit 2',
668             },
669             },
670             0x2500000F => { Name => 'SubjectLocation', Groups => { 2 => 'Camera' } },
671             0x25000010 => 'ExposureIndex',
672             0x25000011 => {
673             Name => 'SpecialEffectsOpticalFilter',
674             PrintConv => {
675             1 => 'None',
676             2 => 'Colored',
677             3 => 'Diffusion',
678             4 => 'Multi-image',
679             5 => 'Polarizing',
680             6 => 'Split-field',
681             7 => 'Star',
682             },
683             },
684             0x25000012 => 'PerPictureNotes',
685             0x26000000 => {
686             Name => 'SensingMethod',
687             Groups => { 2 => 'Camera' },
688             PrintConv => $Image::ExifTool::Exif::Main{0x9217}->{PrintConv},
689             },
690             0x26000001 => { Name => 'FocalPlaneXResolution', Groups => { 2 => 'Camera' } },
691             0x26000002 => { Name => 'FocalPlaneYResolution', Groups => { 2 => 'Camera' } },
692             0x26000003 => {
693             Name => 'FocalPlaneResolutionUnit',
694             Groups => { 2 => 'Camera' },
695             PrintConv => $Image::ExifTool::Exif::Main{0xa210}->{PrintConv},
696             },
697             0x26000004 => 'SpatialFrequencyResponse',
698             0x26000005 => 'CFAPattern',
699             0x27000001 => {
700             Name => 'FilmCategory',
701             PrintConv => {
702             1 => 'Negative B&W',
703             2 => 'Negative Color',
704             3 => 'Reversal B&W',
705             4 => 'Reversal Color',
706             5 => 'Chromagenic',
707             6 => 'Internegative B&W',
708             7 => 'Internegative Color',
709             },
710             },
711             0x26000007 => 'ISO',
712             0x26000008 => 'Opto-ElectricConvFactor',
713             0x27000000 => 'FilmBrand',
714             0x27000001 => 'FilmCategory',
715             0x27000002 => 'FilmSize',
716             0x27000003 => 'FilmRollNumber',
717             0x27000004 => 'FilmFrameNumber',
718             0x29000000 => 'OriginalScannedImageSize',
719             0x29000001 => 'OriginalDocumentSize',
720             0x29000002 => {
721             Name => 'OriginalMedium',
722             PrintConv => {
723             1 => 'Continuous Tone Image',
724             2 => 'Halftone Image',
725             3 => 'Line Art',
726             },
727             },
728             0x29000003 => {
729             Name => 'TypeOfOriginal',
730             PrintConv => {
731             1 => 'B&W Print',
732             2 => 'Color Print',
733             3 => 'B&W Document',
734             4 => 'Color Document',
735             },
736             },
737             0x28000000 => 'ScannerMake',
738             0x28000001 => 'ScannerModel',
739             0x28000002 => 'ScannerSerialNumber',
740             0x28000003 => 'ScanSoftware',
741             0x28000004 => { Name => 'ScanSoftwareRevisionDate', Groups => { 2 => 'Time' } },
742             0x28000005 => 'ServiceOrganizationName',
743             0x28000006 => 'ScanOperatorID',
744             0x28000008 => {
745             Name => 'ScanDate',
746             Groups => { 2 => 'Time' },
747             PrintConv => '$self->ConvertDateTime($val)',
748             },
749             0x28000009 => {
750             Name => 'ModifyDate',
751             Groups => { 2 => 'Time' },
752             PrintConv => '$self->ConvertDateTime($val)',
753             },
754             0x2800000A => 'ScannerPixelSize',
755             );
756              
757             # Image Contents properties
758             %Image::ExifTool::FlashPix::Image = (
759             PROCESS_PROC => \&ProcessProperties,
760             GROUPS => { 2 => 'Image' },
761             # VARS storage is used as a hash lookup for tagID's which aren't constant.
762             # The key is a mask for significant bits of the tagID, and the value
763             # is a lookup for tagID's for which this mask is valid.
764             VARS => {
765             # ID's are different for each subimage
766             0xff00ffff => {
767             0x02000000=>1, 0x02000001=>1, 0x02000002=>1, 0x02000003=>1,
768             0x02000004=>1, 0x02000005=>1, 0x02000006=>1, 0x02000007=>1,
769             0x03000001=>1,
770             },
771             },
772             0x01000000 => 'NumberOfResolutions',
773             0x01000002 => 'ImageWidth', # width of highest resolution image
774             0x01000003 => 'ImageHeight',
775             0x01000004 => 'DefaultDisplayHeight',
776             0x01000005 => 'DefaultDisplayWidth',
777             0x01000006 => {
778             Name => 'DisplayUnits',
779             PrintConv => {
780             0 => 'inches',
781             1 => 'meters',
782             2 => 'cm',
783             3 => 'mm',
784             },
785             },
786             0x02000000 => 'SubimageWidth',
787             0x02000001 => 'SubimageHeight',
788             0x02000002 => {
789             Name => 'SubimageColor',
790             # decode only component count and color space of first component
791             ValueConv => 'sprintf("%.2x %.4x", unpack("x4vx4v",$val))',
792             PrintConv => {
793             '01 0000' => 'Opacity Only',
794             '01 8000' => 'Opacity Only (uncalibrated)',
795             '01 0001' => 'Monochrome',
796             '01 8001' => 'Monochrome (uncalibrated)',
797             '03 0002' => 'YCbCr',
798             '03 8002' => 'YCbCr (uncalibrated)',
799             '03 0003' => 'RGB',
800             '03 8003' => 'RGB (uncalibrated)',
801             '04 0002' => 'YCbCr with Opacity',
802             '04 8002' => 'YCbCr with Opacity (uncalibrated)',
803             '04 0003' => 'RGB with Opacity',
804             '04 8003' => 'RGB with Opacity (uncalibrated)',
805             },
806             },
807             0x02000003 => {
808             Name => 'SubimageNumericalFormat',
809             PrintConv => {
810             17 => '8-bit, Unsigned',
811             18 => '16-bit, Unsigned',
812             19 => '32-bit, Unsigned',
813             },
814             },
815             0x02000004 => {
816             Name => 'DecimationMethod',
817             PrintConv => {
818             0 => 'None (Full-sized Image)',
819             8 => '8-point Prefilter',
820             },
821             },
822             0x02000005 => 'DecimationPrefilterWidth',
823             0x02000007 => 'SubimageICC_Profile',
824             0x03000001 => { Name => 'JPEGTables', Binary => 1 },
825             0x03000002 => 'MaxJPEGTableIndex',
826             );
827              
828             # Extension List properties
829             %Image::ExifTool::FlashPix::Extensions = (
830             PROCESS_PROC => \&ProcessProperties,
831             GROUPS => { 2 => 'Other' },
832             VARS => {
833             # ID's are different for each extension type
834             0x0000ffff => {
835             0x0001=>1, 0x0002=>1, 0x0003=>1, 0x0004=>1,
836             0x0005=>1, 0x0006=>1, 0x0007=>1, 0x1000=>1,
837             0x2000=>1, 0x2001=>1, 0x3000=>1, 0x4000=>1,
838             },
839             0x0000f00f => { 0x3001=>1, 0x3002=>1 },
840             },
841             0x10000000 => 'UsedExtensionNumbers',
842             0x0001 => 'ExtensionName',
843             0x0002 => 'ExtensionClassID',
844             0x0003 => {
845             Name => 'ExtensionPersistence',
846             PrintConv => {
847             0 => 'Always Valid',
848             1 => 'Invalidated By Modification',
849             2 => 'Potentially Invalidated By Modification',
850             },
851             },
852             0x0004 => { Name => 'ExtensionCreateDate', Groups => { 2 => 'Time' } },
853             0x0005 => { Name => 'ExtensionModifyDate', Groups => { 2 => 'Time' } },
854             0x0006 => 'CreatingApplication',
855             0x0007 => 'ExtensionDescription',
856             0x1000 => 'Storage-StreamPathname',
857             0x2000 => 'FlashPixStreamPathname',
858             0x2001 => 'FlashPixStreamFieldOffset',
859             0x3000 => 'PropertySetPathname',
860             0x3001 => 'PropertySetIDCodes',
861             0x3002 => 'PropertyVectorElements',
862             0x4000 => 'SubimageResolutions',
863             );
864              
865             # Subimage Header tags
866             %Image::ExifTool::FlashPix::SubimageHdr = (
867             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
868             FORMAT => 'int32u',
869             # 0 => 'HeaderLength',
870             1 => 'SubimageWidth',
871             2 => 'SubimageHeight',
872             3 => 'SubimageTileCount',
873             4 => 'SubimageTileWidth',
874             5 => 'SubimageTileHeight',
875             6 => 'NumChannels',
876             # 7 => 'TileHeaderOffset',
877             # 8 => 'TileHeaderLength',
878             # ... followed by tile header table
879             );
880              
881             # Data Object properties
882             %Image::ExifTool::FlashPix::DataObject = (
883             PROCESS_PROC => \&ProcessProperties,
884             GROUPS => { 2 => 'Other' },
885             0x00010000 => 'DataObjectID',
886             0x00010002 => 'LockedPropertyList',
887             0x00010003 => 'DataObjectTitle',
888             0x00010004 => 'LastModifier',
889             0x00010005 => 'RevisionNumber',
890             0x00010006 => { Name => 'DataCreateDate', Groups => { 2 => 'Time' } },
891             0x00010007 => { Name => 'DataModifyDate', Groups => { 2 => 'Time' } },
892             0x00010008 => 'CreatingApplication',
893             0x00010100 => {
894             Name => 'DataObjectStatus',
895             PrintConv => q{
896             ($val & 0x0000ffff ? 'Exists' : 'Does Not Exist') .
897             ', ' . ($val & 0xffff0000 ? 'Not ' : '') . 'Purgeable'
898             },
899             },
900             0x00010101 => {
901             Name => 'CreatingTransform',
902             PrintConv => '$val ? $val : "Source Image"',
903             },
904             0x00010102 => 'UsingTransforms',
905             0x10000000 => 'CachedImageHeight',
906             0x10000001 => 'CachedImageWidth',
907             );
908              
909             # Transform properties
910             %Image::ExifTool::FlashPix::Transform = (
911             PROCESS_PROC => \&ProcessProperties,
912             GROUPS => { 2 => 'Other' },
913             0x00010000 => 'TransformNodeID',
914             0x00010001 => 'OperationClassID',
915             0x00010002 => 'LockedPropertyList',
916             0x00010003 => 'TransformTitle',
917             0x00010004 => 'LastModifier',
918             0x00010005 => 'RevisionNumber',
919             0x00010006 => { Name => 'TransformCreateDate', Groups => { 2 => 'Time' } },
920             0x00010007 => { Name => 'TransformModifyDate', Groups => { 2 => 'Time' } },
921             0x00010008 => 'CreatingApplication',
922             0x00010100 => 'InputDataObjectList',
923             0x00010101 => 'OutputDataObjectList',
924             0x00010102 => 'OperationNumber',
925             0x10000000 => 'ResultAspectRatio',
926             0x10000001 => 'RectangleOfInterest',
927             0x10000002 => 'Filtering',
928             0x10000003 => 'SpatialOrientation',
929             0x10000004 => 'ColorTwistMatrix',
930             0x10000005 => 'ContrastAdjustment',
931             );
932              
933             # Operation properties
934             %Image::ExifTool::FlashPix::Operation = (
935             PROCESS_PROC => \&ProcessProperties,
936             0x00010000 => 'OperationID',
937             );
938              
939             # Global Info properties
940             %Image::ExifTool::FlashPix::GlobalInfo = (
941             PROCESS_PROC => \&ProcessProperties,
942             0x00010002 => 'LockedPropertyList',
943             0x00010003 => 'TransformedImageTitle',
944             0x00010004 => 'LastModifier',
945             0x00010100 => 'VisibleOutputs',
946             0x00010101 => 'MaximumImageIndex',
947             0x00010102 => 'MaximumTransformIndex',
948             0x00010103 => 'MaximumOperationIndex',
949             );
950              
951             # Audio Info properties
952             %Image::ExifTool::FlashPix::AudioInfo = (
953             PROCESS_PROC => \&ProcessProperties,
954             GROUPS => { 2 => 'Audio' },
955             );
956              
957             # MacroMedia flash contents
958             %Image::ExifTool::FlashPix::Contents = (
959             PROCESS_PROC => \&ProcessProperties,
960             GROUPS => { 2 => 'Image' },
961             OriginalFileName => { Name => 'OriginalFileName', Hidden => 1 }, # (not a real tag -- extracted from Contents of VNT file)
962             );
963              
964             # CompObj tags
965             %Image::ExifTool::FlashPix::CompObj = (
966             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
967             GROUPS => { 2 => 'Other' },
968             FORMAT => 'int32u',
969             0 => { Name => 'CompObjUserTypeLen' },
970             1 => {
971             Name => 'CompObjUserType',
972             Format => 'string[$val{0}]',
973             RawConv => '$$self{CompObjUserType} = $val', # (use to determine file type)
974             },
975             );
976              
977             # decode Word document FIB header (ref [MS-DOC].pdf)
978             %Image::ExifTool::FlashPix::WordDocument = (
979             PROCESS_PROC => \&ProcessWordDocument,
980             GROUPS => { 2 => 'Other' },
981             FORMAT => 'int16u',
982             NOTES => 'Tags extracted from the Microsoft Word document stream.',
983             0 => {
984             Name => 'Identification',
985             PrintHex => 1,
986             PrintConv => {
987             0x6a62 => 'MS Word 97',
988             0x626a => 'Word 98 Mac',
989             0xa5dc => 'Word 6.0/7.0',
990             0xa5ec => 'Word 8.0',
991             },
992             },
993             3 => {
994             Name => 'LanguageCode',
995             PrintHex => 1,
996             PrintConv => {
997             0x0400 => 'None',
998             0x0401 => 'Arabic',
999             0x0402 => 'Bulgarian',
1000             0x0403 => 'Catalan',
1001             0x0404 => 'Traditional Chinese',
1002             0x0804 => 'Simplified Chinese',
1003             0x0405 => 'Czech',
1004             0x0406 => 'Danish',
1005             0x0407 => 'German',
1006             0x0807 => 'German (Swiss)',
1007             0x0408 => 'Greek',
1008             0x0409 => 'English (US)',
1009             0x0809 => 'English (British)',
1010             0x0c09 => 'English (Australian)',
1011             0x040a => 'Spanish (Castilian)',
1012             0x080a => 'Spanish (Mexican)',
1013             0x040b => 'Finnish',
1014             0x040c => 'French',
1015             0x080c => 'French (Belgian)',
1016             0x0c0c => 'French (Canadian)',
1017             0x100c => 'French (Swiss)',
1018             0x040d => 'Hebrew',
1019             0x040e => 'Hungarian',
1020             0x040f => 'Icelandic',
1021             0x0410 => 'Italian',
1022             0x0810 => 'Italian (Swiss)',
1023             0x0411 => 'Japanese',
1024             0x0412 => 'Korean',
1025             0x0413 => 'Dutch',
1026             0x0813 => 'Dutch (Belgian)',
1027             0x0414 => 'Norwegian (Bokmal)',
1028             0x0814 => 'Norwegian (Nynorsk)',
1029             0x0415 => 'Polish',
1030             0x0416 => 'Portuguese (Brazilian)',
1031             0x0816 => 'Portuguese',
1032             0x0417 => 'Rhaeto-Romanic',
1033             0x0418 => 'Romanian',
1034             0x0419 => 'Russian',
1035             0x041a => 'Croato-Serbian (Latin)',
1036             0x081a => 'Serbo-Croatian (Cyrillic)',
1037             0x041b => 'Slovak',
1038             0x041c => 'Albanian',
1039             0x041d => 'Swedish',
1040             0x041e => 'Thai',
1041             0x041f => 'Turkish',
1042             0x0420 => 'Urdu',
1043             0x0421 => 'Bahasa',
1044             0x0422 => 'Ukrainian',
1045             0x0423 => 'Byelorussian',
1046             0x0424 => 'Slovenian',
1047             0x0425 => 'Estonian',
1048             0x0426 => 'Latvian',
1049             0x0427 => 'Lithuanian',
1050             0x0429 => 'Farsi',
1051             0x042d => 'Basque',
1052             0x042f => 'Macedonian',
1053             0x0436 => 'Afrikaans',
1054             0x043e => 'Malaysian',
1055             },
1056             },
1057             5 => {
1058             Name => 'DocFlags',
1059             Mask => 0xff0f, # ignore save count
1060             RawConv => '$$self{DocFlags} = $val',
1061             PrintConv => { BITMASK => {
1062             0 => 'Template',
1063             1 => 'AutoText only',
1064             2 => 'Complex',
1065             3 => 'Has picture',
1066             # 4-7 = number of incremental saves
1067             8 => 'Encrypted',
1068             9 => '1Table',
1069             10 => 'Read only',
1070             11 => 'Passworded',
1071             12 => 'ExtChar',
1072             13 => 'Load override',
1073             14 => 'Far east',
1074             15 => 'Obfuscated',
1075             }},
1076             },
1077             9.1 => {
1078             Name => 'System',
1079             Mask => 0x0001,
1080             PrintConv => {
1081             0x0000 => 'Windows',
1082             0x0001 => 'Macintosh',
1083             },
1084             },
1085             9.2 => {
1086             Name => 'Word97',
1087             Mask => 0x0010,
1088             PrintConv => { 0 => 'No', 1 => 'Yes' },
1089             },
1090             );
1091              
1092             # tags decoded from Word document table
1093             %Image::ExifTool::FlashPix::DocTable = (
1094             GROUPS => { 1 => 'MS-DOC', 2 => 'Document' },
1095             NOTES => 'Tags extracted from the Microsoft Word document table.',
1096             VARS => { ID_FMT => 'none' },
1097             CommentBy => {
1098             Groups => { 2 => 'Author' },
1099             Notes => 'enable L option to extract all entries',
1100             },
1101             LastSavedBy => {
1102             Groups => { 2 => 'Author' },
1103             Notes => 'enable L option to extract history of up to 10 entries',
1104             },
1105             DOP => { SubDirectory => { TagTable => 'Image::ExifTool::FlashPix::DOP' } },
1106             ModifyDate => {
1107             Groups => { 2 => 'Time' },
1108             Format => 'int64u',
1109             Priority => 0,
1110             RawConv => q{
1111             $val = $val * 1e-7 - 11644473600; # convert to seconds since 1970
1112             return $val > 0 ? $val : undef;
1113             },
1114             ValueConv => 'ConvertUnixTime($val)',
1115             PrintConv => '$self->ConvertDateTime($val)',
1116             },
1117             #
1118             # tags below are used internally in intermediate steps to extract the tags above
1119             #
1120             TableOffsets => { Hidden => 2 }, # stores offsets to extract data from document table
1121             CommentByBlock => { # entire block of CommentBy entries
1122             SubDirectory => {
1123             TagTable => 'Image::ExifTool::FlashPix::DocTable',
1124             ProcessProc => \&ProcessCommentBy,
1125             },
1126             Hidden => 2,
1127             },
1128             LastSavedByBlock => { # entire block of LastSavedBy entries
1129             SubDirectory => {
1130             TagTable => 'Image::ExifTool::FlashPix::DocTable',
1131             ProcessProc => \&ProcessLastSavedBy,
1132             },
1133             Hidden => 2,
1134             },
1135             );
1136              
1137             # Microsoft Office Document Properties (ref [MS-DOC].pdf)
1138             %Image::ExifTool::FlashPix::DOP = (
1139             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
1140             GROUPS => { 1 => 'MS-DOC', 2 => 'Document' },
1141             NOTES => 'Microsoft office document properties.',
1142             20 => {
1143             Name => 'CreateDate',
1144             Format => 'int32u',
1145             Groups => { 2 => 'Time' },
1146             Priority => 0,
1147             RawConv => \&ConvertDTTM,
1148             PrintConv => '$self->ConvertDateTime($val)',
1149             },
1150             24 => {
1151             Name => 'ModifyDate',
1152             Format => 'int32u',
1153             Groups => { 2 => 'Time' },
1154             Priority => 0,
1155             RawConv => \&ConvertDTTM,
1156             PrintConv => '$self->ConvertDateTime($val)',
1157             },
1158             28 => {
1159             Name => 'LastPrinted',
1160             Format => 'int32u',
1161             Groups => { 2 => 'Time' },
1162             RawConv => \&ConvertDTTM,
1163             PrintConv => '$self->ConvertDateTime($val)',
1164             },
1165             32 => { Name => 'RevisionNumber', Format => 'int16u' },
1166             34 => {
1167             Name => 'TotalEditTime',
1168             Format => 'int32u',
1169             PrintConv => 'ConvertTimeSpan($val,60)',
1170             },
1171             # (according to the MS-DOC specification, the following are accurate only if
1172             # flag 'X' is set, and flag 'u' specifies whether the main or subdoc tags are
1173             # used, but in my tests it seems that both are filled in with reasonable values,
1174             # so just extract the main counts and ignore the subdoc counts for now - PH)
1175             38 => { Name => 'Words', Format => 'int32u' },
1176             42 => { Name => 'Characters', Format => 'int32u' },
1177             46 => { Name => 'Pages', Format => 'int16u' },
1178             48 => { Name => 'Paragraphs', Format => 'int32u' },
1179             56 => { Name => 'Lines', Format => 'int32u' },
1180             #60 => { Name => 'WordsWithSubdocs', Format => 'int32u' },
1181             #64 => { Name => 'CharactersWithSubdocs', Format => 'int32u' },
1182             #68 => { Name => 'PagesWithSubdocs', Format => 'int16u' },
1183             #70 => { Name => 'ParagraphsWithSubdocs', Format => 'int32u' },
1184             #74 => { Name => 'LinesWithSubdocs', Format => 'int32u' },
1185             );
1186              
1187             # FujiFilm "Property" information (ref PH)
1188             %Image::ExifTool::FlashPix::PreviewInfo = (
1189             PROCESS_PROC => \&Image::ExifTool::ProcessBinaryData,
1190             GROUPS => { 2 => 'Image' },
1191             NOTES => 'Preview information written by some FujiFilm models.',
1192             FIRST_ENTRY => 0,
1193             # values are all constant for for my samples except the two decoded tags
1194             # 0x0000: 01 01 00 00 02 01 00 00 00 00 00 00 00 xx xx 01
1195             # 0x0010: 01 00 00 00 00 00 00 xx xx 00 00 00 00 00 00 00
1196             # 0x0020: 00 00 00 00 00
1197             0x0d => {
1198             Name => 'PreviewImageWidth',
1199             Format => 'int16u',
1200             },
1201             0x17 => {
1202             Name => 'PreviewImageHeight',
1203             Format => 'int16u',
1204             },
1205             );
1206              
1207             # composite FlashPix tags
1208             %Image::ExifTool::FlashPix::Composite = (
1209             GROUPS => { 2 => 'Image' },
1210             PreviewImage => {
1211             Groups => { 2 => 'Preview' },
1212             # extract JPEG preview from ScreenNail if possible
1213             Require => {
1214             0 => 'ScreenNail',
1215             },
1216             Binary => 1,
1217             RawConv => q{
1218             return undef unless $val[0] =~ /\xff\xd8\xff/g;
1219             @grps = $self->GetGroup($$val{0}); # set groups from ScreenNail
1220             return substr($val[0], pos($val[0])-3);
1221             },
1222             },
1223             );
1224              
1225             # add our composite tags
1226             Image::ExifTool::AddCompositeTags('Image::ExifTool::FlashPix');
1227              
1228             #------------------------------------------------------------------------------
1229             # Convert Microsoft DTTM structure to date/time
1230             # Inputs: 0) DTTM value
1231             # Returns: EXIF-format date/time string ("0000:00:00 00:00:00" for zero date/time)
1232             sub ConvertDTTM($)
1233             {
1234 0     0 0 0 my $val = shift;
1235 0         0 my $yr = ($val >> 20) & 0x1ff;
1236 0         0 my $mon = ($val >> 16) & 0x0f;
1237 0         0 my $day = ($val >> 11) & 0x1f;
1238 0         0 my $hr = ($val >> 6) & 0x1f;
1239 0         0 my $min = ($val & 0x3f);
1240 0 0       0 $yr += 1900 if $val;
1241             # ExifTool 12.48 dropped the "Z" on the time here because a test .doc
1242             # file written by Word 2011 on Mac certainly used local time here
1243 0         0 return sprintf("%.4d:%.2d:%.2d %.2d:%.2d:00",$yr,$mon,$day,$hr,$min);
1244             }
1245              
1246             #------------------------------------------------------------------------------
1247             # Process hyperlinks from PID_HYPERLINKS array
1248             # (ref http://msdn.microsoft.com/archive/default.asp?url=/archive/en-us/dnaro97ta/html/msdn_hyper97.asp)
1249             # Inputs: 0) value, 1) ExifTool ref
1250             # Returns: list of hyperlinks
1251             sub ProcessHyperlinks($$)
1252             {
1253 1     1 0 5 my ($val, $et) = @_;
1254              
1255             # process as an array of VT_VARIANT's
1256 1         4 my $dirEnd = length $val;
1257 1 50       23 return undef if $dirEnd < 4;
1258 1         6 my $num = Get32u(\$val, 0);
1259 1         4 my $valPos = 4;
1260 1         3 my ($i, @vals);
1261 1         6 for ($i=0; $i<$num; ++$i) {
1262             # read VT_BLOB entries as an array of VT_VARIANT's
1263 18         155 my $value = ReadFPXValue($et, \$val, $valPos, VT_VARIANT, $dirEnd);
1264 18 50       49 last unless defined $value;
1265 18         62 push @vals, $value;
1266             }
1267             # filter values to extract only the links
1268 1         7 my @links;
1269 1         6 for ($i=0; $i<@vals; $i+=6) {
1270 3         7 push @links, $vals[$i+4]; # get address
1271 3 100       15 $links[-1] .= '#' . $vals[$i+5] if length $vals[$i+5]; # add subaddress
1272             }
1273 1         7 return \@links;
1274             }
1275              
1276             #------------------------------------------------------------------------------
1277             # Read FlashPix value
1278             # Inputs: 0) ExifTool ref, 1) data ref, 2) value offset, 3) FPX format number,
1279             # 4) end offset, 5) flag for no padding, 6) code page
1280             # Returns: converted value (or list of values in list context) and updates
1281             # value offset to end of value if successful, or returns undef on error
1282             sub ReadFPXValue($$$$$;$$)
1283             {
1284 310     310 0 799 my ($et, $dataPt, $valPos, $type, $dirEnd, $noPad, $codePage) = @_;
1285 310         510 my @vals;
1286              
1287 310         891 my $format = $oleFormat{$type & 0x0fff};
1288 310         700 while ($format) {
1289 310         618 my $count = 1;
1290             # handle VT_VECTOR types
1291 310         530 my $flags = $type & 0xf000;
1292 310 100       705 if ($flags) {
1293 46 50       125 if ($flags == VT_VECTOR) {
1294 46         96 $noPad = 1; # values sometimes aren't padded inside vectors!!
1295 46         154 my $size = $oleFormatSize{VT_VECTOR};
1296 46 50       140 if ($valPos + $size > $dirEnd) {
1297 0         0 $et->Warn('Incorrect FPX VT_VECTOR size');
1298 0         0 last;
1299             }
1300 46         147 $count = Get32u($dataPt, $valPos);
1301 46 50       129 push @vals, '' if $count == 0; # allow zero-element vector
1302 46         140 $valPos += 4;
1303             } else {
1304             # can't yet handle this property flag
1305 0         0 $et->Warn('Unknown FPX property');
1306 0         0 last;
1307             }
1308             }
1309 310 100       8053 unless ($format =~ /^VT_/) {
1310 101         341 my $size = Image::ExifTool::FormatSize($format) * $count;
1311 101 50       259 if ($valPos + $size > $dirEnd) {
1312 0         0 $et->Warn("Incorrect FPX $format size");
1313 0         0 last;
1314             }
1315 101         311 @vals = ReadValue($dataPt, $valPos, $format, $count, $size);
1316             # update position to end of value plus padding
1317 101         257 $valPos += ($count * $size + 3) & 0xfffffffc;
1318 101         200 last;
1319             }
1320 209         510 my $size = $oleFormatSize{$format};
1321 209         529 my ($item, $val, $len);
1322 209         618 for ($item=0; $item<$count; ++$item) {
1323 216 50       558 if ($valPos + $size > $dirEnd) {
1324 0         0 $et->Warn("Truncated FPX $format value");
1325 0         0 last;
1326             }
1327             # sometimes VT_VECTOR items are padded to even 4-byte boundaries, and sometimes they aren't
1328 216 100 100     712 if ($noPad and defined $len and $len & 0x03) {
      66        
1329 2         6 my $pad = 4 - ($len & 0x03);
1330 2 50       35 if ($valPos + $pad + $size <= $dirEnd) {
1331             # skip padding if all zeros
1332 2 50       13 $valPos += $pad if substr($$dataPt, $valPos, $pad) eq "\0" x $pad;
1333             }
1334             }
1335 216         350 undef $len;
1336 216 100 66     1184 if ($format eq 'VT_VARIANT') {
    100          
    50          
    100          
    100          
    50          
1337 24         73 my $subType = Get32u($dataPt, $valPos);
1338 24         50 $valPos += $size;
1339 24         86 $val = ReadFPXValue($et, $dataPt, $valPos, $subType, $dirEnd, $noPad, $codePage);
1340 24 50       61 last unless defined $val;
1341 24         53 push @vals, $val;
1342 24         84 next; # avoid adding $size to $valPos again
1343             } elsif ($format eq 'VT_FILETIME') {
1344             # convert from time in 100 ns increments to time in seconds
1345 50         286 $val = 1e-7 * Image::ExifTool::Get64u($dataPt, $valPos);
1346             # print as date/time if value is greater than one year (PH hack)
1347 50         127 my $secDay = 24 * 3600;
1348 50 100       383 if ($val > 365 * $secDay) {
1349             # shift from Jan 1, 1601 to Jan 1, 1970
1350 49         94 my $unixTimeZero = 134774 * $secDay;
1351 49         96 $val -= $unixTimeZero;
1352             # there are a lot of bad programmers out there...
1353 49         114 my $sec100yr = 100 * 365 * $secDay;
1354 49 50 33     302 if ($val < 0 || $val > $sec100yr) {
1355             # some software writes the wrong byte order (but the proper word order)
1356 0         0 my @w = unpack("x${valPos}NN", $$dataPt);
1357 0         0 my $v2 = ($w[0] + $w[1] * 4294967296) * 1e-7 - $unixTimeZero;
1358 0 0 0     0 if ($v2 > 0 && $v2 < $sec100yr) {
    0 0        
1359 0         0 $val = $v2;
1360             # also check for wrong time base
1361             } elsif ($val < 0 && $val + $unixTimeZero > 0) {
1362 0         0 $val += $unixTimeZero;
1363             }
1364             }
1365 49         197 $val = Image::ExifTool::ConvertUnixTime($val);
1366             }
1367             } elsif ($format eq 'VT_DATE') {
1368 0         0 $val = Image::ExifTool::GetDouble($dataPt, $valPos);
1369             # shift zero from Dec 30, 1899 to Jan 1, 1970 and convert to secs
1370 0 0       0 $val = ($val - 25569) * 24 * 3600 if $val != 0;
1371 0         0 $val = Image::ExifTool::ConvertUnixTime($val);
1372             } elsif ($format =~ /STR$/) {
1373 117         310 $len = Get32u($dataPt, $valPos);
1374 117 100       326 $len *= 2 if $format eq 'VT_LPWSTR'; # convert to byte count
1375 117 50       316 if ($valPos + $len + 4 > $dirEnd) {
1376 0         0 $et->Warn("Truncated $format value");
1377 0         0 last;
1378             }
1379 117         358 $val = substr($$dataPt, $valPos + 4, $len);
1380 117 100       301 if ($format eq 'VT_LPWSTR') {
    50          
1381             # convert wide string from Unicode
1382 98         378 $val = $et->Decode($val, 'UCS2');
1383             } elsif ($codePage) {
1384 19         60 my $charset = $Image::ExifTool::charsetName{"cp$codePage"};
1385 19 50       38 if ($charset) {
    0          
1386 19         74 $val = $et->Decode($val, $charset);
1387             } elsif ($codePage == 1200) { # UTF-16, little endian
1388 0         0 $val = $et->Decode($val, 'UCS2', 'II');
1389             }
1390             }
1391 117         362 $val =~ s/\0.*//s; # truncate at null terminator
1392             # update position for string length
1393             # (the spec states that strings should be padded to align
1394             # on even 32-bit boundaries, but this isn't always the case)
1395 117 100       368 $valPos += $noPad ? $len : ($len + 3) & 0xfffffffc;
1396             } elsif ($format eq 'VT_BLOB' or $format eq 'VT_CF') {
1397 2         8 my $len = Get32u($dataPt, $valPos); # (use local $len because we always expect padding)
1398 2 50       10 if ($valPos + $len + 4 > $dirEnd) {
1399 0         0 $et->Warn("Truncated $format value");
1400 0         0 last;
1401             }
1402 2         7 $val = substr($$dataPt, $valPos + 4, $len);
1403             # update position for data length plus padding
1404             # (does this padding disappear in arrays too?)
1405 2         6 $valPos += ($len + 3) & 0xfffffffc;
1406             } elsif ($format eq 'VT_CLSID') {
1407 23         177 $val = Image::ExifTool::ASF::GetGUID(substr($$dataPt, $valPos, $size));
1408             }
1409 192         363 $valPos += $size; # update value pointer to end of value
1410 192         660 push @vals, $val;
1411             }
1412             # join VT_ values with commas unless we want an array
1413 209 50 66     850 @vals = ( join $et->Options('ListSep'), @vals ) if @vals > 1 and not wantarray;
1414 209         430 last; # didn't really want to loop
1415             }
1416 310         879 $_[2] = $valPos; # return updated value position
1417              
1418 310 50       896 push @vals, '' if $type eq 0; # (VT_EMPTY)
1419 310 100       734 if (wantarray) {
    50          
1420 268         1016 return @vals;
1421             } elsif (@vals > 1) {
1422 0         0 return join(' ', @vals);
1423             } else {
1424 42         113 return $vals[0];
1425             }
1426             }
1427              
1428             #------------------------------------------------------------------------------
1429             # Scan for XMP in FLA Contents (ref PH)
1430             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
1431             # Returns: 1 on success
1432             # Notes: FLA format is proprietary and I couldn't find any documentation,
1433             # so this routine is entirely based on observations from sample files
1434             sub ProcessContents($$$)
1435             {
1436 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
1437 0         0 my $dataPt = $$dirInfo{DataPt};
1438 0         0 my $isFLA;
1439              
1440             # all of my FLA samples contain "Contents" data, and no other FPX-like samples have
1441             # this (except Scene7 VNT viles), but check the data for a familiar pattern to be
1442             # sure this is FLA: the Contents of all of my FLA samples start with two bytes
1443             # (0x29,0x38,0x3f,0x43 or 0x47, then 0x01) followed by a number of zero bytes
1444             # (from 0x18 to 0x26 of them, related somehow to the value of the first byte),
1445             # followed by the string "DocumentPage"
1446 0 0       0 if ($$dataPt =~ /^..\0+\xff\xff\x01\0\x0d\0CDocumentPage/s) {
    0          
1447 0         0 $isFLA = 1;
1448             } elsif ($$dataPt =~ /^\0{4}.(.{1,255})\x60\xa1\x3f\x22\0{5}(.{8})/sg) {
1449             # this looks like a VNT file
1450 0         0 $et->OverrideFileType('VNT', 'image/x-vignette');
1451             # hack to set proper file description (extension is the same for V-Note files)
1452 0         0 $Image::ExifTool::static_vars{OverrideFileDescription}{VNT} = 'Scene7 Vignette',
1453             my $name = $1;
1454 0         0 my ($w, $h) = unpack('V2',$2);
1455 0         0 $et->FoundTag(ImageWidth => $w);
1456 0         0 $et->FoundTag(ImageHeight => $h);
1457 0         0 $et->HandleTag($tagTablePtr, OriginalFileName => $name);
1458 0 0       0 if ($$dataPt =~ /\G\x01\0{4}(.{12})/sg) {
1459             # (first 4 bytes seem to be number of objects, next 4 bytes are zero, then ICC size)
1460 0         0 my $size = unpack('x8V', $1);
1461             # (not useful?) $et->FoundTag(NumObjects => $num);
1462 0 0 0     0 if ($size and pos($$dataPt) + $size < length($$dataPt)) {
1463 0         0 my $dat = substr($$dataPt, pos($$dataPt), $size);
1464 0         0 $et->FoundTag(ICC_Profile => $dat);
1465 0         0 pos($$dataPt) += $size;
1466             }
1467 0         0 $$et{IeImg_lkup} = { };
1468 0         0 $$et{IeImg_class} = { };
1469             # - the byte before \x80 is 0x0d, 0x11 or 0x1f for separate images in my samples,
1470             # and 0x1c or 0x23 for inline masks
1471             # - the byte after \xff\xff is 0x3b in my samples for $1 containing 'VnMask' or 'VnCache'
1472 0         0 while ($$dataPt =~ /\x0bTargetRole1(?:.\x80|\xff\xff.\0.\0Vn(\w+))\0\0\x01.{4}(.{24})/sg) {
1473 0         0 my ($index, @coords) = unpack('Vx4V4', $2);
1474 0 0       0 next if $index == 0xffffffff;
1475 0 0       0 $$et{IeImg_lkup}{$index} and $et->Warn('Duplicate image index');
1476 0         0 $$et{IeImg_lkup}{$index} = "@coords";
1477 0 0       0 $$et{IeImg_class}{$index} = $1 if $1;
1478             }
1479             }
1480             }
1481              
1482             # do a brute-force scan of the "Contents" for UTF-16 XMP
1483             # (this may always be little-endian, but allow for either endianness)
1484 0 0       0 if ($$dataPt =~ /<\0\?\0x\0p\0a\0c\0k\0e\0t\0 \0b\0e\0g\0i\0n\0=\0['"](\0\xff\xfe|\xfe\xff)/g) {
1485 0         0 $$dirInfo{DirStart} = pos($$dataPt) - 36;
1486 0 0       0 if ($$dataPt =~ /<\0\?\0x\0p\0a\0c\0k\0e\0t\0 \0e\0n\0d\0=\0['"]\0[wr]\0['"]\0\?\0>\0?/g) {
1487 0         0 $$dirInfo{DirLen} = pos($$dataPt) - $$dirInfo{DirStart};
1488 0         0 Image::ExifTool::XMP::ProcessXMP($et, $dirInfo, $tagTablePtr);
1489             # override format if not already FLA but XMP-dc:Format indicates it is
1490             $isFLA = 1 if $$et{FILE_TYPE} ne 'FLA' and $$et{VALUE}{Format} and
1491 0 0 0     0 $$et{VALUE}{Format} eq 'application/vnd.adobe.fla';
      0        
1492             }
1493             }
1494 0 0       0 $et->OverrideFileType('FLA') if $isFLA;
1495 0         0 return 1;
1496             }
1497              
1498             #------------------------------------------------------------------------------
1499             # Process WordDocument stream of MSWord doc file (ref 6)
1500             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
1501             # Returns: 1 on success
1502             sub ProcessWordDocument($$$)
1503             {
1504 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
1505 0 0       0 my $dataPt = $$dirInfo{DataPt} or return 0;
1506 0         0 my $dirLen = length $$dataPt;
1507             # validate the FIB signature
1508 0 0 0     0 unless ($dirLen > 2 and Get16u($dataPt,0) == 0xa5ec) {
1509 0         0 $et->Warn('Invalid FIB signature', 1);
1510 0         0 return 0;
1511             }
1512 0         0 $et->ProcessBinaryData($dirInfo, $tagTablePtr); # process FIB
1513             # continue parsing the WordDocument stream until we find the FibRgFcLcb
1514 0         0 my $pos = 32;
1515 0 0       0 return 0 if $pos + 2 > $dirLen;
1516 0         0 my $n = Get16u($dataPt, $pos); # read csw
1517 0         0 $pos += 2 + $n * 2; # skip fibRgW
1518 0 0       0 return 0 if $pos + 2 > $dirLen;
1519 0         0 $n = Get16u($dataPt, $pos); # read cslw
1520 0         0 $pos += 2 + $n * 4; # skip fibRgLw
1521 0 0       0 return 0 if $pos + 2 > $dirLen;
1522 0         0 $n = Get16u($dataPt, $pos); # read cbRgFcLcb
1523 0         0 $pos += 2; # point to start of fibRgFcLcbBlob
1524 0 0       0 return 0 if $pos + $n * 8 > $dirLen;
1525 0         0 my ($off, @tableOffsets);
1526             # save necessary entries for later processing of document table
1527             # (DOP, CommentBy, LastSavedBy)
1528 0         0 foreach $off (0xf8, 0x120, 0x238) {
1529 0 0       0 last if $off + 8 > $n * 8;
1530 0         0 push @tableOffsets, Get32u($dataPt, $pos + $off);
1531 0         0 push @tableOffsets, Get32u($dataPt, $pos + $off + 4);
1532             }
1533 0         0 my $tbl = GetTagTable('Image::ExifTool::FlashPix::DocTable');
1534             # extract ModifyDate if it exists
1535 0         0 $et->HandleTag($tbl, 'ModifyDate', undef,
1536             DataPt => $dataPt,
1537             Start => $pos + 0x2b8,
1538             Size => 8,
1539             );
1540 0         0 $et->HandleTag($tbl, TableOffsets => \@tableOffsets); # save for later
1541             # $pos += $n * 8; # skip fibRgFcLcbBlob
1542             # return 0 if $pos + 2 > $dirLen;
1543             # $n = Get16u($dataPt, $pos); # read cswNew
1544             # return 0 if $pos + 2 + $n * 2 > $dirLen;
1545             # my $nFib = Get16u($dataPt, 2 + ($n ? $pos : 0));
1546             # $pos += 2 + $n * 2; # skip fibRgCswNew
1547 0         0 return 1;
1548             }
1549              
1550             #------------------------------------------------------------------------------
1551             # Process Microsoft Word Document Table
1552             # Inputs: 0) ExifTool object ref
1553             sub ProcessDocumentTable($)
1554             {
1555 1     1 0 3 my $et = shift;
1556 1         4 my $value = $$et{VALUE};
1557 1         4 my $extra = $$et{TAG_EXTRA};
1558 1         3 my ($i, $j, $tag);
1559             # loop through TableOffsets for each sub-document
1560 1         3 for ($i=0; ; ++$i) {
1561 1 50       6 my $key = 'TableOffsets' . ($i ? " ($i)" : '');
1562 1         4 my $offsets = $$value{$key};
1563 1 50       5 last unless defined $offsets;
1564 0         0 my $doc;
1565 0   0     0 $doc = $$extra{$key}{G3} || '';
1566             # get DocFlags for this sub-document
1567 0         0 my ($docFlags, $docTable);
1568 0         0 for ($j=0; ; ++$j) {
1569 0 0       0 my $key = 'DocFlags' . ($j ? " ($j)" : '');
1570 0 0       0 last unless defined $$value{$key};
1571 0         0 my $tmp;
1572 0   0     0 $tmp = $$extra{$key}{G3} || '';
1573 0 0       0 if ($tmp eq $doc) {
1574 0         0 $docFlags = $$value{$key};
1575 0         0 last;
1576             }
1577             }
1578 0 0       0 next unless defined $docFlags;
1579 0 0       0 $tag = $docFlags & 0x200 ? 'Table1' : 'Table0';
1580             # get table for this sub-document
1581 0         0 for ($j=0; ; ++$j) {
1582 0 0       0 my $key = $tag . ($j ? " ($j)" : '');
1583 0 0       0 last unless defined $$value{$key};
1584 0         0 my $tmp;
1585 0   0     0 $tmp = $$extra{$key}{G3} || '';
1586 0 0       0 if ($tmp eq $doc) {
1587 0         0 $docTable = \$$value{$key};
1588 0         0 last;
1589             }
1590             }
1591 0 0       0 next unless defined $docTable;
1592             # extract DOP and LastSavedBy information from document table
1593 0         0 $$et{DOC_NUM} = $doc; # use same document number
1594 0         0 my $tagTablePtr = GetTagTable('Image::ExifTool::FlashPix::DocTable');
1595 0         0 foreach $tag (qw(DOP CommentByBlock LastSavedByBlock)) {
1596 0 0       0 last unless @$offsets;
1597 0         0 my $off = shift @$offsets;
1598 0         0 my $len = shift @$offsets;
1599 0 0 0     0 next unless $len and $off + $len <= length $$docTable;
1600 0         0 $et->HandleTag($tagTablePtr, $tag, undef,
1601             DataPt => $docTable,
1602             Start => $off,
1603             Size => $len,
1604             );
1605             }
1606 0         0 delete $$et{DOC_NUM};
1607             }
1608             # delete intermediate tags
1609 1         4 foreach $tag (qw(TableOffsets Table0 Table1)) {
1610 3         6 for ($i=0; ; ++$i) {
1611 3 50       9 my $key = $tag . ($i ? " ($i)" : '');
1612 3 50       15 last unless defined $$value{$key};
1613 0         0 $et->DeleteTag($key);
1614             }
1615             }
1616             }
1617              
1618             #------------------------------------------------------------------------------
1619             # Extract names of comment authors (ref 6)
1620             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
1621             # Returns: 1 on success
1622             sub ProcessCommentBy($$$)
1623             {
1624 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
1625 0         0 my $dataPt = $$dirInfo{DataPt};
1626 0         0 my $pos = $$dirInfo{DirStart};
1627 0         0 my $end = $$dirInfo{DirLen} + $pos;
1628 0         0 $et->VerboseDir($$dirInfo{DirName});
1629 0         0 while ($pos + 2 < $end) {
1630 0         0 my $len = Get16u($dataPt, $pos);
1631 0         0 $pos += 2;
1632 0 0       0 last if $pos + $len * 2 > $end;
1633 0         0 my $author = $et->Decode(substr($$dataPt, $pos, $len*2), 'UCS2');
1634 0         0 $pos += $len * 2;
1635 0         0 $et->HandleTag($tagTablePtr, CommentBy => $author);
1636             }
1637 0         0 return 1;
1638             }
1639              
1640             #------------------------------------------------------------------------------
1641             # Extract last-saved-by names (ref 5)
1642             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
1643             # Returns: 1 on success
1644             sub ProcessLastSavedBy($$$)
1645             {
1646 0     0 0 0 my ($et, $dirInfo, $tagTablePtr) = @_;
1647 0         0 my $dataPt = $$dirInfo{DataPt};
1648 0         0 my $pos = $$dirInfo{DirStart};
1649 0         0 my $end = $$dirInfo{DirLen} + $pos;
1650 0 0       0 return 0 if $pos + 6 > $end;
1651 0         0 $et->VerboseDir($$dirInfo{DirName});
1652 0         0 my $num = Get16u($dataPt, $pos+2);
1653 0         0 $pos += 6;
1654 0         0 while ($num >= 2) {
1655 0 0       0 last if $pos + 2 > $end;
1656 0         0 my $len = Get16u($dataPt, $pos);
1657 0         0 $pos += 2;
1658 0 0       0 last if $pos + $len * 2 > $end;
1659 0         0 my $author = $et->Decode(substr($$dataPt, $pos, $len*2), 'UCS2');
1660 0         0 $pos += $len * 2;
1661 0 0       0 last if $pos + 2 > $end;
1662 0         0 $len = Get16u($dataPt, $pos);
1663 0         0 $pos += 2;
1664 0 0       0 last if $pos + $len * 2 > $end;
1665 0         0 my $path = $et->Decode(substr($$dataPt, $pos, $len*2), 'UCS2');
1666 0         0 $pos += $len * 2;
1667 0         0 $et->HandleTag($tagTablePtr, LastSavedBy => "$author ($path)");
1668 0         0 $num -= 2;
1669             }
1670 0         0 return 1;
1671             }
1672              
1673             #------------------------------------------------------------------------------
1674             # Check FPX byte order mark (BOM) and set byte order appropriately
1675             # Inputs: 0) data ref, 1) offset to BOM
1676             # Returns: true on success
1677             sub CheckBOM($$)
1678             {
1679 25     25 0 90 my ($dataPt, $pos) = @_;
1680 25         111 my $bom = Get16u($dataPt, $pos);
1681 25 100       91 return 1 if $bom == 0xfffe;
1682 23 50       68 return 0 unless $bom == 0xfeff;
1683 23         99 ToggleByteOrder();
1684 23         132 return 1;
1685             }
1686              
1687             #------------------------------------------------------------------------------
1688             # Process FlashPix properties
1689             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
1690             # Returns: 1 on success
1691             sub ProcessProperties($$$)
1692             {
1693 25     25 0 75 my ($et, $dirInfo, $tagTablePtr) = @_;
1694 25         75 my $dataPt = $$dirInfo{DataPt};
1695 25   50     173 my $pos = $$dirInfo{DirStart} || 0;
1696 25   66     137 my $dirLen = $$dirInfo{DirLen} || length($$dataPt) - $pos;
1697 25         68 my $dirEnd = $pos + $dirLen;
1698 25         104 my $verbose = $et->Options('Verbose');
1699 25         54 my $n;
1700              
1701 25 50       78 if ($dirLen < 48) {
1702 0         0 $et->Warn('Truncated FPX properties');
1703 0         0 return 0;
1704             }
1705             # check and set our byte order if necessary
1706 25 50       108 unless (CheckBOM($dataPt, $pos)) {
1707 0         0 $et->Warn('Bad FPX property byte order mark');
1708 0         0 return 0;
1709             }
1710             # get position of start of section
1711 25         126 $pos = Get32u($dataPt, $pos + 44);
1712 25 50       117 if ($pos < 48) {
1713 0         0 $et->Warn('Bad FPX property section offset');
1714 0         0 return 0;
1715             }
1716 25         106 for ($n=0; $n<2; ++$n) {
1717 26         73 my %dictionary; # dictionary to translate user-defined properties
1718             my $codePage;
1719 26 50       82 last if $pos + 8 > $dirEnd;
1720             # read property section header
1721 26         77 my $size = Get32u($dataPt, $pos);
1722 26 50       80 last unless $size;
1723 26         72 my $numEntries = Get32u($dataPt, $pos + 4);
1724 26 50       87 $verbose and $et->VerboseDir('Property Info', $numEntries, $size);
1725 26 50       113 if ($pos + 8 + 8 * $numEntries > $dirEnd) {
1726 0         0 $et->Warn('Truncated property list');
1727 0         0 last;
1728             }
1729 26         54 my $index;
1730 26         108 for ($index=0; $index<$numEntries; ++$index) {
1731 269         604 my $entry = $pos + 8 + 8 * $index;
1732 269         804 my $tag = Get32u($dataPt, $entry);
1733 269         652 my $offset = Get32u($dataPt, $entry + 4);
1734 269         542 my $valStart = $pos + 4 + $offset;
1735 269 50       610 last if $valStart >= $dirEnd;
1736 269         406 my $valPos = $valStart;
1737 269         685 my $type = Get32u($dataPt, $pos + $offset);
1738 269 100       669 if ($tag == 0) {
1739             # read dictionary to get tag name lookup for this property set
1740 1         3 my $i;
1741 1         5 for ($i=0; $i<$type; ++$i) {
1742 6 50       16 last if $valPos + 8 > $dirEnd;
1743 6         17 $tag = Get32u($dataPt, $valPos);
1744 6         16 my $len = Get32u($dataPt, $valPos + 4);
1745 6         14 $valPos += 8 + $len;
1746 6 50       15 last if $valPos > $dirEnd;
1747 6         17 my $name = substr($$dataPt, $valPos - $len, $len);
1748 6         31 $name =~ s/\0.*//s;
1749 6 50       17 next unless length $name;
1750 6         20 $dictionary{$tag} = $name;
1751 6 100       22 next if $$tagTablePtr{$name};
1752 4         7 $tag = $name;
1753 4         25 $name =~ s/(^| )([a-z])/\U$2/g; # start with uppercase
1754 4         12 $name =~ tr/-_a-zA-Z0-9//dc; # remove illegal characters
1755 4 50       11 next unless length $name;
1756 4 50       13 $et->VPrint(0, "$$et{INDENT}\[adding $name]\n") if $verbose;
1757 4         20 AddTagToTable($tagTablePtr, $tag, { Name => $name });
1758             }
1759 1         5 next;
1760             }
1761             # use tag name from dictionary if available
1762 268         502 my ($custom, $val);
1763 268 100       786 if (defined $dictionary{$tag}) {
1764 6         15 $tag = $dictionary{$tag};
1765 6         13 $custom = 1;
1766             }
1767 268         853 my @vals = ReadFPXValue($et, $dataPt, $valPos, $type, $dirEnd, undef, $codePage);
1768 268 50       883 @vals or $et->Warn('Error reading property value');
1769 268 100       622 $val = @vals > 1 ? \@vals : $vals[0];
1770 268         493 my $format = $type & 0x0fff;
1771 268         446 my $flags = $type & 0xf000;
1772 268   33     815 my $formStr = $oleFormat{$format} || "Type $format";
1773 268 100 33     683 $formStr .= '|' . ($oleFlags{$flags} || sprintf("0x%x",$flags)) if $flags;
1774 268         5161 my $tagInfo;
1775             # check for common tag ID's: Dictionary, CodePage and LocaleIndicator
1776             # (must be done before masking because masked tags may overlap these ID's)
1777 268 100 66     2101 if (not $custom and ($tag == 1 or $tag == 0x80000000)) {
    100 100        
    100 66        
1778             # get tagInfo from SummaryInfo table
1779 26         134 my $summaryTable = GetTagTable('Image::ExifTool::FlashPix::SummaryInfo');
1780 26         119 $tagInfo = $et->GetTagInfo($summaryTable, $tag);
1781 26 50       83 if ($tag == 1) {
1782 26 50       92 $val += 0x10000 if $val < 0; # (may be incorrectly stored as int16s)
1783 26         69 $codePage = $val; # save code page for translating values
1784             }
1785             } elsif ($$tagTablePtr{$tag}) {
1786 56         234 $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
1787             } elsif ($$tagTablePtr{VARS} and not $custom) {
1788             # mask off insignificant bits of tag ID if necessary
1789 184         411 my $masked = $$tagTablePtr{VARS};
1790 184         268 my $mask;
1791 184         602 foreach $mask (keys %$masked) {
1792 200 100       776 if ($masked->{$mask}->{$tag & $mask}) {
1793 184         839 $tagInfo = $et->GetTagInfo($tagTablePtr, $tag & $mask);
1794 184         427 last;
1795             }
1796             }
1797             }
1798 268         1530 $et->HandleTag($tagTablePtr, $tag, $val,
1799             DataPt => $dataPt,
1800             Start => $valStart,
1801             Size => $valPos - $valStart,
1802             Format => $formStr,
1803             Index => $index,
1804             TagInfo => $tagInfo,
1805             Extra => ", type=$type",
1806             );
1807             }
1808             # issue warning if we hit end of property section prematurely
1809 26 50       102 $et->Warn('Truncated property data') if $index < $numEntries;
1810 26 100       137 last unless $$dirInfo{Multi};
1811 2         17 $pos += $size;
1812             }
1813              
1814 25         92 return 1;
1815             }
1816              
1817             #------------------------------------------------------------------------------
1818             # Load chain of sectors from file
1819             # Inputs: 0) RAF ref, 1) first sector number, 2) FAT ref, 3) sector size, 4) header size
1820             sub LoadChain($$$$$)
1821             {
1822 6     6 0 19 my ($raf, $sect, $fatPt, $sectSize, $hdrSize) = @_;
1823 6 50       19 return undef unless $raf;
1824 6         13 my $chain = '';
1825 6         15 my ($buff, %loadedSect);
1826 6         27 for (;;) {
1827 39 100       112 last if $sect >= END_OF_CHAIN;
1828 33 50       91 return undef if $loadedSect{$sect}; # avoid infinite loop
1829 33         99 $loadedSect{$sect} = 1;
1830 33         61 my $offset = $sect * $sectSize + $hdrSize;
1831 33 50 33     159 return undef unless ($offset <= 0x7fffffff or $$raf{LargeFileSupport}) and
      33        
      33        
1832             $raf->Seek($offset, 0) and
1833             $raf->Read($buff, $sectSize) == $sectSize;
1834 33         106 $chain .= $buff;
1835             # step to next sector in chain
1836 33 50       86 return undef if $sect * 4 > length($$fatPt) - 4;
1837 33         125 $sect = Get32u($fatPt, $sect * 4);
1838             }
1839 6         30 return $chain;
1840             }
1841              
1842             #------------------------------------------------------------------------------
1843             # Extract information from a JPEG APP2 FPXR segment
1844             # Inputs: 0) ExifTool object ref, 1) dirInfo ref, 2) tag table ref
1845             # Returns: 1 on success
1846             sub ProcessFPXR($$$)
1847             {
1848 67     67 0 182 my ($et, $dirInfo, $tagTablePtr) = @_;
1849 67         157 my $dataPt = $$dirInfo{DataPt};
1850 67         145 my $dirStart = $$dirInfo{DirStart};
1851 67         143 my $dirLen = $$dirInfo{DirLen};
1852 67         299 my $verbose = $et->Options('Verbose');
1853              
1854 67 50       211 if ($dirLen < 13) {
1855 0         0 $et->Warn('FPXR segment too small');
1856 0         0 return 0;
1857             }
1858              
1859             # get version and segment type (version is 0 in all my samples)
1860 67         305 my ($vers, $type) = unpack('x5C2', $$dataPt);
1861              
1862 67 100       253 if ($type == 1) { # a "Contents List" segment
    50          
    0          
1863              
1864 21 50       81 $vers != 0 and $et->Warn("Untested FPXR version $vers");
1865 21 50       137 if ($$et{FPXR}) {
1866 0         0 $et->Warn('Multiple FPXR contents lists');
1867 0         0 delete $$et{FPXR};
1868             }
1869 21         73 my $numEntries = unpack('x7n', $$dataPt);
1870 21         46 my @contents;
1871 21 50       78 $verbose and $et->VerboseDir('Contents List', $numEntries);
1872 21         51 my $pos = 9;
1873 21         46 my $entry;
1874 21         82 for ($entry = 0; $entry < $numEntries; ++$entry) {
1875 48 50       166 if ($pos + 4 > $dirLen) {
1876 0         0 $et->Warn('Truncated FPXR contents');
1877 0         0 return 0;
1878             }
1879 48         231 my ($size, $default) = unpack("x${pos}Na", $$dataPt);
1880 48         210 pos($$dataPt) = $pos + 5;
1881             # according to the spec, this string is little-endian
1882             # (very odd, since the size word is big-endian),
1883             # and the first char must be '/'
1884 48 50       501 unless ($$dataPt =~ m{\G(/\0(..)*?)\0\0}sg) {
1885 0         0 $et->Warn('Invalid FPXR stream name');
1886 0         0 return 0;
1887             }
1888             # convert stream pathname to ascii
1889 48         203 my $name = Image::ExifTool::Decode(undef, $1, 'UCS2', 'II', 'Latin');
1890 48 50       142 if ($verbose) {
1891 0 0       0 my $psize = ($size == 0xffffffff) ? 'storage' : "$size bytes";
1892 0         0 $et->VPrint(0," | $entry) Name: '${name}' [$psize]\n");
1893             }
1894             # remove directory specification
1895 48         304 $name =~ s{.*/}{}s;
1896             # read storage class ID if necessary
1897 48         98 my $classID;
1898 48 100       150 if ($size == 0xffffffff) {
1899 2 50       40 unless ($$dataPt =~ m{(.{16})}sg) {
1900 0         0 $et->Warn('Truncated FPXR storage class ID');
1901 0         0 return 0;
1902             }
1903             # unpack class ID in case we want to use it sometime
1904 2         14 $classID = Image::ExifTool::ASF::GetGUID($1);
1905             }
1906             # find the tagInfo if available
1907 48         77 my $tagInfo;
1908 48 100       165 unless ($$tagTablePtr{$name}) {
1909             # remove instance number or class ID from tag if necessary
1910             $tagInfo = $et->GetTagInfo($tagTablePtr, $1) if
1911             ($name =~ /(.*) \d{6}$/s and $$tagTablePtr{$1}) or
1912 25 100 66     469 ($name =~ /(.*)_[0-9a-f]{16}$/s and $$tagTablePtr{$1});
      66        
      100        
1913             }
1914             # update position in list
1915 48         108 $pos = pos($$dataPt);
1916             # add to our contents list
1917 48         478 push @contents, {
1918             Name => $name,
1919             Size => $size,
1920             Default => $default,
1921             ClassID => $classID,
1922             TagInfo => $tagInfo,
1923             };
1924             }
1925             # save contents list as $et member variable
1926             # (must do this last so we don't save list on error)
1927 21         113 $$et{FPXR} = \@contents;
1928              
1929             } elsif ($type == 2) { # a "Stream Data" segment
1930              
1931             # get the contents list index and stream data offset
1932 46         139 my ($index, $offset) = unpack('x7nN', $$dataPt);
1933 46         113 my $fpxr = $$et{FPXR};
1934 46 50 33     260 if ($fpxr and $$fpxr[$index]) {
    0 0        
      0        
      0        
1935 46         100 my $obj = $$fpxr[$index];
1936             # extract stream data (after 13-byte header)
1937 46 50       153 if (not defined $$obj{Stream}) {
1938             # ignore offset for first segment of this type
1939             # (in my sample images, this isn't always zero as one would expect)
1940 46         360 $$obj{Stream} = substr($$dataPt, $dirStart+13);
1941             } else {
1942             # add data at the proper offset to the stream
1943 0         0 my $overlap = length($$obj{Stream}) - $offset;
1944 0         0 my $start = $dirStart + 13;
1945 0 0 0     0 if ($overlap < 0 or $dirLen - $overlap < 13) {
1946 0         0 $et->Warn("Bad FPXR stream $index offset",1);
1947             } else {
1948             # ignore any overlapping data in this segment
1949             # (this seems to be the convention)
1950 0         0 $start += $overlap;
1951             }
1952             # concatenate data with this stream
1953 0         0 $$obj{Stream} .= substr($$dataPt, $start);
1954             }
1955             # save value for this tag if stream is complete
1956 46         141 my $len = length $$obj{Stream};
1957 46 50       146 if ($len >= $$obj{Size}) {
1958 46 50       122 $et->VPrint(0, " + [FPXR stream $index, $len bytes]\n") if $verbose;
1959 46 50       165 if ($len > $$obj{Size}) {
1960 0         0 $et->Warn('Extra data in FPXR segment (truncated)');
1961 0         0 $$obj{Stream} = substr($$obj{Stream}, 0, $$obj{Size});
1962             }
1963             # handle this tag
1964             $et->HandleTag($tagTablePtr, $$obj{Name}, $$obj{Stream},
1965             DataPt => \$$obj{Stream},
1966             TagInfo => $$obj{TagInfo},
1967 46         307 );
1968 46         187 delete $$obj{Stream}; # done with this stream
1969             }
1970             # hack for improperly stored FujiFilm PreviewImage (stored with no contents list)
1971             } elsif ($index == 512 and $dirLen > 60 and ($$et{FujiPreview} or
1972             ($dirLen > 64 and substr($$dataPt, $dirStart+60, 4) eq "\xff\xd8\xff\xdb")))
1973             {
1974 0 0       0 $$et{FujiPreview} = '' unless defined $$et{FujiPreview};
1975             # recombine PreviewImage, skipping 13-byte FPXR header + 47-byte Fuji header
1976 0         0 $$et{FujiPreview} .= substr($$dataPt, $dirStart+60);
1977             } else {
1978             # (Kodak uses index 255 for a free segment in images from some cameras)
1979 0 0       0 $et->Warn("Unlisted FPXR segment (index $index)") if $index != 255;
1980             }
1981              
1982             } elsif ($type != 3) { # not a "Reserved" segment
1983              
1984 0         0 $et->Warn("Unknown FPXR segment (type $type)");
1985              
1986             }
1987              
1988             # clean up if this was the last FPXR segment
1989 67 100       245 if ($$dirInfo{LastFPXR}) {
1990 21 50       99 if ($$et{FPXR}) {
1991 21         50 my $obj;
1992 21         44 foreach $obj (@{$$et{FPXR}}) {
  21         73  
1993 48 50 33     178 next unless defined $$obj{Stream} and length $$obj{Stream};
1994             # parse it even though it isn't the proper length
1995             $et->HandleTag($tagTablePtr, $$obj{Name}, $$obj{Stream},
1996             DataPt => \$$obj{Stream},
1997             TagInfo => $$obj{TagInfo},
1998 0         0 );
1999             }
2000 21         161 delete $$et{FPXR}; # delete our temporary variables
2001             }
2002 21 50       85 if ($$et{FujiPreview}) {
2003 0         0 $et->FoundTag('PreviewImage', $$et{FujiPreview});
2004 0         0 delete $$et{FujiPreview};
2005             }
2006             }
2007 67         210 return 1;
2008             }
2009              
2010             #------------------------------------------------------------------------------
2011             # Set document number for objects
2012             # Inputs: 0) object hierarchy hash ref, 1) object index, 2) doc number list ref,
2013             # 3) doc numbers used at each level, 4) flag set for metadata levels
2014             sub SetDocNum($$;$$$)
2015             {
2016 5     5 0 12 my ($hier, $index, $doc, $used, $meta) = @_;
2017 5 50       19 my $obj = $$hier{$index} or return;
2018 5 50       15 return if exists $$obj{DocNum};
2019 5         13 $$obj{DocNum} = $doc;
2020 5 100       29 SetDocNum($hier, $$obj{Left}, $doc, $used, $meta) if $$obj{Left};
2021 5 100       16 SetDocNum($hier, $$obj{Right}, $doc, $used, $meta) if $$obj{Right};
2022 5 100       18 if (defined $$obj{Child}) {
2023 1 50       6 $used or $used = [ ];
2024 1         2 my @subDoc;
2025 1 50       4 push @subDoc, @$doc if $doc;
2026             # we must dive down 2 levels for each sub-document, so use the
2027             # $meta flag to add a sub-document level only for every 2nd generation
2028 1 50       8 if ($meta) {
    50          
2029 0   0     0 my $subNum = ($$used[scalar @subDoc] || 0);
2030 0         0 $$used[scalar @subDoc] = $subNum;
2031 0         0 push @subDoc, $subNum;
2032             } elsif (@subDoc) {
2033 0         0 $subDoc[-1] = ++$$used[$#subDoc];
2034             }
2035 1         8 SetDocNum($hier, $$obj{Child}, \@subDoc, $used, not $meta);
2036             }
2037             }
2038              
2039             #------------------------------------------------------------------------------
2040             # Extract information from a FlashPix (FPX) file
2041             # Inputs: 0) ExifTool object ref, 1) dirInfo ref
2042             # Returns: 1 on success, 0 if this wasn't a valid FPX-format file
2043             sub ProcessFPX($$)
2044             {
2045 1     1 0 4 my ($et, $dirInfo) = @_;
2046 1         4 my $raf = $$dirInfo{RAF};
2047 1         4 my ($buff, $out, $oldIndent, $miniStreamBuff);
2048 1         0 my ($tag, %hier, %objIndex, %loadedDifSect);
2049              
2050             # handle FPX format in memory from PNG cpIp chunk
2051 1 50       4 $raf or $raf = File::RandomAccess->new($$dirInfo{DataPt});
2052              
2053             # read header
2054 1 50       5 return 0 unless $raf->Read($buff,HDR_SIZE) == HDR_SIZE;
2055             # check signature
2056 1 50       6 return 0 unless $buff =~ /^\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1/;
2057              
2058             # set FileType initially based on file extension (we may override this later)
2059 1         13 my $fileType = $$et{FILE_EXT};
2060 1 50 33     11 $fileType = 'FPX' unless $fileType and $fpxFileType{$fileType};
2061 1         9 $et->SetFileType($fileType);
2062 1 50       11 SetByteOrder(substr($buff, 0x1c, 2) eq "\xff\xfe" ? 'MM' : 'II');
2063 1         4 my $tagTablePtr = GetTagTable('Image::ExifTool::FlashPix::Main');
2064 1         8 my $verbose = $et->Options('Verbose');
2065             # copy LargeFileSupport option to RAF for use in LoadChain
2066 1         4 $$raf{LargeFileSupport} = $et->Options('LargeFileSupport');
2067              
2068 1         7 my $sectSize = 1 << Get16u(\$buff, 0x1e);
2069 1         4 my $miniSize = 1 << Get16u(\$buff, 0x20);
2070 1         5 my $fatCount = Get32u(\$buff, 0x2c); # number of FAT sectors
2071 1         3 my $dirStart = Get32u(\$buff, 0x30); # first directory sector
2072 1         3 my $miniCutoff = Get32u(\$buff, 0x38); # minimum size for big-FAT streams
2073 1         19 my $miniStart = Get32u(\$buff, 0x3c); # first sector of mini-FAT
2074 1         5 my $miniCount = Get32u(\$buff, 0x40); # number of mini-FAT sectors
2075 1         3 my $difStart = Get32u(\$buff, 0x44); # first sector of DIF chain
2076 1         3 my $difCount = Get32u(\$buff, 0x48); # number of DIF sectors
2077              
2078 1 50       12 if ($verbose) {
2079 0         0 $out = $et->Options('TextOut');
2080 0         0 print $out " Sector size=$sectSize\n FAT: Count=$fatCount\n";
2081 0         0 print $out " DIR: Start=$dirStart\n";
2082 0         0 print $out " MiniFAT: Mini-sector size=$miniSize Start=$miniStart Count=$miniCount Cutoff=$miniCutoff\n";
2083 0         0 print $out " DIF FAT: Start=$difStart Count=$difCount\n";
2084             }
2085             #
2086             # load the FAT
2087             #
2088 1         2 my $pos = 0x4c;
2089 1         3 my $endPos = length($buff);
2090 1         3 my $fat = '';
2091 1         2 my $fatCountCheck = 0;
2092 1         2 my $difCountCheck = 0;
2093 1 50       11 my $hdrSize = $sectSize > HDR_SIZE ? $sectSize : HDR_SIZE;
2094              
2095 1         2 for (;;) {
2096 1         4 while ($pos <= $endPos - 4) {
2097 109         234 my $sect = Get32u(\$buff, $pos);
2098 109         171 $pos += 4;
2099 109 100       330 next if $sect == FREE_SECT;
2100 1         3 my $offset = $sect * $sectSize + $hdrSize;
2101 1         3 my $fatSect;
2102 1 50 33     5 unless ($raf->Seek($offset, 0) and
2103             $raf->Read($fatSect, $sectSize) == $sectSize)
2104             {
2105 0         0 $et->Error("Error reading FAT from sector $sect");
2106 0         0 return 1;
2107             }
2108 1         4 $fat .= $fatSect;
2109 1         9 ++$fatCountCheck;
2110             }
2111 1 50       7 last if $difStart >= END_OF_CHAIN;
2112             # read next DIF (Dual Indirect FAT) sector
2113 0 0       0 if (++$difCountCheck > $difCount) {
2114 0         0 $et->Warn('Unterminated DIF FAT');
2115 0         0 last;
2116             }
2117 0 0       0 if ($loadedDifSect{$difStart}) {
2118 0         0 $et->Warn('Cyclical reference in DIF FAT');
2119 0         0 last;
2120             }
2121 0         0 my $offset = $difStart * $sectSize + $hdrSize;
2122 0 0 0     0 unless ($raf->Seek($offset, 0) and $raf->Read($buff, $sectSize) == $sectSize) {
2123 0         0 $et->Error("Error reading DIF sector $difStart");
2124 0         0 return 1;
2125             }
2126 0         0 $loadedDifSect{$difStart} = 1;
2127             # set end of sector information in this DIF
2128 0         0 $pos = 0;
2129 0         0 $endPos = $sectSize - 4;
2130             # next time around we want to read next DIF in chain
2131 0         0 $difStart = Get32u(\$buff, $endPos);
2132             }
2133 1 50       5 if ($fatCountCheck != $fatCount) {
2134 0         0 $et->Warn("Bad number of FAT sectors (expected $fatCount but found $fatCountCheck)");
2135             }
2136             #
2137             # load the mini-FAT and the directory
2138             #
2139 1         6 my $miniFat = LoadChain($raf, $miniStart, \$fat, $sectSize, $hdrSize);
2140 1         4 my $dir = LoadChain($raf, $dirStart, \$fat, $sectSize, $hdrSize);
2141 1 50 33     9 unless (defined $miniFat and defined $dir) {
2142 0         0 $et->Error('Error reading mini-FAT or directory stream');
2143 0         0 return 1;
2144             }
2145 1 50       4 if ($verbose) {
2146 0         0 print $out " FAT [",length($fat)," bytes]:\n";
2147 0         0 $et->VerboseDump(\$fat);
2148 0         0 print $out " Mini-FAT [",length($miniFat)," bytes]:\n";
2149 0         0 $et->VerboseDump(\$miniFat);
2150 0         0 print $out " Directory [",length($dir)," bytes]:\n";
2151 0         0 $et->VerboseDump(\$dir);
2152             }
2153             #
2154             # process the directory
2155             #
2156 1 50       4 if ($verbose) {
2157 0         0 $oldIndent = $$et{INDENT};
2158 0         0 $$et{INDENT} .= '| ';
2159 0         0 $et->VerboseDir('FPX', undef, length $dir);
2160             }
2161 1         2 my $miniStream;
2162 1         3 $endPos = length($dir);
2163 1         2 my $index = 0;
2164 1         15 my $ee; # name of next tag to extract if unknown
2165 1 50       6 $ee = 0 if $et->Options('ExtractEmbedded');
2166              
2167 1         14 for ($pos=0; $pos<=$endPos-128; $pos+=128, ++$index) {
2168              
2169             # get directory entry type
2170             # (0=invalid, 1=storage, 2=stream, 3=lockbytes, 4=property, 5=root)
2171 8         32 my $type = Get8u(\$dir, $pos + 0x42);
2172 8 100       32 next if $type == 0; # skip invalid entries
2173 5 50       17 if ($type > 5) {
2174 0         0 $et->Warn("Invalid directory entry type $type");
2175 0         0 last; # rest of directory is probably garbage
2176             }
2177             # get entry name (note: this is supposed to be length in 2-byte
2178             # characters but this isn't what is done in my sample FPX file, so
2179             # be very tolerant of this count -- it's null terminated anyway)
2180 5         37 my $len = Get16u(\$dir, $pos + 0x40);
2181 5 100       14 $len > 32 and $len = 32;
2182 5         30 $tag = Image::ExifTool::Decode(undef, substr($dir,$pos,$len*2), 'UCS2', 'II', 'Latin');
2183 5         18 $tag =~ s/\0.*//s; # truncate at null (in case length was wrong)
2184              
2185 5 50 33     20 if ($tag eq '0' and not defined $ee) {
2186 0         0 $et->Warn('Use the ExtractEmbedded option to extract embedded information', 3);
2187             }
2188 5         22 my $sect = Get32u(\$dir, $pos + 0x74); # start sector number
2189 5         16 my $size = Get32u(\$dir, $pos + 0x78); # stream length
2190              
2191             # load Ministream (referenced from first directory entry)
2192 5 100       20 unless ($miniStream) {
2193 1         4 $miniStreamBuff = LoadChain($raf, $sect, \$fat, $sectSize, $hdrSize);
2194 1 50       7 unless (defined $miniStreamBuff) {
2195 0         0 $et->Warn('Error loading Mini-FAT stream');
2196 0         0 last;
2197             }
2198 1         11 $miniStream = File::RandomAccess->new(\$miniStreamBuff);
2199             }
2200              
2201 5         10 my $tagInfo;
2202 5 100 33     25 if ($$tagTablePtr{$tag}) {
    50          
2203 3         14 $tagInfo = $et->GetTagInfo($tagTablePtr, $tag);
2204             } elsif (defined $ee and $tag eq $ee) {
2205 0         0 $tagInfo = ''; # won't know the actual tagID untile we read the stream
2206 0         0 $ee = sprintf('%x', hex($ee)+1); # tag to look for next
2207             } else {
2208             # remove instance number or class ID from tag if necessary
2209             $tagInfo = $et->GetTagInfo($tagTablePtr, $1) if
2210             ($tag =~ /(.*) \d{6}$/s and $$tagTablePtr{$1}) or
2211             ($tag =~ /(.*)_[0-9a-f]{16}$/s and $$tagTablePtr{$1}) or
2212 2 0 33     78 ($tag =~ /(.*)_[0-9]{4}$/s and $$tagTablePtr{$1}); # IeImg instances
      33        
      33        
      33        
      33        
2213             }
2214              
2215 5         18 my $lSib = Get32u(\$dir, $pos + 0x44); # left sibling
2216 5         15 my $rSib = Get32u(\$dir, $pos + 0x48); # right sibling
2217 5         15 my $chld = Get32u(\$dir, $pos + 0x4c); # child directory
2218              
2219             # save information about object hierarchy
2220 5         11 my ($obj, $sub);
2221 5 100       28 $obj = $hier{$index} or $obj = $hier{$index} = { };
2222 5 100       17 $$obj{Left} = $lSib unless $lSib == FREE_SECT;
2223 5 100       44 $$obj{Right} = $rSib unless $rSib == FREE_SECT;
2224 5 100       16 unless ($chld == FREE_SECT) {
2225 1         3 $$obj{Child} = $chld;
2226 1 50       6 $sub = $hier{$chld} or $sub = $hier{$chld} = { };
2227 1         4 $$sub{Parent} = $index;
2228             }
2229              
2230 5 100 66     27 next unless defined $tagInfo or $verbose;
2231              
2232             # load the data for stream types
2233 3         9 my $extra = '';
2234 3   33     14 my $typeStr = $dirEntryType[$type] || $type;
2235 3 50       11 if ($typeStr eq 'STREAM') {
    0          
2236 3 50       14 if ($size >= $miniCutoff) {
    50          
2237             # stream is in the main FAT
2238 0         0 $buff = LoadChain($raf, $sect, \$fat, $sectSize, $hdrSize);
2239             } elsif ($size) {
2240             # stream is in the mini-FAT
2241 3         35 $buff = LoadChain($miniStream, $sect, \$miniFat, $miniSize, 0);
2242             } else {
2243 0         0 $buff = ''; # an empty stream
2244             }
2245 3 50       11 unless (defined $buff) {
2246 0 0       0 my $name = $tagInfo ? $$tagInfo{Name} : 'unknown';
2247 0         0 $et->Warn("Error reading $name stream");
2248 0         0 $buff = '';
2249             }
2250             } elsif ($typeStr eq 'ROOT') {
2251 0         0 $buff = $miniStreamBuff;
2252 0         0 $extra .= ' (Ministream)';
2253             } else {
2254 0         0 $buff = '';
2255 0         0 undef $size;
2256             }
2257 3 50       10 if ($verbose) {
2258 0         0 my $flags = Get8u(\$dir, $pos + 0x43); # 0=red, 1=black
2259 0   0     0 my $col = { 0 => 'Red', 1 => 'Black' }->{$flags} || $flags;
2260 0         0 $extra .= " Type=$typeStr Flags=$col";
2261 0 0       0 $extra .= " Left=$lSib" unless $lSib == FREE_SECT;
2262 0 0       0 $extra .= " Right=$rSib" unless $rSib == FREE_SECT;
2263 0 0       0 $extra .= " Child=$chld" unless $chld == FREE_SECT;
2264 0 0       0 $extra .= " Size=$size" if defined $size;
2265 0         0 my $name;
2266 0 0 0     0 $name = "Unknown_0x$tag" if not $tagInfo and $tag =~ /^[0-9a-f]{1,3}$/;
2267 0         0 $et->VerboseInfo($tag, $tagInfo,
2268             Index => $index,
2269             Value => $buff,
2270             DataPt => \$buff,
2271             Extra => $extra,
2272             # Size => $size, (moved to $extra so we can see the rest of the stream if larger)
2273             Name => $name,
2274             );
2275             }
2276 3 50 33     17 if (defined $tagInfo and $buff) {
2277 3         8 my $num = $$et{NUM_FOUND};
2278 3 100 66     41 if ($tagInfo and $$tagInfo{SubDirectory}) {
    50 33        
2279 2         6 my $subdir = $$tagInfo{SubDirectory};
2280             my %dirInfo = (
2281             DataPt => \$buff,
2282             DirStart => $$subdir{DirStart},
2283             DirLen => length $buff,
2284             Multi => $$tagInfo{Multi},
2285 2         17 );
2286 2         11 my $subTablePtr = GetTagTable($$subdir{TagTable});
2287 2         17 $et->ProcessDirectory(\%dirInfo, $subTablePtr, $$subdir{ProcessProc});
2288             } elsif (defined $size and $size > length($buff)) {
2289 0         0 $et->Warn('Truncated object');
2290             } else {
2291 1 50 33     13 $buff = substr($buff, 0, $size) if defined $size and $size < length($buff);
2292 1 50       10 if ($tag =~ /^IeImg_0*(\d+)$/) {
    50          
2293             # set document number for embedded images and their positions (if available, VNT files)
2294 0         0 my $num = $1;
2295 0         0 $$et{DOC_NUM} = ++$$et{DOC_COUNT};
2296 0         0 $et->FoundTag($tagInfo, $buff);
2297 0 0 0     0 if ($$et{IeImg_lkup} and $$et{IeImg_lkup}{$num}) {
2298             # save position of this image
2299 0         0 $et->HandleTag($tagTablePtr, IeImg_rect => $$et{IeImg_lkup}{$num});
2300 0         0 delete $$et{IeImg_lkup}{$num};
2301 0 0 0     0 if ($$et{IeImg_class} and $$et{IeImg_class}{$num}) {
2302 0         0 $et->HandleTag($tagTablePtr, IeImg_class => $$et{IeImg_class}{$num});
2303 0         0 delete $$et{IeImg_class}{$num};
2304             }
2305             }
2306 0         0 delete $$et{DOC_NUM};
2307             } elsif (not $tagInfo) {
2308             # extract some embedded information from PNG Plus images
2309 0 0 0     0 if ($buff =~ /^(.{19,40})(\xff\xd8\xff\xe0|\x89PNG\r\n\x1a\n)/sg) {
    0          
2310 0 0       0 my $id = $2 eq "\xff\xd8\xff\xe0" ? '_eeJPG' : '_eePNG';
2311 0         0 $et->HandleTag($tagTablePtr, $id, substr($buff, length($1)));
2312             } elsif ($buff =~ /^\0\x80\0\0\x01\0\0\0\x0e\0/ and length($buff) > 18) {
2313 0         0 my $len = unpack('x17C', $buff);
2314 0 0       0 next if $len + 18 > length($buff);
2315 0         0 my $filename = $et->Decode(substr($buff,18,$len), 'UTF16', 'II');
2316 0         0 $et->HandleTag($tagTablePtr, '_eeLink', $filename);
2317             } else {
2318 0         0 next;
2319             }
2320             } else {
2321 1         8 $et->FoundTag($tagInfo, $buff);
2322             }
2323             }
2324             # save object index number for all found tags
2325 3         18 my $num2 = $$et{NUM_FOUND};
2326 3         114 $objIndex{++$num} = $index while $num < $num2;
2327             }
2328             }
2329             # set document numbers for tags extracted from embedded documents
2330 1 50       8 unless ($$et{DOC_NUM}) {
2331             # initialize document number for all objects, beginning at root (index 0)
2332 1         7 SetDocNum(\%hier, 0);
2333             # set family 3 group name for all tags in embedded documents
2334 1         4 my $order = $$et{FILE_ORDER};
2335 1         3 my (@pri, $copy, $member);
2336 1         58 foreach $tag (keys %$order) {
2337 50         102 my $num = $$order{$tag};
2338 50 100 66     180 next unless defined $num and $objIndex{$num};
2339 39 50       116 my $obj = $hier{$objIndex{$num}} or next;
2340 39         70 my $docNums = $$obj{DocNum};
2341 39 50 33     137 next unless $docNums and @$docNums;
2342 0         0 $$et{TAG_EXTRA}{$tag}{G3} = join '-', @$docNums;
2343 0 0       0 push @pri, $tag unless $tag =~ / /; # save keys for priority sub-doc tags
2344             }
2345             # swap priority sub-document tags with main document tags if they exist
2346 1         9 foreach $tag (@pri) {
2347 0         0 for ($copy=1; ;++$copy) {
2348 0         0 my $key = "$tag ($copy)";
2349 0 0       0 last unless defined $$et{VALUE}{$key};
2350 0 0       0 next if $$et{TAG_EXTRA}{$key}{G3}; # not Main if family 3 group is set
2351 0         0 foreach $member ('PRIORITY','VALUE','FILE_ORDER','TAG_INFO','TAG_EXTRA') {
2352 0         0 my $pHash = $$et{$member};
2353 0         0 my $t = $$pHash{$tag};
2354 0         0 $$pHash{$tag} = $$pHash{$key};
2355 0         0 $$pHash{$key} = $t;
2356             }
2357 0         0 last;
2358             }
2359             }
2360             }
2361 1 50       5 $$et{INDENT} = $oldIndent if $verbose;
2362             # try to better identify the file type
2363 1 50       7 if ($$et{FileType} eq 'FPX') {
2364 0   0     0 my $val = $$et{CompObjUserType} || $$et{Software};
2365 0 0       0 if ($val) {
2366 0         0 my %type = ( '^3ds Max' => 'MAX', Word => 'DOC', PowerPoint => 'PPT', Excel => 'XLS' );
2367 0         0 my $pat;
2368 0         0 foreach $pat (sort keys %type) {
2369 0 0       0 next unless $val =~ /$pat/;
2370 0         0 $et->OverrideFileType($type{$pat});
2371 0         0 last;
2372             }
2373             }
2374             }
2375             # process Word document table
2376 1         8 ProcessDocumentTable($et);
2377              
2378 1 50 33     6 if ($$et{IeImg_lkup} and %{$$et{IeImg_lkup}}) {
  0         0  
2379 0         0 $et->Warn('Image positions exist without corresponding images');
2380             }
2381              
2382 1         38 return 1;
2383             }
2384              
2385             1; # end
2386              
2387             __END__