openxml-package 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +121 -0
- data/Rakefile +9 -0
- data/lib/openxml-package.rb +1 -0
- data/lib/openxml-package/version.rb +3 -0
- data/lib/openxml/builder.rb +45 -0
- data/lib/openxml/content_types_presets.rb +18 -0
- data/lib/openxml/errors.rb +8 -0
- data/lib/openxml/package.rb +135 -0
- data/lib/openxml/part.rb +32 -0
- data/lib/openxml/parts.rb +10 -0
- data/lib/openxml/parts/content_types.rb +51 -0
- data/lib/openxml/parts/rels.rb +57 -0
- data/lib/openxml/parts/unparsed_part.rb +20 -0
- data/lib/openxml/rubyzip_fix.rb +17 -0
- data/lib/openxml/types.rb +6 -0
- data/openxml-package.gemspec +37 -0
- data/test/content_types_test.rb +34 -0
- data/test/package_test.rb +150 -0
- data/test/support/sample.docx +0 -0
- data/test/test_helper.rb +16 -0
- metadata +259 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9125838f28df960214ac225121b29ecabee182e4
|
4
|
+
data.tar.gz: 23eefb7c402ecf3b0c500c1241a9e5105c24a7e3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b22bbdd8a0064445003508a9f5c96528cf5950725f863a5c8eba95389885f3767f6c5d44ec325bb4a7e451cdfe788d84afaf2445454472d437606b182d69c93b
|
7
|
+
data.tar.gz: 81aa9f271fb5f65ca9e31c5b6a03b99d8f701003d1fd534749c5c87863cc84eb12b72f717e49065d4007502cada293c6ff9b31db6f6b2637ece7c64b2196fd63
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Bob Lail
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# OpenXml::Package
|
2
|
+
|
3
|
+
A Ruby implementation of [DocumentFormat.OpenXml.Packaging.OpenXmlPackage](http://msdn.microsoft.com/en-us/library/documentformat.openxml.packaging.openxmlpackage_members(v=office.14).aspx) from Microsoft's Open XML SDK.
|
4
|
+
|
5
|
+
The base class for [Rocx::Package](https://github.com/openxml/openxml-docx/blob/master/lib/openxml-docx/package.rb), [Xlsx::Package](https://github.com/openxml/openxml-xlsx/blob/master/lib/openxml-xlsx/package.rb), and [Pptx::Package](https://github.com/openxml/openxml-pptx/blob/master/lib/openxml-pptx/package.rb).
|
6
|
+
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
gem 'openxml-package'
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
|
16
|
+
$ bundle
|
17
|
+
|
18
|
+
Or install it yourself as:
|
19
|
+
|
20
|
+
$ gem install openxml-package
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
#### Writing
|
27
|
+
|
28
|
+
You can assemble an Open XML Package in-memory and then write it to disk:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
package = OpenXml::Package.new
|
32
|
+
package.add_part "content/document.xml", OpenXml::Parts::UnparsedPart.new("<document></document>")
|
33
|
+
package.add_part "media/image.png", OpenXml::Parts::UnparsedPart.new(File.open(image_path, "rb", &:read))
|
34
|
+
package.write_to "~/Desktop/output.zip"
|
35
|
+
```
|
36
|
+
|
37
|
+
|
38
|
+
#### Reading
|
39
|
+
|
40
|
+
You can read the contents of an Open XML Package:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
OpenXmlPackage.open("~/Desktop/output.zip") do |package|
|
44
|
+
package.parts.keys # => ["content/document.xml", "media/image.png"]
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
|
49
|
+
#### Subclassing
|
50
|
+
|
51
|
+
`OpenXml::Package` is intended to be the base class for libraries that implement Open XML formats for Microsoft Office products.
|
52
|
+
|
53
|
+
For example, a very simple Microsoft Word document can be defined as follows:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
require "openxml/package"
|
57
|
+
|
58
|
+
module Rocx
|
59
|
+
class Package < OpenXml::Package
|
60
|
+
attr_reader :document,
|
61
|
+
:doc_rels,
|
62
|
+
:settings,
|
63
|
+
:styles
|
64
|
+
|
65
|
+
content_types do
|
66
|
+
default "png", TYPE_PNG
|
67
|
+
override "/word/styles.xml", TYPE_STYLES
|
68
|
+
override "/word/settings.xml", TYPE_SETTINGS
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize
|
72
|
+
super
|
73
|
+
|
74
|
+
rels.add_relationship REL_DOCUMENT, "/word/document.xml"
|
75
|
+
@doc_rels = OpenXml::Parts::Rels.new([
|
76
|
+
{ type: REL_STYLES, target: "/word/styles.xml"},
|
77
|
+
{ type: REL_SETTINGS, target: "/word/settings.xml"}
|
78
|
+
])
|
79
|
+
@settings = Rocx::Parts::Settings.new
|
80
|
+
@styles = Rocx::Parts::Styles.new
|
81
|
+
@document = Rocx::Parts::Document.new
|
82
|
+
|
83
|
+
add_part "word/_rels/document.xml.rels", doc_rels
|
84
|
+
add_part "word/document.xml", document
|
85
|
+
add_part "word/settings.xml", settings
|
86
|
+
add_part "word/styles.xml", styles
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
This gem also defines two "Parts" that are commonly used in Open XML packages.
|
94
|
+
|
95
|
+
##### OpenXml::Parts::ContentTypes
|
96
|
+
|
97
|
+
Is used to identify the ContentType of all of the files in the package. There are two ways of identifying content types:
|
98
|
+
|
99
|
+
1. **Default**: declares the default content type for a file with a given extension
|
100
|
+
2. **Override**: declares the content type for a specific file with the given path inside the package
|
101
|
+
|
102
|
+
##### OpenXml::Parts::Rels
|
103
|
+
|
104
|
+
Is used to identify links within the package
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
## Contributing
|
109
|
+
|
110
|
+
1. Fork it ( https://github.com/openxml/openxml-package/fork )
|
111
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
112
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
113
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
114
|
+
5. Create a new Pull Request
|
115
|
+
|
116
|
+
#### Reference
|
117
|
+
|
118
|
+
- [DocumentFormat.OpenXml.Packaging.OpenXmlPackage](http://msdn.microsoft.com/en-us/library/documentformat.openxml.packaging.openxmlpackage_members(v=office.14).aspx)
|
119
|
+
- [DocumentFormat.OpenXml.Packaging.OpenXmlPartContainer](http://msdn.microsoft.com/en-us/library/documentformat.openxml.packaging.openxmlpartcontainer_members(v=office.14).aspx)
|
120
|
+
- [DocumentFormat.OpenXml.Packaging.OpenXmlPart](http://msdn.microsoft.com/en-us/library/documentformat.openxml.packaging.openxmlpart_members(v=office.14).aspx)
|
121
|
+
- [System.IO.Packaging.Package](http://msdn.microsoft.com/en-us/library/system.io.packaging.package.aspx)
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "openxml/package"
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# Constructing a large XML document (5MB) with the Ox
|
2
|
+
# gem is about 4x faster than with Nokogiri and about
|
3
|
+
# 5x fater than with Builder.
|
4
|
+
#
|
5
|
+
# This class mimics the XML Builder DSL.
|
6
|
+
require "ox"
|
7
|
+
|
8
|
+
module OpenXml
|
9
|
+
class Builder
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@document = Ox::Document.new(version: "1.0")
|
13
|
+
@current = @document
|
14
|
+
yield self if block_given?
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
Ox.dump @document
|
19
|
+
end
|
20
|
+
alias :to_xml :to_s
|
21
|
+
|
22
|
+
def method_missing(tag_name, *args)
|
23
|
+
new_element = Ox::Element.new(tag_name)
|
24
|
+
attributes = args.extract_options!
|
25
|
+
attributes.each do |key, value|
|
26
|
+
new_element[key] = value
|
27
|
+
end
|
28
|
+
|
29
|
+
if block_given?
|
30
|
+
begin
|
31
|
+
was_current = @current
|
32
|
+
@current = new_element
|
33
|
+
yield self
|
34
|
+
ensure
|
35
|
+
@current = was_current
|
36
|
+
end
|
37
|
+
elsif value = args.first
|
38
|
+
new_element << value.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
@current << new_element
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module OpenXml
|
2
|
+
class ContentTypesPresets
|
3
|
+
attr_reader :defaults, :overrides
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@defaults, @overrides = {}, {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def default(extension, content_type)
|
10
|
+
defaults[extension] = content_type
|
11
|
+
end
|
12
|
+
|
13
|
+
def override(part_name, content_type)
|
14
|
+
overrides[part_name] = content_type
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require "openxml-package/version"
|
2
|
+
require "openxml/content_types_presets"
|
3
|
+
require "openxml/rubyzip_fix"
|
4
|
+
require "openxml/errors"
|
5
|
+
require "openxml/types"
|
6
|
+
require "openxml/parts"
|
7
|
+
require "zip"
|
8
|
+
|
9
|
+
module OpenXml
|
10
|
+
class Package
|
11
|
+
attr_reader :parts, :content_types, :rels
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def content_types_presets
|
17
|
+
@content_types_presets ||= OpenXml::ContentTypesPresets.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def content_types(&block)
|
21
|
+
content_types_presets.instance_eval &block
|
22
|
+
end
|
23
|
+
|
24
|
+
def open(path)
|
25
|
+
if block_given?
|
26
|
+
Zip::File.open(path) do |zipfile|
|
27
|
+
yield new(zipfile)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
new Zip::File.open(path)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def from_stream(stream)
|
35
|
+
stream = StringIO.new(stream) if stream.is_a?(String)
|
36
|
+
|
37
|
+
# Hack: Zip::Entry.read_c_dir_entry initializes
|
38
|
+
# a new Zip::Entry by calling `io.path`. Zip::Entry
|
39
|
+
# uses this to open the original zipfile; but in
|
40
|
+
# this case, the StringIO _is_ the original.
|
41
|
+
def stream.path
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
zipfile = ::Zip::File.new("", true, true)
|
46
|
+
zipfile.read_from_stream(stream)
|
47
|
+
new(zipfile)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
def initialize(zipfile=nil)
|
54
|
+
@zipfile = zipfile
|
55
|
+
@parts = {}
|
56
|
+
|
57
|
+
if zipfile
|
58
|
+
read_zipfile!
|
59
|
+
else
|
60
|
+
set_defaults
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
def add_part(path, part)
|
67
|
+
@parts[path] = part
|
68
|
+
end
|
69
|
+
|
70
|
+
def get_part(path)
|
71
|
+
@parts.fetch(path)
|
72
|
+
end
|
73
|
+
|
74
|
+
def type_of(path)
|
75
|
+
raise Errors::MissingContentTypesPart, "We haven't yet read [ContentTypes].xml; but are reading #{path.inspect}" unless content_types
|
76
|
+
content_types.of(path)
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
|
81
|
+
def close
|
82
|
+
zipfile.close if zipfile
|
83
|
+
end
|
84
|
+
|
85
|
+
def write_to(path)
|
86
|
+
File.open(path, "w") do |file|
|
87
|
+
file.write to_stream.string
|
88
|
+
end
|
89
|
+
end
|
90
|
+
alias :save :write_to
|
91
|
+
|
92
|
+
def to_stream
|
93
|
+
Zip::OutputStream.write_buffer do |io|
|
94
|
+
parts.each do |path, part|
|
95
|
+
io.put_next_entry path
|
96
|
+
io.write part.content
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
attr_reader :zipfile
|
104
|
+
|
105
|
+
def read_zipfile!
|
106
|
+
zipfile.entries.each do |entry|
|
107
|
+
path, part = entry.name, Parts::UnparsedPart.new(entry)
|
108
|
+
add_part path, case path
|
109
|
+
when "[Content_Types].xml" then @content_types = Parts::ContentTypes.parse(part.content)
|
110
|
+
when "_rels/.rels" then @rels = Parts::Rels.parse(part.content)
|
111
|
+
else part_for(path, type_of(path), part)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
def set_defaults
|
119
|
+
presets = self.class.content_types_presets
|
120
|
+
@content_types = Parts::ContentTypes.new(presets.defaults, presets.overrides)
|
121
|
+
add_part "[Content_Types].xml", content_types
|
122
|
+
|
123
|
+
@rels = Parts::Rels.new
|
124
|
+
add_part "_rels/.rels", rels
|
125
|
+
end
|
126
|
+
|
127
|
+
def part_for(path, content_type, part)
|
128
|
+
case content_type
|
129
|
+
when Types::RELATIONSHIPS then Parts::Rels.parse(part.content)
|
130
|
+
else part
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
data/lib/openxml/part.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
require "openxml/builder"
|
3
|
+
|
4
|
+
module OpenXml
|
5
|
+
class Part
|
6
|
+
include ::Nokogiri
|
7
|
+
|
8
|
+
def build_xml
|
9
|
+
OpenXml::Builder.new { |xml| yield xml }.to_xml
|
10
|
+
end
|
11
|
+
|
12
|
+
def build_standalone_xml(&block)
|
13
|
+
"<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" + build_xml(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def read
|
17
|
+
strip_whitespace to_xml
|
18
|
+
end
|
19
|
+
alias :content :read
|
20
|
+
|
21
|
+
def to_xml
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def strip_whitespace(xml)
|
28
|
+
xml.lines.map(&:strip).join
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module OpenXml
|
2
|
+
module Parts
|
3
|
+
class ContentTypes < OpenXml::Part
|
4
|
+
attr_reader :defaults, :overrides
|
5
|
+
|
6
|
+
REQUIRED_DEFAULTS = {
|
7
|
+
"xml" => Types::XML,
|
8
|
+
"rels" => Types::RELATIONSHIPS
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
def self.parse(xml)
|
12
|
+
document = Nokogiri(xml)
|
13
|
+
self.new.tap do |part|
|
14
|
+
document.css("Default").each do |default|
|
15
|
+
part.add_default default["Extension"], default["ContentType"]
|
16
|
+
end
|
17
|
+
document.css("Override").each do |default|
|
18
|
+
part.add_override default["PartName"], default["ContentType"]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(defaults={}, overrides={})
|
24
|
+
@defaults = REQUIRED_DEFAULTS.merge(defaults)
|
25
|
+
@overrides = overrides
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_default(extension, content_type)
|
29
|
+
defaults[extension] = content_type
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_override(part_name, content_type)
|
33
|
+
overrides[part_name] = content_type
|
34
|
+
end
|
35
|
+
|
36
|
+
def of(path)
|
37
|
+
overrides.fetch(path, defaults[File.extname(path)[1..-1]])
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_xml
|
41
|
+
build_xml do |xml|
|
42
|
+
xml.Types(xmlns: "http://schemas.openxmlformats.org/package/2006/content-types") {
|
43
|
+
defaults.each { |extension, content_type| xml.Default("Extension" => extension, "ContentType" => content_type) }
|
44
|
+
overrides.each { |part_name, content_type| xml.Override("PartName" => part_name, "ContentType" => content_type) }
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
|
3
|
+
module OpenXml
|
4
|
+
module Parts
|
5
|
+
class Rels < OpenXml::Part
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def self.parse(xml)
|
9
|
+
document = Nokogiri(xml)
|
10
|
+
self.new.tap do |part|
|
11
|
+
document.css("Relationship").each do |rel|
|
12
|
+
part.add_relationship rel["Type"], rel["Target"], rel["Id"]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(defaults=[])
|
18
|
+
@relationships = []
|
19
|
+
Array(defaults).each do |default|
|
20
|
+
add_relationship(*default.values_at("Type", "Target", "Id"))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_relationship(type, target, id=nil)
|
25
|
+
Relationship.new(type, target, id).tap do |relationship|
|
26
|
+
relationships.push relationship
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def each(&block)
|
31
|
+
relationships.each(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_xml
|
35
|
+
build_standalone_xml do |xml|
|
36
|
+
xml.Relationships(xmlns: "http://schemas.openxmlformats.org/package/2006/relationships") do
|
37
|
+
relationships.each do |rel|
|
38
|
+
xml.Relationship("Id" => rel.id, "Type" => rel.type, "Target" => rel.target)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
class Relationship < Struct.new(:type, :target, :id)
|
47
|
+
def initialize(type, target, id=nil)
|
48
|
+
super type, target, id || "R#{SecureRandom.hex}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
attr_reader :relationships
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module OpenXml
|
2
|
+
module Parts
|
3
|
+
class UnparsedPart
|
4
|
+
|
5
|
+
def initialize(content)
|
6
|
+
@content = content
|
7
|
+
end
|
8
|
+
|
9
|
+
def content
|
10
|
+
@content = @content.get_input_stream.read if promise?
|
11
|
+
@content
|
12
|
+
end
|
13
|
+
|
14
|
+
def promise?
|
15
|
+
@content.respond_to? :get_input_stream
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "zip"
|
2
|
+
|
3
|
+
module Zip
|
4
|
+
class InputStream
|
5
|
+
protected
|
6
|
+
|
7
|
+
# The problem in RubyZip 1.1.0 is that we only call `seek`
|
8
|
+
# when `io` is a File. We need to move the cursor to the
|
9
|
+
# right position when `io` is a StringIO as well.
|
10
|
+
def get_io(io, offset = 0)
|
11
|
+
io = ::File.open(io, "rb") unless io.is_a?(IO) || io.is_a?(StringIO)
|
12
|
+
io.seek(offset, ::IO::SEEK_SET)
|
13
|
+
io
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "openxml-package/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "openxml-package"
|
8
|
+
spec.version = OpenXmlPackage::VERSION
|
9
|
+
spec.authors = ["Bob Lail"]
|
10
|
+
spec.email = ["bob.lailfamily@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{A Ruby implementation of OpenXmlPackage}
|
13
|
+
spec.description = %q{A Ruby implementation of OpenXmlPackage}
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.homepage = "https://github.com/openxml/openxml-package"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "rubyzip", "~> 1.1.0"
|
23
|
+
spec.add_dependency "nokogiri"
|
24
|
+
spec.add_dependency "ox"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "rails", ">= 3.2", "< 5.0"
|
29
|
+
spec.add_development_dependency "minitest"
|
30
|
+
spec.add_development_dependency "minitest-reporters"
|
31
|
+
spec.add_development_dependency "minitest-reporters-turn_reporter"
|
32
|
+
spec.add_development_dependency "pry"
|
33
|
+
spec.add_development_dependency "rr"
|
34
|
+
spec.add_development_dependency "simplecov"
|
35
|
+
spec.add_development_dependency "shoulda-context"
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class ContentTypesTest < ActiveSupport::TestCase
|
4
|
+
attr_reader :content_types
|
5
|
+
|
6
|
+
WORDPROCESSING_DOCUMENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
|
7
|
+
|
8
|
+
setup do
|
9
|
+
@content_types = OpenXml::Parts::ContentTypes.new(
|
10
|
+
{"xml" => OpenXml::Types::XML, "rels" => OpenXml::Types::RELATIONSHIPS},
|
11
|
+
{"word/document.xml" => WORDPROCESSING_DOCUMENT_TYPE})
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
context "Given a path without an override" do
|
16
|
+
should "identify the content type from its extension" do
|
17
|
+
assert_equal OpenXml::Types::XML, content_types.of("content/some.xml")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "Given a path with an override" do
|
22
|
+
should "identify the content type from its path" do
|
23
|
+
assert_equal WORDPROCESSING_DOCUMENT_TYPE, content_types.of("word/document.xml")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "Given a path with an unrecognized extension" do
|
28
|
+
should "be nil" do
|
29
|
+
assert_equal nil, content_types.of("img/screenshot.jpg")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
require "fileutils"
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
class OpenXmlPackageTest < ActiveSupport::TestCase
|
6
|
+
attr_reader :package, :temp_file
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
context "#add_part" do
|
11
|
+
should "accept a path and a part" do
|
12
|
+
package = OpenXml::Package.new
|
13
|
+
assert_difference "package.parts.count", +1 do
|
14
|
+
package.add_part "PATH", OpenXml::Part.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
context "Writing" do
|
22
|
+
setup do
|
23
|
+
@temp_file = expand_path "../tmp/test.zip"
|
24
|
+
FileUtils.rm temp_file, force: true
|
25
|
+
end
|
26
|
+
|
27
|
+
context "Given a simple part" do
|
28
|
+
setup do
|
29
|
+
@package = OpenXml::Package.new
|
30
|
+
package.add_part "content/document.xml", OpenXml::Parts::UnparsedPart.new(document_content)
|
31
|
+
end
|
32
|
+
|
33
|
+
should "write a valid zip file with the expected parts" do
|
34
|
+
package.write_to temp_file
|
35
|
+
assert File.exists?(temp_file), "Expected the file #{temp_file.inspect} to have been created"
|
36
|
+
assert_equal %w{[Content_Types].xml _rels/.rels content/document.xml},
|
37
|
+
Zip::File.open(temp_file).entries.map(&:name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
context "Reading" do
|
45
|
+
context "Given a sample Word document" do
|
46
|
+
setup do
|
47
|
+
@temp_file = expand_path "./support/sample.docx"
|
48
|
+
@expected_contents = Set[
|
49
|
+
"[Content_Types].xml",
|
50
|
+
"_rels/.rels",
|
51
|
+
"docProps/app.xml",
|
52
|
+
"docProps/core.xml",
|
53
|
+
"docProps/thumbnail.jpeg",
|
54
|
+
"word/_rels/document.xml.rels",
|
55
|
+
"word/document.xml",
|
56
|
+
"word/fontTable.xml",
|
57
|
+
"word/media/image1.png",
|
58
|
+
"word/settings.xml",
|
59
|
+
"word/styles.xml",
|
60
|
+
"word/stylesWithEffects.xml",
|
61
|
+
"word/theme/theme1.xml",
|
62
|
+
"word/webSettings.xml" ]
|
63
|
+
end
|
64
|
+
|
65
|
+
context ".open" do
|
66
|
+
setup do
|
67
|
+
@package = OpenXml::Package.open(temp_file)
|
68
|
+
end
|
69
|
+
|
70
|
+
teardown do
|
71
|
+
package.close
|
72
|
+
end
|
73
|
+
|
74
|
+
should "discover the expected parts" do
|
75
|
+
assert_equal @expected_contents, package.parts.keys.to_set
|
76
|
+
end
|
77
|
+
|
78
|
+
should "read their content on-demand" do
|
79
|
+
assert_equal web_settings_content, package.get_part("word/webSettings.xml").content
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context ".from_stream" do
|
84
|
+
setup do
|
85
|
+
@package = OpenXml::Package.from_stream(File.open(temp_file, "rb", &:read))
|
86
|
+
end
|
87
|
+
|
88
|
+
should "also discover the expected parts" do
|
89
|
+
assert_equal @expected_contents, package.parts.keys.to_set
|
90
|
+
end
|
91
|
+
|
92
|
+
should "read their content" do
|
93
|
+
assert_equal web_settings_content, package.get_part("word/webSettings.xml").content
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "ContentTypes" do
|
98
|
+
setup do
|
99
|
+
@package = OpenXml::Package.open(temp_file)
|
100
|
+
end
|
101
|
+
|
102
|
+
teardown do
|
103
|
+
package.close
|
104
|
+
end
|
105
|
+
|
106
|
+
should "be parsed" do
|
107
|
+
assert_equal %w{jpeg png rels xml}, package.content_types.defaults.keys.sort
|
108
|
+
assert_equal "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
|
109
|
+
package.content_types.overrides["/word/document.xml"]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context "Rels" do
|
114
|
+
setup do
|
115
|
+
@package = OpenXml::Package.open(temp_file)
|
116
|
+
end
|
117
|
+
|
118
|
+
teardown do
|
119
|
+
package.close
|
120
|
+
end
|
121
|
+
|
122
|
+
should "be parsed" do
|
123
|
+
assert_equal %w{docProps/core.xml docProps/app.xml word/document.xml docProps/thumbnail.jpeg},
|
124
|
+
package.rels.map(&:target)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def document_content
|
135
|
+
<<-STR
|
136
|
+
<document>
|
137
|
+
<body>Works!</body>
|
138
|
+
</document>
|
139
|
+
STR
|
140
|
+
end
|
141
|
+
|
142
|
+
def web_settings_content
|
143
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\r\n<w:webSettings xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" xmlns:w14=\"http://schemas.microsoft.com/office/word/2010/wordml\" mc:Ignorable=\"w14\"><w:allowPNG/><w:doNotSaveAsSingleFile/></w:webSettings>"
|
144
|
+
end
|
145
|
+
|
146
|
+
def expand_path(path)
|
147
|
+
File.expand_path(File.join(File.dirname(__FILE__), path))
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
Binary file
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
require "simplecov"
|
4
|
+
SimpleCov.start do
|
5
|
+
add_filter "test/"
|
6
|
+
end
|
7
|
+
|
8
|
+
require "rails"
|
9
|
+
require "rails/test_help"
|
10
|
+
require "pry"
|
11
|
+
require "rr"
|
12
|
+
require "shoulda/context"
|
13
|
+
require "minitest/reporters/turn_reporter"
|
14
|
+
MiniTest::Reporters.use! Minitest::Reporters::TurnReporter.new
|
15
|
+
|
16
|
+
require "openxml/package"
|
metadata
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: openxml-package
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bob Lail
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-11-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rubyzip
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: nokogiri
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ox
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.6'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.6'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rails
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.2'
|
90
|
+
- - "<"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '5.0'
|
93
|
+
type: :development
|
94
|
+
prerelease: false
|
95
|
+
version_requirements: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '3.2'
|
100
|
+
- - "<"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '5.0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: minitest
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: minitest-reporters
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: minitest-reporters-turn_reporter
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: pry
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
type: :development
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
- !ruby/object:Gem::Dependency
|
160
|
+
name: rr
|
161
|
+
requirement: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '0'
|
173
|
+
- !ruby/object:Gem::Dependency
|
174
|
+
name: simplecov
|
175
|
+
requirement: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
type: :development
|
181
|
+
prerelease: false
|
182
|
+
version_requirements: !ruby/object:Gem::Requirement
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '0'
|
187
|
+
- !ruby/object:Gem::Dependency
|
188
|
+
name: shoulda-context
|
189
|
+
requirement: !ruby/object:Gem::Requirement
|
190
|
+
requirements:
|
191
|
+
- - ">="
|
192
|
+
- !ruby/object:Gem::Version
|
193
|
+
version: '0'
|
194
|
+
type: :development
|
195
|
+
prerelease: false
|
196
|
+
version_requirements: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
description: A Ruby implementation of OpenXmlPackage
|
202
|
+
email:
|
203
|
+
- bob.lailfamily@gmail.com
|
204
|
+
executables: []
|
205
|
+
extensions: []
|
206
|
+
extra_rdoc_files: []
|
207
|
+
files:
|
208
|
+
- ".gitignore"
|
209
|
+
- Gemfile
|
210
|
+
- LICENSE.txt
|
211
|
+
- README.md
|
212
|
+
- Rakefile
|
213
|
+
- lib/openxml-package.rb
|
214
|
+
- lib/openxml-package/version.rb
|
215
|
+
- lib/openxml/builder.rb
|
216
|
+
- lib/openxml/content_types_presets.rb
|
217
|
+
- lib/openxml/errors.rb
|
218
|
+
- lib/openxml/package.rb
|
219
|
+
- lib/openxml/part.rb
|
220
|
+
- lib/openxml/parts.rb
|
221
|
+
- lib/openxml/parts/content_types.rb
|
222
|
+
- lib/openxml/parts/rels.rb
|
223
|
+
- lib/openxml/parts/unparsed_part.rb
|
224
|
+
- lib/openxml/rubyzip_fix.rb
|
225
|
+
- lib/openxml/types.rb
|
226
|
+
- openxml-package.gemspec
|
227
|
+
- test/content_types_test.rb
|
228
|
+
- test/package_test.rb
|
229
|
+
- test/support/sample.docx
|
230
|
+
- test/test_helper.rb
|
231
|
+
homepage: https://github.com/openxml/openxml-package
|
232
|
+
licenses:
|
233
|
+
- MIT
|
234
|
+
metadata: {}
|
235
|
+
post_install_message:
|
236
|
+
rdoc_options: []
|
237
|
+
require_paths:
|
238
|
+
- lib
|
239
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
240
|
+
requirements:
|
241
|
+
- - ">="
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: '0'
|
244
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
245
|
+
requirements:
|
246
|
+
- - ">="
|
247
|
+
- !ruby/object:Gem::Version
|
248
|
+
version: '0'
|
249
|
+
requirements: []
|
250
|
+
rubyforge_project:
|
251
|
+
rubygems_version: 2.4.8
|
252
|
+
signing_key:
|
253
|
+
specification_version: 4
|
254
|
+
summary: A Ruby implementation of OpenXmlPackage
|
255
|
+
test_files:
|
256
|
+
- test/content_types_test.rb
|
257
|
+
- test/package_test.rb
|
258
|
+
- test/support/sample.docx
|
259
|
+
- test/test_helper.rb
|