File Coverage

blib/lib/Document/OOXML.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1 2     2   188608 use utf8;
  2         4  
  2         11  
2             package Document::OOXML;
3 2     2   669 use Moose;
  2         747412  
  2         13  
4 2     2   12954 use namespace::autoclean;
  2         12033  
  2         6  
5              
6             # ABSTRACT: Manipulation of Office Open XML files
7             our $VERSION = '0.172601'; # VERSION
8              
9 2     2   546 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
  2         65141  
  2         272  
10 2     2   15 use Carp;
  2         4  
  2         137  
11 2     2   12 use List::Util qw(any);
  2         4  
  2         1037  
12 2     2   159 use XML::LibXML;
  0            
  0            
13              
14             use Document::OOXML::ContentTypes;
15             use Document::OOXML::Part::WordprocessingML;
16             use Document::OOXML::Document::Wordprocessor;
17              
18              
19             my $RELATIONSHIPS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
20              
21             my %ROOT_PART_REL_TYPES = (
22             transitionalDocument => 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument',
23             strictDocument => 'http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument',
24             );
25              
26             my %PART_TYPES = (
27             WordprocessingML => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml'
28             );
29              
30              
31             sub read_document {
32             my $class = shift;
33             my $filename = shift;
34              
35             my $zip = Archive::Zip->new();
36              
37             my $zip_status = $zip->read($filename);
38             croak("Cannot read: $zip_status") unless $zip_status == AZ_OK;
39              
40             my $content_types = do {
41             my $ct_xml = $zip->contents('[Content_Types].xml')
42             or croak("No member named '/[Content_Types].xml'. Is it OOXML?");
43              
44             Document::OOXML::ContentTypes->new_from_xml($ct_xml);
45             };
46              
47             my $base_rels = $zip->contents('_rels/.rels')
48             or croak("No member named '_rels/.rels' in document. Is it OOXML?");
49              
50             my $xml = XML::LibXML->load_xml( string => $base_rels );
51             my $xpc = XML::LibXML::XPathContext->new();
52             $xpc->registerNs('r' => $RELATIONSHIPS_NS);
53              
54             # The "old"/transitional XML uses schemas.openxmlformats.org
55             # "New"/ISO standard/strict XML uses purl.oclc.org/ooxml
56             my ($document_part_relation) = $xpc->findnodes(
57             qq{
58             /r:Relationships/r:Relationship[
59             \@Type='$ROOT_PART_REL_TYPES{transitionalDocument}'
60             or
61             \@Type='$ROOT_PART_REL_TYPES{strictDocument}'
62             ]
63             },
64             $xml->documentElement,
65             );
66              
67             my $id = $xpc->findvalue('@Id', $document_part_relation);
68             my $type = $xpc->findvalue('@Type', $document_part_relation);
69             my $part_name = $xpc->findvalue('@Target', $document_part_relation);
70              
71             my $strict;
72             if ($type eq $ROOT_PART_REL_TYPES{strictDocument}) {
73             $strict = 1;
74             } else {
75             $strict = 0;
76             }
77              
78             my $part_contents = $zip->contents($part_name)
79             or croak("No member named '$part_name' in document. Is it OOXML?");
80              
81             my $doc_part = _parse_part(
82             content_type => $content_types->get_content_type_for_part("/$part_name"),
83             contents => $part_contents,
84             part_name => $part_name,
85             is_strict => $strict,
86             );
87              
88             my $document_class;
89             if ($doc_part->isa('Document::OOXML::Part::WordprocessingML')) {
90             $document_class = 'Document::OOXML::Document::Wordprocessor';
91             }
92             else {
93             croak("Unsupported document type");
94             }
95              
96             my $ooxml = $document_class->new(
97             content_types => $content_types,
98             filename => $filename,
99             source => $zip,
100             is_strict => $strict,
101             );
102              
103             # Parts have weak references to the document they're in, so they don't
104             # create reference loops.
105             #
106             # They can use this reference to find or add other parts (images,
107             # headers, footers, etc.) referenced by the main document.
108             $doc_part->document($ooxml);
109             $ooxml->set_document_part($doc_part);
110              
111             return $ooxml;
112             }
113              
114             sub _parse_part {
115             my %args = @_;
116              
117             if ($args{content_type} eq $PART_TYPES{WordprocessingML}) {
118             return Document::OOXML::Part::WordprocessingML->new_from_xml(
119             $args{part_name},
120             $args{contents},
121             $args{is_strict} ? 1 : 0,
122             );
123             }
124              
125             croak("Unknown part of type '$args{content_type}'");
126             }
127              
128             __PACKAGE__->meta->make_immutable;
129              
130             __END__
131              
132             =pod
133              
134             =encoding UTF-8
135              
136             =head1 NAME
137              
138             Document::OOXML - Manipulation of Office Open XML files
139              
140             =head1 VERSION
141              
142             version 0.172601
143              
144             =head1 SYNOPSIS
145              
146             my $doc = Document::OOXML->read_document('some.docx');
147              
148             $doc->search_and_replace(
149             qr/examples?/,
150             sub { return reverse(shift); }
151             );
152              
153             =head1 DESCRIPTION
154              
155             This module provides a way to open, modify and save Office Open XML files
156             (also known as OOXML or Microsoft Office XML).
157              
158             =head1 METHODS
159              
160             =head2 read_document($filename)
161              
162             Opens the file named C<$filename> and parses it.
163              
164             If the file doesn't appear to be a valid package, it will croak.
165              
166             Returns an instance of a subclass of L<Document::OOXML::Document> that can
167             be used to manipulate the contents of the document:
168              
169             =over
170              
171             =item * L<Document::OOXML::Document::Wordprocessor>
172              
173             =back
174              
175             =head1 SEE ALSO
176              
177             The format of Office Open XML files is described in the
178             L<ISO/IEC 29500|https://www.iso.org/standard/71691.html> and
179             L<ECMA-376|https://www.ecma-international.org/publications/standards/Ecma-376.htm>
180             standards.
181              
182             =head1 AUTHOR
183              
184             Martijn van de Streek <martijn@vandestreek.net>
185              
186             =head1 COPYRIGHT AND LICENSE
187              
188             This software is copyright (c) 2017 by Martijn van de Streek.
189              
190             This is free software; you can redistribute it and/or modify it under
191             the same terms as the Perl 5 programming language system itself.
192              
193             =cut