OOXML.pm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. #------------------------------------------------------------------------------
  2. # File: OOXML.pm
  3. #
  4. # Description: Read Office Open XML+ZIP files
  5. #
  6. # Revisions: 2009/10/31 - P. Harvey Created
  7. #------------------------------------------------------------------------------
  8. package Image::ExifTool::OOXML;
  9. use strict;
  10. use vars qw($VERSION);
  11. use Image::ExifTool qw(:DataAccess :Utils);
  12. use Image::ExifTool::XMP;
  13. use Image::ExifTool::ZIP;
  14. $VERSION = '1.07';
  15. # test for recognized OOXML document extensions
  16. my %isOOXML = (
  17. DOCX => 1, DOCM => 1,
  18. DOTX => 1, DOTM => 1,
  19. POTX => 1, POTM => 1,
  20. PPSX => 1, PPSM => 1,
  21. PPTX => 1, PPTM => 1, THMX => 1,
  22. XLAM => 1,
  23. XLSX => 1, XLSM => 1, XLSB => 1,
  24. XLTX => 1, XLTM => 1,
  25. );
  26. # generate reverse lookup for file type based on MIME
  27. my %fileType;
  28. {
  29. my $type;
  30. foreach $type (keys %isOOXML) {
  31. $fileType{$Image::ExifTool::mimeType{$type}} = $type;
  32. }
  33. }
  34. # XML attributes to queue
  35. my %queuedAttrs;
  36. my %queueAttrs = (
  37. fmtid => 1,
  38. pid => 1,
  39. name => 1,
  40. );
  41. # keep track of items in a vector (to accumulate as a list)
  42. my $vectorCount;
  43. my @vectorVals;
  44. # Office Open XML tags
  45. %Image::ExifTool::OOXML::Main = (
  46. GROUPS => { 0 => 'XML', 1 => 'XML', 2 => 'Document' },
  47. PROCESS_PROC => \&Image::ExifTool::XMP::ProcessXMP,
  48. VARS => { NO_ID => 1 },
  49. NOTES => q{
  50. The Office Open XML (OOXML) format was introduced with Microsoft Office 2007
  51. and is used by file types such as DOCX, PPTX and XLSX. These are
  52. essentially ZIP archives containing XML files. The table below lists some
  53. tags which have been observed in OOXML documents, but ExifTool will extract
  54. any tags found from XML files of the OOXML document properties ("docProps")
  55. directory.
  56. B<Tips:>
  57. 1) Structural ZIP tags may be ignored (if desired) with C<--ZIP:all> on the
  58. command line.
  59. 2) Tags may be grouped by their document number in the ZIP archive with the
  60. C<-g3> or C<-G3> option.
  61. },
  62. # These tags all have 1:1 correspondence with FlashPix tags except for:
  63. # OOXML FlashPix
  64. # --------------- -------------
  65. # DocSecurity Security
  66. # Application Software
  67. # dc:Description Comments
  68. # dc:Creator Author
  69. Application => { },
  70. AppVersion => { },
  71. category => { },
  72. Characters => { },
  73. CharactersWithSpaces => { },
  74. CheckedBy => { },
  75. Client => { },
  76. Company => { },
  77. created => {
  78. Name => 'CreateDate',
  79. Groups => { 2 => 'Time' },
  80. Format => 'date',
  81. PrintConv => '$self->ConvertDateTime($val)',
  82. },
  83. createdType => { Hidden => 1, RawConv => 'undef' }, # ignore this XML type name
  84. DateCompleted => {
  85. Groups => { 2 => 'Time' },
  86. Format => 'date',
  87. PrintConv => '$self->ConvertDateTime($val)',
  88. },
  89. Department => { },
  90. Destination => { },
  91. Disposition => { },
  92. Division => { },
  93. DocSecurity => {
  94. # (http://msdn.microsoft.com/en-us/library/documentformat.openxml.extendedproperties.documentsecurity.aspx)
  95. PrintConv => {
  96. 0 => 'None',
  97. 1 => 'Password protected',
  98. 2 => 'Read-only recommended',
  99. 4 => 'Read-only enforced',
  100. 8 => 'Locked for annotations',
  101. },
  102. },
  103. DocumentNumber=> { },
  104. Editor => { Groups => { 2 => 'Author'} },
  105. ForwardTo => { },
  106. Group => { },
  107. HeadingPairs=> { },
  108. HiddenSlides=> { },
  109. HyperlinkBase=>{ },
  110. HyperlinksChanged => { PrintConv => { 'false' => 'No', 'true' => 'Yes' } },
  111. keywords => { },
  112. Language => { },
  113. lastModifiedBy => { Groups => { 2 => 'Author'} },
  114. lastPrinted => {
  115. Groups => { 2 => 'Time' },
  116. Format => 'date',
  117. PrintConv => '$self->ConvertDateTime($val)',
  118. },
  119. Lines => { },
  120. LinksUpToDate=>{ PrintConv => { 'false' => 'No', 'true' => 'Yes' } },
  121. Mailstop => { },
  122. Manager => { },
  123. Matter => { },
  124. MMClips => { },
  125. modified => {
  126. Name => 'ModifyDate',
  127. Groups => { 2 => 'Time' },
  128. Format => 'date',
  129. PrintConv => '$self->ConvertDateTime($val)',
  130. },
  131. modifiedType=> { Hidden => 1, RawConv => 'undef' }, # ignore this XML type name
  132. Notes => { },
  133. Office => { },
  134. Owner => { Groups => { 2 => 'Author'} },
  135. Pages => { },
  136. Paragraphs => { },
  137. PresentationFormat => { },
  138. Project => { },
  139. Publisher => { },
  140. Purpose => { },
  141. ReceivedFrom=> { },
  142. RecordedBy => { },
  143. RecordedDate=> {
  144. Groups => { 2 => 'Time' },
  145. Format => 'date',
  146. PrintConv => '$self->ConvertDateTime($val)',
  147. },
  148. Reference => { },
  149. revision => { Name => 'RevisionNumber' },
  150. ScaleCrop => { PrintConv => { 'false' => 'No', 'true' => 'Yes' } },
  151. SharedDoc => { PrintConv => { 'false' => 'No', 'true' => 'Yes' } },
  152. Slides => { },
  153. Source => { },
  154. Status => { },
  155. TelephoneNumber => { },
  156. Template => { },
  157. TitlesOfParts=>{ },
  158. TotalTime => {
  159. Name => 'TotalEditTime',
  160. PrintConv => 'ConvertTimeSpan($val, 60)',
  161. },
  162. Typist => { },
  163. Words => { },
  164. );
  165. #------------------------------------------------------------------------------
  166. # Generate a tag ID for this XML tag
  167. # Inputs: 0) tag property name list ref
  168. # Returns: tagID and outtermost interesting namespace (or '' if no namespace)
  169. sub GetTagID($)
  170. {
  171. my $props = shift;
  172. my ($tag, $prop, $namespace);
  173. foreach $prop (@$props) {
  174. # split name into namespace and property name
  175. # (Note: namespace can be '' for property qualifiers)
  176. my ($ns, $nm) = ($prop =~ /(.*?):(.*)/) ? ($1, $2) : ('', $prop);
  177. next if $ns eq 'vt'; # ignore 'vt' properties
  178. if (defined $tag) {
  179. $tag .= ucfirst($nm); # add to tag name
  180. } elsif ($prop ne 'Properties' and $prop ne 'cp:coreProperties' and
  181. $prop ne 'property')
  182. {
  183. $tag = $nm;
  184. # save namespace of first property to contribute to tag name
  185. $namespace = $ns unless $namespace;
  186. }
  187. }
  188. return ($tag, $namespace || '');
  189. }
  190. #------------------------------------------------------------------------------
  191. # We found an XMP property name/value
  192. # Inputs: 0) ExifTool object ref, 1) tag table ref
  193. # 2) reference to array of XMP property names (last is current property)
  194. # 3) property value, 4) attribute hash ref (not used here)
  195. # Returns: 1 if valid tag was found
  196. sub FoundTag($$$$;$)
  197. {
  198. my ($et, $tagTablePtr, $props, $val, $attrs) = @_;
  199. return 0 unless @$props;
  200. my $verbose = $et->Options('Verbose');
  201. my $tag = $$props[-1];
  202. $et->VPrint(0, " | - Tag '", join('/',@$props), "'\n") if $verbose > 1;
  203. # un-escape XML character entities
  204. $val = Image::ExifTool::XMP::UnescapeXML($val);
  205. # convert OOXML-escaped characters (eg. "_x0000d_" is a newline)
  206. $val =~ s/_x([0-9a-f]{4})_/Image::ExifTool::PackUTF8(hex($1))/gie;
  207. # convert from UTF8 to ExifTool Charset
  208. $val = $et->Decode($val, 'UTF8');
  209. # queue this attribute for later if necessary
  210. if ($queueAttrs{$tag}) {
  211. $queuedAttrs{$tag} = $val;
  212. return 0;
  213. }
  214. my $ns;
  215. ($tag, $ns) = GetTagID($props);
  216. if (not $tag) {
  217. # all properties are in ignored namespaces
  218. # so 'name' from our queued attributes for the tag
  219. my $name = $queuedAttrs{name} or return 0;
  220. $name =~ s/(^| )([a-z])/$1\U$2/g; # start words with uppercase
  221. ($tag = $name) =~ tr/-_a-zA-Z0-9//dc;
  222. return 0 unless length $tag;
  223. unless ($$tagTablePtr{$tag}) {
  224. my %tagInfo = (
  225. Name => $tag,
  226. Description => $name,
  227. );
  228. # format as a date/time value if type is 'vt:filetime'
  229. if ($$props[-1] eq 'vt:filetime') {
  230. $tagInfo{Groups} = { 2 => 'Time' },
  231. $tagInfo{Format} = 'date',
  232. $tagInfo{PrintConv} = '$self->ConvertDateTime($val)';
  233. }
  234. $et->VPrint(0, " | [adding $tag]\n") if $verbose;
  235. AddTagToTable($tagTablePtr, $tag, \%tagInfo);
  236. }
  237. } elsif ($tag eq 'xmlns') {
  238. # ignore namespaces (for now)
  239. return 0;
  240. } elsif (ref $Image::ExifTool::XMP::Main{$ns} eq 'HASH' and
  241. $Image::ExifTool::XMP::Main{$ns}{SubDirectory})
  242. {
  243. # use standard XMP table if it exists
  244. my $table = $Image::ExifTool::XMP::Main{$ns}{SubDirectory}{TagTable};
  245. no strict 'refs';
  246. if ($table and %$table) {
  247. $tagTablePtr = Image::ExifTool::GetTagTable($table);
  248. }
  249. } elsif (@$props > 2 and grep /^vt:vector$/, @$props) {
  250. # handle vector properties (accumulate as lists)
  251. if ($$props[-1] eq 'vt:size') {
  252. $vectorCount = $val;
  253. undef @vectorVals;
  254. return 0;
  255. } elsif ($$props[-1] eq 'vt:baseType') {
  256. return 0; # ignore baseType
  257. } elsif ($vectorCount) {
  258. --$vectorCount;
  259. if ($vectorCount) {
  260. push @vectorVals, $val;
  261. return 0;
  262. }
  263. $val = [ @vectorVals, $val ] if @vectorVals;
  264. # Note: we will lose any improper-sized vector elements here
  265. }
  266. }
  267. # add any unknown tags to table
  268. if ($$tagTablePtr{$tag}) {
  269. my $tagInfo = $$tagTablePtr{$tag};
  270. if (ref $tagInfo eq 'HASH') {
  271. # reformat date/time values
  272. my $fmt = $$tagInfo{Format} || $$tagInfo{Writable} || '';
  273. $val = Image::ExifTool::XMP::ConvertXMPDate($val) if $fmt eq 'date';
  274. }
  275. } else {
  276. $et->VPrint(0, " [adding $tag]\n") if $verbose;
  277. AddTagToTable($tagTablePtr, $tag, { Name => ucfirst $tag });
  278. }
  279. # save the tag
  280. $et->HandleTag($tagTablePtr, $tag, $val);
  281. # start fresh for next tag
  282. undef $vectorCount;
  283. undef %queuedAttrs;
  284. return 1;
  285. }
  286. #------------------------------------------------------------------------------
  287. # Extract information from an OOXML file
  288. # Inputs: 0) ExifTool object reference, 1) dirInfo reference
  289. # Returns: 1
  290. # Notes: Upon entry to this routine, the file type has already been verified
  291. # and the dirInfo hash contains 2 elements unique to this process proc:
  292. # MIME - mime type of main document from "[Content_Types].xml"
  293. # ZIP - reference to Archive::Zip object for this file
  294. sub ProcessDOCX($$)
  295. {
  296. my ($et, $dirInfo) = @_;
  297. my $zip = $$dirInfo{ZIP};
  298. my $tagTablePtr = GetTagTable('Image::ExifTool::OOXML::Main');
  299. my $mime = $$dirInfo{MIME} || $Image::ExifTool::mimeType{DOCX};
  300. # set the file type ('DOCX' by default)
  301. my $fileType = $fileType{$mime};
  302. if ($fileType) {
  303. # THMX is a special case because its contents.main MIME types is PPTX
  304. if ($fileType eq 'PPTX' and $$et{FILE_EXT} and $$et{FILE_EXT} eq 'THMX') {
  305. $fileType = 'THMX';
  306. }
  307. } else {
  308. $et->VPrint(0, "Unrecognized MIME type: $mime\n");
  309. # get MIME type according to file extension
  310. $fileType = $$et{FILE_EXT};
  311. # default to 'DOCX' if this isn't a known OOXML extension
  312. $fileType = 'DOCX' unless $fileType and $isOOXML{$fileType};
  313. }
  314. $et->SetFileType($fileType);
  315. # must catch all Archive::Zip warnings
  316. local $SIG{'__WARN__'} = \&Image::ExifTool::ZIP::WarnProc;
  317. # extract meta information from all files in ZIP "docProps" directory
  318. my $docNum = 0;
  319. my @members = $zip->members();
  320. my $member;
  321. foreach $member (@members) {
  322. # get filename of this ZIP member
  323. my $file = $member->fileName();
  324. next unless defined $file;
  325. $et->VPrint(0, "File: $file\n");
  326. # set the document number and extract ZIP tags
  327. $$et{DOC_NUM} = ++$docNum;
  328. Image::ExifTool::ZIP::HandleMember($et, $member);
  329. # process only XML and JPEG/WMF thumbnail images in "docProps" directory
  330. next unless $file =~ m{^docProps/(.*\.xml|(thumbnail\.(jpe?g|wmf)))$}i;
  331. # get the file contents (CAREFUL! $buff MUST be local since we hand off a value ref)
  332. my ($buff, $status) = $zip->contents($member);
  333. $status and $et->Warn("Error extracting $file"), next;
  334. # extract docProps/thumbnail.(jpg|mwf) as PreviewImage|PreviewMWF
  335. if ($file =~ /\.(jpe?g|wmf)$/i) {
  336. my $tag = $file =~ /\.wmf$/i ? 'PreviewWMF' : 'PreviewImage';
  337. $et->FoundTag($tag, \$buff);
  338. next;
  339. }
  340. # process XML files (docProps/app.xml, docProps/core.xml, docProps/custom.xml)
  341. my %dirInfo = (
  342. DataPt => \$buff,
  343. DirLen => length $buff,
  344. DataLen => length $buff,
  345. XMPParseOpts => {
  346. FoundProc => \&FoundTag,
  347. },
  348. );
  349. $et->ProcessDirectory(\%dirInfo, $tagTablePtr);
  350. undef $buff; # (free memory now)
  351. }
  352. delete $$et{DOC_NUM};
  353. return 1;
  354. }
  355. 1; # end
  356. __END__
  357. =head1 NAME
  358. Image::ExifTool::OOXML - Read Office Open XML+ZIP files
  359. =head1 SYNOPSIS
  360. This module is used by Image::ExifTool
  361. =head1 DESCRIPTION
  362. This module contains definitions required by Image::ExifTool to extract meta
  363. information from Office Open XML files. This is the format of Word, Excel
  364. and PowerPoint files written by Microsoft Office 2007 -- essentially ZIP
  365. archives of XML files.
  366. =head1 AUTHOR
  367. Copyright 2003-2016, Phil Harvey (phil at owl.phy.queensu.ca)
  368. This library is free software; you can redistribute it and/or modify it
  369. under the same terms as Perl itself.
  370. =head1 SEE ALSO
  371. L<Image::ExifTool::TagNames/OOXML Tags>,
  372. L<Image::ExifTool::TagNames/FlashPix Tags>,
  373. L<Image::ExifTool(3pm)|Image::ExifTool>
  374. =cut