bookbinder 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.
- data/README.md +97 -0
- data/Rakefile +12 -0
- data/bin/bookbinder +17 -0
- data/lib/bookbinder/document_proxy.rb +171 -0
- data/lib/bookbinder/file.rb +149 -0
- data/lib/bookbinder/file_system/directory.rb +62 -0
- data/lib/bookbinder/file_system/memory.rb +57 -0
- data/lib/bookbinder/file_system/zip_file.rb +106 -0
- data/lib/bookbinder/file_system.rb +35 -0
- data/lib/bookbinder/media_type.rb +17 -0
- data/lib/bookbinder/operations.rb +59 -0
- data/lib/bookbinder/package/epub.rb +69 -0
- data/lib/bookbinder/package/openbook.rb +33 -0
- data/lib/bookbinder/package.rb +295 -0
- data/lib/bookbinder/transform/epub/audio_overlay.rb +227 -0
- data/lib/bookbinder/transform/epub/audio_soundtrack.rb +73 -0
- data/lib/bookbinder/transform/epub/contributor.rb +11 -0
- data/lib/bookbinder/transform/epub/cover_image.rb +80 -0
- data/lib/bookbinder/transform/epub/cover_page.rb +148 -0
- data/lib/bookbinder/transform/epub/creator.rb +67 -0
- data/lib/bookbinder/transform/epub/description.rb +43 -0
- data/lib/bookbinder/transform/epub/language.rb +29 -0
- data/lib/bookbinder/transform/epub/metadata.rb +140 -0
- data/lib/bookbinder/transform/epub/nav.rb +60 -0
- data/lib/bookbinder/transform/epub/nav_toc.rb +177 -0
- data/lib/bookbinder/transform/epub/ncx.rb +63 -0
- data/lib/bookbinder/transform/epub/ocf.rb +33 -0
- data/lib/bookbinder/transform/epub/opf.rb +22 -0
- data/lib/bookbinder/transform/epub/package_identifier.rb +87 -0
- data/lib/bookbinder/transform/epub/rendition.rb +265 -0
- data/lib/bookbinder/transform/epub/resources.rb +38 -0
- data/lib/bookbinder/transform/epub/spine.rb +79 -0
- data/lib/bookbinder/transform/epub/title.rb +92 -0
- data/lib/bookbinder/transform/epub/version.rb +39 -0
- data/lib/bookbinder/transform/generator.rb +8 -0
- data/lib/bookbinder/transform/openbook/json.rb +15 -0
- data/lib/bookbinder/transform/organizer.rb +41 -0
- data/lib/bookbinder/transform.rb +7 -0
- data/lib/bookbinder/version.rb +5 -0
- data/lib/bookbinder.rb +29 -0
- metadata +131 -0
@@ -0,0 +1,177 @@
|
|
1
|
+
class Bookbinder::Transform::EPUB_NavToc < Bookbinder::Transform
|
2
|
+
|
3
|
+
def dependencies
|
4
|
+
[Bookbinder::Transform::EPUB_Nav, Bookbinder::Transform::EPUB_NCX]
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
def to_map(package)
|
9
|
+
toc = []
|
10
|
+
package.if_file(:nav) { |nav_file|
|
11
|
+
nav_doc = nav_file.document('r')
|
12
|
+
base_path = content_rooted_path(package.content_root, nav_file.path)
|
13
|
+
nav_doc.each('nav[epub|type="toc"] > ol > li') { |li|
|
14
|
+
toc.push(extract_nav_chapter(nav_doc, li, base_path))
|
15
|
+
}
|
16
|
+
}
|
17
|
+
unless toc.any?
|
18
|
+
package.if_file(:ncx) { |ncx_file|
|
19
|
+
ncx_doc = ncx_file.document
|
20
|
+
base_path = content_rooted_path(package.content_root, ncx_file.path)
|
21
|
+
ncx_doc.each('ncx|navMap > ncx|navPoint') { |point|
|
22
|
+
toc.push(extract_ncx_chapter(ncx_doc, point, base_path))
|
23
|
+
}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
if toc.any?
|
27
|
+
package.map['nav'] ||= {}
|
28
|
+
package.map['nav']['toc'] = toc
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def from_map(package)
|
34
|
+
package.if_file(:ncx) { |ncx_file|
|
35
|
+
@max_depth = 0
|
36
|
+
@chapter_count = 0
|
37
|
+
toc = (package.map['nav'] || {})['toc'] || []
|
38
|
+
build_ncx_map(ncx_file.document, toc)
|
39
|
+
build_ncx_depth_meta(ncx_file.document, @max_depth)
|
40
|
+
}
|
41
|
+
package.if_file(:nav) { |nav_file|
|
42
|
+
toc = (package.map['nav'] || {})['toc'] || []
|
43
|
+
build_nav_tag(nav_file.document, toc)
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
# NAV to_map
|
51
|
+
|
52
|
+
def extract_nav_chapter(doc, li, base_path)
|
53
|
+
{}.tap { |chp|
|
54
|
+
content_node = doc.find_within(li, '> a, span')
|
55
|
+
chp['title'] = content_node['title'] || content_node.content.strip
|
56
|
+
if content_node.node_name.downcase == 'a'
|
57
|
+
chp['path'] = resolved_path(base_path, content_node['href'])
|
58
|
+
end
|
59
|
+
children = doc.search_within(li, '> li')
|
60
|
+
if children.any?
|
61
|
+
chp['contents'] = children.collect { |child|
|
62
|
+
extract_nav_chapter(doc, child, base_path)
|
63
|
+
}
|
64
|
+
end
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
# NCX to_map
|
70
|
+
|
71
|
+
def extract_ncx_chapter(doc, point, base_path)
|
72
|
+
{
|
73
|
+
'title' => doc.find_within(point, 'text').text,
|
74
|
+
'path' => resolved_path(
|
75
|
+
base_path,
|
76
|
+
doc.find_within(point, 'content')['src']
|
77
|
+
)
|
78
|
+
}.tap { |out|
|
79
|
+
children = doc.search_within(point, '> navPoint')
|
80
|
+
if children.any?
|
81
|
+
out['contents'] = children.collect { |pt|
|
82
|
+
extract_ncx_chapter(doc, pt, base_path)
|
83
|
+
}
|
84
|
+
end
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# NAV from_map
|
90
|
+
|
91
|
+
def build_nav_tag(nav_doc, toc)
|
92
|
+
nav_tag_frag = Nokogiri::XML::DocumentFragment.parse('')
|
93
|
+
Nokogiri::XML::Builder.with(nav_tag_frag) { |x|
|
94
|
+
x.nav('epub:type' => 'toc') { build_nav_ol_for_chapters(x, toc) }
|
95
|
+
}
|
96
|
+
nav_doc.find('body').add_child(nav_tag_frag)
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def build_nav_ol_for_chapters(x, chapters)
|
101
|
+
x.ol {
|
102
|
+
chapters.each { |chp|
|
103
|
+
x.li {
|
104
|
+
atts = { 'href' => chp['path'] }
|
105
|
+
x.a(chp['title'], atts)
|
106
|
+
build_nav_ol_for_chapters(x, chp['contents']) if chp['contents']
|
107
|
+
}
|
108
|
+
}
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
# NCX from_map
|
114
|
+
|
115
|
+
def build_ncx_map(ncx_doc, toc)
|
116
|
+
nav_map_frag = Nokogiri::XML::DocumentFragment.parse('')
|
117
|
+
Nokogiri::XML::Builder.with(nav_map_frag) { |x|
|
118
|
+
x.navMap {
|
119
|
+
toc.each { |chp| build_ncx_point_for_chapter(x, chp, 0) }
|
120
|
+
}
|
121
|
+
}
|
122
|
+
ncx_doc.root.add_child(nav_map_frag)
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def build_ncx_depth_meta(ncx_doc, max_depth)
|
127
|
+
ncx_doc.new_node('meta', :append => 'ncx|head') { |depth_meta_tag|
|
128
|
+
depth_meta_tag['name'] = 'dtb:depth'
|
129
|
+
depth_meta_tag['content'] = max_depth
|
130
|
+
}
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
def build_ncx_point_for_chapter(x, chp, depth)
|
135
|
+
@chapter_count += 1
|
136
|
+
depth += 1
|
137
|
+
@max_depth = depth if depth > @max_depth
|
138
|
+
x.navPoint(
|
139
|
+
'id' => "toc-#{@chapter_count}",
|
140
|
+
'class' => 'chapter',
|
141
|
+
'playOrder' => @chapter_count
|
142
|
+
) {
|
143
|
+
x.navLabel { x.text_(chp['title']) }
|
144
|
+
x.content('src' => chp['path'])
|
145
|
+
if chp['children']
|
146
|
+
chp['children'].each { |chd|
|
147
|
+
build_ncx_point_for_chapter(x, chd, depth)
|
148
|
+
}
|
149
|
+
end
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# Utilities
|
155
|
+
def content_rooted_path(content_root, path)
|
156
|
+
File.dirname(path).sub(/^#{content_root}\/?/, '').sub(/^\.$/, '')
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
def resolved_path(base_path, path)
|
161
|
+
relative_path = File.join(base_path, path).sub(/^\//, '')
|
162
|
+
relative_parts = relative_path.split('/')
|
163
|
+
resolved_parts = []
|
164
|
+
while relative_parts.any?
|
165
|
+
part = relative_parts.shift
|
166
|
+
if part == '.'
|
167
|
+
# do nothing
|
168
|
+
elsif part == '..'
|
169
|
+
resolved_parts.pop
|
170
|
+
else
|
171
|
+
resolved_parts.push(part)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
File.join(resolved_parts)
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class Bookbinder::Transform::EPUB_NCX < Bookbinder::Transform
|
2
|
+
|
3
|
+
DEFAULT_FILE_NAME = 'book.ncx'
|
4
|
+
|
5
|
+
|
6
|
+
def dependencies
|
7
|
+
[Bookbinder::Transform::EPUB_Resources]
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def to_map(package)
|
12
|
+
opf_doc = package.file(:opf).document('r')
|
13
|
+
spine = opf_doc.find('opf|spine')
|
14
|
+
if spine && ncx_id = spine['toc']
|
15
|
+
ncx_item = opf_doc.find('opf|manifest > opf|item[id="'+ncx_id+'"]')
|
16
|
+
ncx_path = package.make_path(ncx_item['href'])
|
17
|
+
package.file_aliases[:ncx] = ncx_path
|
18
|
+
package.map['resources'].delete_if { |rsrc|
|
19
|
+
package.file_path(rsrc['path']) == ncx_path
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def from_map(package)
|
26
|
+
if package.options['ncx_file'] != false
|
27
|
+
stub_ncx(package)
|
28
|
+
add_to_opf_manifest(package)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def stub_ncx(package)
|
36
|
+
package.file_aliases[:ncx] = DEFAULT_FILE_NAME
|
37
|
+
package.file(:ncx).new_xml_document { |doc, x|
|
38
|
+
x.ncx('version' => '2005-1', 'xml:lang' => 'en') {
|
39
|
+
doc.add_node_namespace(x.parent, 'ncx', true)
|
40
|
+
x.head {
|
41
|
+
x.meta(
|
42
|
+
'name' => 'dtb:generator',
|
43
|
+
'content' => "Bookbinder #{Bookbinder::VERSION}"
|
44
|
+
)
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def add_to_opf_manifest(package)
|
52
|
+
opf_file = package.file(:opf)
|
53
|
+
ncx_file = package.file(:ncx)
|
54
|
+
ncx_id = package.make_id(DEFAULT_FILE_NAME)
|
55
|
+
opf_file.document.new_node('item', :append => 'opf|manifest') { |item_tag|
|
56
|
+
item_tag['href'] = package.make_href(DEFAULT_FILE_NAME)
|
57
|
+
item_tag['media-type'] = ncx_file.media_type
|
58
|
+
item_tag['id'] = ncx_id
|
59
|
+
}
|
60
|
+
opf_file.document.find('opf|spine')['toc'] = ncx_id
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Bookbinder::Transform::EPUB_OCF < Bookbinder::Transform
|
2
|
+
|
3
|
+
OCF_PATH = 'META-INF/container.xml'
|
4
|
+
|
5
|
+
# No dependencies! Something has to have no dependencies, after all.
|
6
|
+
|
7
|
+
def to_map(package)
|
8
|
+
package.file_aliases[:ocf] = OCF_PATH
|
9
|
+
ocf = package.file(:ocf, false)
|
10
|
+
opf_path = ocf.document('r').find('ocf|rootfile')['full-path']
|
11
|
+
package.file_aliases[:opf] = File.basename(opf_path)
|
12
|
+
package.content_root = File.dirname(opf_path).sub(/^\./, '')
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def from_map(package)
|
17
|
+
package.file_aliases[:ocf] = OCF_PATH
|
18
|
+
package.file_aliases[:opf] = 'book.opf'
|
19
|
+
opf_file = package.file(:opf)
|
20
|
+
package.file(:ocf, false).new_xml_document { |doc, x|
|
21
|
+
x.container('version' => '1.0') {
|
22
|
+
doc.add_node_namespace(x.parent, 'ocf', true)
|
23
|
+
x.rootfiles {
|
24
|
+
x.rootfile(
|
25
|
+
'full-path' => opf_file.path,
|
26
|
+
'media-type' => opf_file.media_type
|
27
|
+
)
|
28
|
+
}
|
29
|
+
}
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Bookbinder::Transform::EPUB_OPF < Bookbinder::Transform
|
2
|
+
|
3
|
+
def dependencies
|
4
|
+
[Bookbinder::Transform::EPUB_OCF]
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
def from_map(package)
|
9
|
+
package.file(:opf).new_xml_document { |opf_doc, x|
|
10
|
+
x.package('version' => '3.0') {
|
11
|
+
opf_doc.add_node_namespace(x.parent, 'opf', true)
|
12
|
+
x.metadata {
|
13
|
+
opf_doc.add_node_namespace(x.parent, 'dc')
|
14
|
+
opf_doc.add_node_namespace(x.parent, 'dcterms')
|
15
|
+
}
|
16
|
+
x.manifest
|
17
|
+
x.spine
|
18
|
+
}
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# See: http://www.idpf.org/epub/30/spec/epub30-publications.html#sec-opf-metadata-identifiers-pid
|
2
|
+
#
|
3
|
+
# "To redress this problem of identifying minor modifications and releases
|
4
|
+
# without changing the Unique Identifier, this specification defines the
|
5
|
+
# semantics for a Package Identifier, or means of distinguishing and
|
6
|
+
# sequentially ordering Publications with the same Unique Identifier. The
|
7
|
+
# Package Identifier is not an actual property in the package metadata section,
|
8
|
+
# but is a value that can be obtained from two required pieces of metadata: the
|
9
|
+
# Unique Identifier and the last modification date of the Publication.
|
10
|
+
#
|
11
|
+
# "When the taken together, the combined value represents a unique identity that
|
12
|
+
# can be used to distinguish any particular version of an EPUB Manifestation
|
13
|
+
# from another. To ensure that a Package Identifier can be constructed, the
|
14
|
+
# Publication must include exactly one `modified` property containing
|
15
|
+
# the last modification date (see meta)."
|
16
|
+
#
|
17
|
+
class Bookbinder::Transform::EPUB_PackageIdentifier < Bookbinder::Transform
|
18
|
+
|
19
|
+
def dependencies
|
20
|
+
[Bookbinder::Transform::EPUB_Metadata, Bookbinder::Transform::EPUB_NCX]
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def to_map(package)
|
25
|
+
opf_doc = package.file(:opf).document('r')
|
26
|
+
|
27
|
+
# Unique identifier
|
28
|
+
unless id_hashes = package.map['metadata']['identifier']
|
29
|
+
package.warn('No identifiers found in package metadata')
|
30
|
+
end
|
31
|
+
if id_hashes && uid_id = opf_doc.root['unique-identifier']
|
32
|
+
uid_hash = id_hashes.detect { |id_hash|
|
33
|
+
id_hash['id'] && id_hash['id']['@'] == uid_id
|
34
|
+
}
|
35
|
+
else
|
36
|
+
package.warn('OPF root "unique-identifier" attribute not found')
|
37
|
+
end
|
38
|
+
if uid_hash
|
39
|
+
id_hashes.delete(uid_hash)
|
40
|
+
package.map['metadata'].delete('identifier') if id_hashes.empty?
|
41
|
+
package.map['unique-identifier'] = uid_hash['@']
|
42
|
+
else
|
43
|
+
package.warn('OPF metadata for required "unique-identifier" not found')
|
44
|
+
end
|
45
|
+
|
46
|
+
# Modified
|
47
|
+
begin
|
48
|
+
mod_hash = package.map['metadata'].delete('dcterms:modified').first
|
49
|
+
timestamp = Time.parse(mod_hash['@'])
|
50
|
+
rescue
|
51
|
+
package.warn('OPF metadata for "modified" not found - using now()')
|
52
|
+
timestamp = Time.now
|
53
|
+
end
|
54
|
+
package.map['modified'] = timestamp.utc.iso8601
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def from_map(package)
|
59
|
+
map_uid = package.map['unique-identifier']
|
60
|
+
map_mod = package.map['modified']
|
61
|
+
opf_doc = package.file(:opf).document
|
62
|
+
metadata_tag = opf_doc.find('opf|metadata')
|
63
|
+
|
64
|
+
# Unique identifier
|
65
|
+
opf_doc.root['unique-identifier'] = 'unique-identifier'
|
66
|
+
opf_doc.new_node('dc:identifier', :append => metadata_tag) { |uid_tag|
|
67
|
+
uid_tag['id'] = 'unique-identifier'
|
68
|
+
uid_tag.content = map_uid
|
69
|
+
}
|
70
|
+
|
71
|
+
# Add the unique identifier to the NCX as well, because too
|
72
|
+
# much data redundancy is never enough.
|
73
|
+
package.if_file(:ncx) { |ncx_file|
|
74
|
+
ncx_file.document.new_node('meta', :append => 'ncx|head') { |uid_meta_tag|
|
75
|
+
uid_meta_tag['name'] = 'dtb:uid'
|
76
|
+
uid_meta_tag['content'] = map_uid
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
# Modified
|
81
|
+
opf_doc.new_node('meta', :append => metadata_tag) { |mod_tag|
|
82
|
+
mod_tag['property'] = 'dcterms:modified'
|
83
|
+
mod_tag.content = map_mod
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
# For the original Apple extension to EPUB 2, see any version of the iBooks
|
2
|
+
# Asset Guide prior to version 5. Here's a link to version 4.7:
|
3
|
+
#
|
4
|
+
# http://www.mobileread.com/forums/attachment.php?attachmentid=74234
|
5
|
+
#
|
6
|
+
# In particular, see the section titled "Configuring Display Options"
|
7
|
+
#
|
8
|
+
# For the EPUB 3 Fixed Layout specification (actually a Mixed Layout
|
9
|
+
# specification), see:
|
10
|
+
#
|
11
|
+
# http://www.idpf.org/epub/301/spec/epub-publications.html#sec-package-metadata-fxl
|
12
|
+
#
|
13
|
+
# NOTE: a potential gotcha. The EPUB3 properties are 'rendition:...',
|
14
|
+
# with a colon, whereas the Openbook form follows the dash convention
|
15
|
+
# for property names, ie 'rendition-...'.
|
16
|
+
#
|
17
|
+
class Bookbinder::Transform::EPUB_Rendition < Bookbinder::Transform
|
18
|
+
|
19
|
+
RENDITION_DEFAULT_PROPERTIES = {
|
20
|
+
'rendition:flow' => 'paginated',
|
21
|
+
'rendition:layout' => 'reflowable',
|
22
|
+
'rendition:spread' => 'auto',
|
23
|
+
'rendition:orientation' => 'auto',
|
24
|
+
'rendition:viewport' => nil
|
25
|
+
}
|
26
|
+
APPLE_FXL_PATH = 'META-INF/com.apple.ibooks.display-options.xml'
|
27
|
+
|
28
|
+
|
29
|
+
def dependencies
|
30
|
+
[Bookbinder::Transform::EPUB_Spine, Bookbinder::Transform::EPUB_Metadata]
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def to_map(package)
|
35
|
+
book_properties = book_properties_from_apple_display_options(package)
|
36
|
+
book_properties.update(book_properties_from_opf(package))
|
37
|
+
component_rendition_properties_from_opf(package, book_properties)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def from_map(package)
|
42
|
+
first_cmpt = package.map['spine'][0]
|
43
|
+
book_properties = {
|
44
|
+
'rendition:flow' => first_cmpt['rendition-flow'],
|
45
|
+
'rendition:layout' => first_cmpt['rendition-layout'],
|
46
|
+
'rendition:spread' => first_cmpt['rendition-spread'],
|
47
|
+
'rendition:orientation' => first_cmpt['rendition-orientation'],
|
48
|
+
'rendition:viewport' => first_cmpt['rendition-viewport']
|
49
|
+
}
|
50
|
+
opf_doc = package.file(:opf).document
|
51
|
+
opf_doc.add_prefix('rendition')
|
52
|
+
book_properties.each_pair { |prop, content|
|
53
|
+
if content && RENDITION_DEFAULT_PROPERTIES[prop] != content
|
54
|
+
book_meta_tag(opf_doc, prop, content)
|
55
|
+
end
|
56
|
+
}
|
57
|
+
opf_doc.each('opf|spine > opf|itemref') { |itemref|
|
58
|
+
# Update the itemref properties attribute.
|
59
|
+
cmpt = component_for_itemref(package, itemref)
|
60
|
+
props = (itemref['properties'] || '').split
|
61
|
+
book_properties.each_pair { |prop, book_content|
|
62
|
+
key = prop.gsub(':', '-')
|
63
|
+
if cmpt[key] != book_content
|
64
|
+
cmpt_content = cmpt[key] || RENDITION_DEFAULT_PROPERTIES[prop]
|
65
|
+
props.push("#{prop}-#{cmpt_content}")
|
66
|
+
end
|
67
|
+
}
|
68
|
+
if ['left', 'right'].include?(cmpt['rendition-position'])
|
69
|
+
props.push('page-spread-'+cmpt['rendition-position'])
|
70
|
+
elsif cmpt['rendition-position'] == 'center'
|
71
|
+
props.push('rendition:page-spread-center')
|
72
|
+
end
|
73
|
+
if cmpt['rendition-align-x-center']
|
74
|
+
props.push('rendition:align-x-center')
|
75
|
+
end
|
76
|
+
itemref['properties'] = props.join(' ') if props.any?
|
77
|
+
|
78
|
+
# Update the viewport/viewBox attribute in the component file.
|
79
|
+
add_icb_to_component(
|
80
|
+
package.file(cmpt['path']),
|
81
|
+
cmpt['rendition-viewport']
|
82
|
+
) if cmpt['rendition-viewport']
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
protected
|
88
|
+
|
89
|
+
# Inspects Apple's display-options file in META-INF for some
|
90
|
+
# option values that we can convert to EPUB3 equivalents.
|
91
|
+
# Returns a hash with zero or more rendition:* properties.
|
92
|
+
#
|
93
|
+
def book_properties_from_apple_display_options(package)
|
94
|
+
{}.tap { |book_properties|
|
95
|
+
package.if_file(APPLE_FXL_PATH, false) { |apple_file|
|
96
|
+
apple_options = apple_file.document('r').search(
|
97
|
+
'display_options > platform[name="*"] > option'
|
98
|
+
)
|
99
|
+
apple_hash = apple_options.inject({}) { |acc, apple_option|
|
100
|
+
acc.update(apple_option['name'] => apple_option.content.strip)
|
101
|
+
}
|
102
|
+
|
103
|
+
if apple_hash['fixed-layout'] == 'true'
|
104
|
+
book_properties['rendition-layout'] = 'pre-paginated'
|
105
|
+
end
|
106
|
+
|
107
|
+
if apple_hash['orientation-lock'] == 'landscape-only'
|
108
|
+
book_properties['rendition-orientation'] = 'landscape'
|
109
|
+
elsif apple_hash['orientation-lock'] == 'portrait-only'
|
110
|
+
book_properties['rendition-orientation'] = 'portrait'
|
111
|
+
end
|
112
|
+
|
113
|
+
# FIXME: not really a 1-to-1 mapping - should we ignore this option?
|
114
|
+
# UPDATE: yes, disabled for now.
|
115
|
+
# if apple_hash['open-to-spread'] == 'true'
|
116
|
+
# book_properties['rendition-spread'] = 'both'
|
117
|
+
# end
|
118
|
+
}
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
# Looks for book rendition properties in the OPF <metadata>
|
124
|
+
# tag, returning a hash with zero or more such properties.
|
125
|
+
#
|
126
|
+
# Book-level rendition properties are:
|
127
|
+
#
|
128
|
+
# <meta property="rendition:layout">reflowable</meta>
|
129
|
+
# <meta property="rendition:spread">auto</meta>
|
130
|
+
# <meta property="rendition:orientation">landscape</meta>
|
131
|
+
#
|
132
|
+
def book_properties_from_opf(package)
|
133
|
+
{}.tap { |book_properties|
|
134
|
+
spent = []
|
135
|
+
package.map['metadata'].each_pair { |key, value|
|
136
|
+
if RENDITION_DEFAULT_PROPERTIES.keys.include?(key)
|
137
|
+
book_properties.update(key.sub(':', '-') => value.first['@'])
|
138
|
+
spent.push(key)
|
139
|
+
end
|
140
|
+
}
|
141
|
+
spent.each { |key| package.map['metadata'].delete(key) }
|
142
|
+
if vpt = book_properties['rendition-viewport']
|
143
|
+
book_properties['rendition-viewport'] = parse_viewport_string(vpt)
|
144
|
+
end
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
|
149
|
+
# Iterates over each spine component looking for component-specific
|
150
|
+
# rendition properties, which are merged over the top of the default
|
151
|
+
# book-level properties, then applied directly to the map's component.
|
152
|
+
#
|
153
|
+
def component_rendition_properties_from_opf(package, book_properties)
|
154
|
+
opf_doc = package.file(:opf).document('r')
|
155
|
+
opf_doc.each('opf|spine opf|itemref') { |itemref|
|
156
|
+
cmpt = component_for_itemref(package, itemref)
|
157
|
+
cmpt_properties = book_properties.clone
|
158
|
+
(itemref['properties'] || '').split.each { |itemref_prop|
|
159
|
+
if match = itemref_prop.match(/^rendition:flow-(.+)/)
|
160
|
+
cmpt_properties['rendition-flow'] = match[1]
|
161
|
+
end
|
162
|
+
if match = itemref_prop.match(/^rendition:layout-(.+)/)
|
163
|
+
cmpt_properties['rendition-layout'] = match[1]
|
164
|
+
end
|
165
|
+
if match = itemref_prop.match(/^rendition:spread-(.+)/)
|
166
|
+
cmpt_properties['rendition-spread'] = match[1]
|
167
|
+
end
|
168
|
+
if match = itemref_prop.match(/^rendition:orientation-(.+)/)
|
169
|
+
cmpt_properties['rendition-spread'] = match[1]
|
170
|
+
end
|
171
|
+
if match = itemref_prop.match(/^(rendition:)?page-spread-(\w+)/)
|
172
|
+
cmpt_properties['rendition-position'] = match[2]
|
173
|
+
end
|
174
|
+
if match = itemref_prop.match(/^rendition:align-x-center$/)
|
175
|
+
cmpt_properties['rendition-align-x-center'] = true
|
176
|
+
end
|
177
|
+
}
|
178
|
+
|
179
|
+
# The rendition:viewport should ONLY be set on the component if
|
180
|
+
# it is pre-paginated, and we should prefer the values in the
|
181
|
+
# HTML file itself over anything we find in the OPF.
|
182
|
+
book_vpt = cmpt_properties.delete('rendition-viewport')
|
183
|
+
if cmpt_properties['rendition-layout'] == 'pre-paginated'
|
184
|
+
cmpt_vpt = retrieve_icb_from_component(package.file(cmpt['path']))
|
185
|
+
if cmpt_vpt || book_vpt
|
186
|
+
cmpt_properties['rendition-viewport'] = cmpt_vpt || book_vpt
|
187
|
+
else
|
188
|
+
package.warn("Pre-paginated: viewport not found: #{cmpt['path']}")
|
189
|
+
puts("Expected viewport, found nothing")
|
190
|
+
cmpt_properties.delete('rendition-layout')
|
191
|
+
end
|
192
|
+
end
|
193
|
+
cmpt.update(cmpt_properties)
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
# Given an itemref tag, finds the map's component hash that
|
199
|
+
# corresponds to it. (Look-up is performed using href, via
|
200
|
+
# the manifest item tag.)
|
201
|
+
#
|
202
|
+
def component_for_itemref(package, itemref)
|
203
|
+
package.map['spine'].detect { |cmpt|
|
204
|
+
cmpt['id'] == itemref['idref']
|
205
|
+
}
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
# Finds the "initial containing block" (in EPUB3 parlance) for
|
210
|
+
# the given component document. This is either the SVG viewBox
|
211
|
+
# attribute, or the HTML <meta name="viewport"> content attribute.
|
212
|
+
#
|
213
|
+
def retrieve_icb_from_component(cmpt_file)
|
214
|
+
cmpt_doc = cmpt_file.document('r')
|
215
|
+
if cmpt_file.media_type.match(/svg/)
|
216
|
+
icb = cmpt_doc.root['viewBox'].split
|
217
|
+
{ 'width' => icb[0], 'height' => icb[1] }
|
218
|
+
elsif viewport_meta = cmpt_doc.find('meta[name="viewport"]')
|
219
|
+
parse_viewport_string(viewport_meta['content'])
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
def parse_viewport_string(icb)
|
225
|
+
{
|
226
|
+
'width' => icb.match(/width\s*=\s*(\d+)/)[1].to_i,
|
227
|
+
'height' => icb.match(/height\s*=\s*(\d+)/)[1].to_i
|
228
|
+
}
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
# Creates a book-level meta tag for the given rendition:* property,
|
233
|
+
# inserting it into the OPF document.
|
234
|
+
#
|
235
|
+
def book_meta_tag(opf_doc, property, content)
|
236
|
+
opf_doc.new_node('meta', :append => 'opf|metadata') { |meta_tag|
|
237
|
+
meta_tag['property'] = property
|
238
|
+
meta_tag.content = content
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
# Updates the viewBox or meta-viewport declaration in the
|
244
|
+
# component document (SVG or HTML respectively), creating
|
245
|
+
# tags/attributes as necessary.
|
246
|
+
#
|
247
|
+
def add_icb_to_component(cmpt_file, icb)
|
248
|
+
cmpt_doc = cmpt_file.document
|
249
|
+
if cmpt_file.media_type.match(/svg/)
|
250
|
+
cmpt_doc.root['viewBox'] = icb.join(' ')
|
251
|
+
elsif viewport_meta = cmpt_doc.find('meta[name="viewport"]')
|
252
|
+
vpc = viewport_meta['content']
|
253
|
+
vpc.gsub!(/width\s*=\s*\d+/, "width=#{icb['width']}")
|
254
|
+
vpc.gsub!(/height\s*=\s*\d+/, "height=#{icb['height']}")
|
255
|
+
viewport_meta['content'] = vpc
|
256
|
+
else
|
257
|
+
cmpt_doc.new_node('meta', :append => 'head') { |viewport_meta_tag|
|
258
|
+
viewport_meta_tag['name'] = 'viewport'
|
259
|
+
viewport_meta_tag['content'] =
|
260
|
+
"width=#{icb['width']},height=#{icb['height']}"
|
261
|
+
}
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Bookbinder::Transform::EPUB_Resources < Bookbinder::Transform
|
2
|
+
|
3
|
+
def dependencies
|
4
|
+
[Bookbinder::Transform::EPUB_OPF]
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
# Find all the items in the manifest -- except all spine items, the NCX,
|
9
|
+
# and the NAV.
|
10
|
+
#
|
11
|
+
def to_map(package)
|
12
|
+
opf_doc = package.file(:opf).document('r')
|
13
|
+
items = opf_doc.search('opf|manifest > opf|item')
|
14
|
+
package.map['resources'] = items.collect { |item|
|
15
|
+
path = package.make_path(item['href'])
|
16
|
+
{
|
17
|
+
'path' => path,
|
18
|
+
'media-type' => item['media-type'],
|
19
|
+
'id' => item['id'] || package.make_id(path)
|
20
|
+
}
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def from_map(package)
|
26
|
+
return unless package.map['resources']
|
27
|
+
opf_doc = package.file(:opf).document
|
28
|
+
manifest_tag = opf_doc.find('opf|manifest')
|
29
|
+
package.map['resources'].each { |rsrc|
|
30
|
+
opf_doc.new_node('item', :append => manifest_tag) { |manifest_item_tag|
|
31
|
+
manifest_item_tag['href'] = package.make_href(rsrc['path'])
|
32
|
+
manifest_item_tag['media-type'] = rsrc['media-type']
|
33
|
+
manifest_item_tag['id'] = rsrc['id']
|
34
|
+
}
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|