bb-epub 0.0.1
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 +14 -0
- data/Rakefile +12 -0
- data/lib/bb-epub/package.rb +64 -0
- data/lib/bb-epub/transform/audio_overlay.rb +227 -0
- data/lib/bb-epub/transform/audio_soundtrack.rb +73 -0
- data/lib/bb-epub/transform/contributor.rb +11 -0
- data/lib/bb-epub/transform/cover_image.rb +123 -0
- data/lib/bb-epub/transform/cover_page.rb +158 -0
- data/lib/bb-epub/transform/creator.rb +67 -0
- data/lib/bb-epub/transform/description.rb +43 -0
- data/lib/bb-epub/transform/language.rb +29 -0
- data/lib/bb-epub/transform/metadata.rb +143 -0
- data/lib/bb-epub/transform/nav.rb +60 -0
- data/lib/bb-epub/transform/nav_toc.rb +177 -0
- data/lib/bb-epub/transform/ncx.rb +63 -0
- data/lib/bb-epub/transform/ocf.rb +33 -0
- data/lib/bb-epub/transform/opf.rb +22 -0
- data/lib/bb-epub/transform/package_identifier.rb +87 -0
- data/lib/bb-epub/transform/rendition.rb +273 -0
- data/lib/bb-epub/transform/resources.rb +38 -0
- data/lib/bb-epub/transform/spine.rb +79 -0
- data/lib/bb-epub/transform/title.rb +92 -0
- data/lib/bb-epub/transform/version.rb +39 -0
- data/lib/bb-epub/version.rb +5 -0
- data/lib/bb-epub.rb +10 -0
- metadata +92 -0
@@ -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 BbEPUB::Transform::PackageIdentifier < Bookbinder::Transform
|
18
|
+
|
19
|
+
def dependencies
|
20
|
+
[BbEPUB::Transform::Metadata, BbEPUB::Transform::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,273 @@
|
|
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 BbEPUB::Transform::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
|
+
[BbEPUB::Transform::Spine, BbEPUB::Transform::Metadata]
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def to_map(package)
|
35
|
+
package.map['rendition-format'] = 'ebook'
|
36
|
+
book_properties = book_properties_from_apple_display_options(package)
|
37
|
+
book_properties.update(book_properties_from_opf(package))
|
38
|
+
component_rendition_properties_from_opf(package, book_properties)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def from_map(package)
|
43
|
+
first_cmpt = package.map['spine'][0]
|
44
|
+
book_properties = {
|
45
|
+
'rendition:flow' => first_cmpt['rendition-flow'],
|
46
|
+
'rendition:layout' => first_cmpt['rendition-layout'],
|
47
|
+
'rendition:spread' => first_cmpt['rendition-spread'],
|
48
|
+
'rendition:orientation' => first_cmpt['rendition-orientation'],
|
49
|
+
'rendition:viewport' => first_cmpt['rendition-viewport']
|
50
|
+
}
|
51
|
+
opf_doc = package.file(:opf).document
|
52
|
+
opf_doc.add_prefix('rendition')
|
53
|
+
book_properties.each_pair { |prop, content|
|
54
|
+
if content && RENDITION_DEFAULT_PROPERTIES[prop] != content
|
55
|
+
book_meta_tag(opf_doc, prop, content)
|
56
|
+
end
|
57
|
+
}
|
58
|
+
opf_doc.each('opf|spine > opf|itemref') { |itemref|
|
59
|
+
# Update the itemref properties attribute.
|
60
|
+
cmpt = component_for_itemref(package, itemref)
|
61
|
+
props = (itemref['properties'] || '').split
|
62
|
+
book_properties.each_pair { |prop, book_content|
|
63
|
+
key = prop.gsub(':', '-')
|
64
|
+
if cmpt[key] != book_content
|
65
|
+
cmpt_content = cmpt[key] || RENDITION_DEFAULT_PROPERTIES[prop]
|
66
|
+
props.push("#{prop}-#{cmpt_content}")
|
67
|
+
end
|
68
|
+
}
|
69
|
+
if ['left', 'right'].include?(cmpt['rendition-position'])
|
70
|
+
props.push('page-spread-'+cmpt['rendition-position'])
|
71
|
+
elsif cmpt['rendition-position'] == 'center'
|
72
|
+
props.push('rendition:page-spread-center')
|
73
|
+
end
|
74
|
+
if cmpt['rendition-align-x-center']
|
75
|
+
props.push('rendition:align-x-center')
|
76
|
+
end
|
77
|
+
itemref['properties'] = props.join(' ') if props.any?
|
78
|
+
|
79
|
+
# Update the viewport/viewBox attribute in the component file.
|
80
|
+
add_icb_to_component(
|
81
|
+
package.file(cmpt['path']),
|
82
|
+
cmpt['rendition-viewport']
|
83
|
+
) if cmpt['rendition-viewport']
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
# Inspects Apple's display-options file in META-INF for some
|
91
|
+
# option values that we can convert to EPUB3 equivalents.
|
92
|
+
# Returns a hash with zero or more rendition:* properties.
|
93
|
+
#
|
94
|
+
def book_properties_from_apple_display_options(package)
|
95
|
+
{}.tap { |book_properties|
|
96
|
+
package.if_file(APPLE_FXL_PATH, false) { |apple_file|
|
97
|
+
apple_options = apple_file.document('r').search(
|
98
|
+
'display_options > platform[name="*"] > option'
|
99
|
+
)
|
100
|
+
apple_hash = apple_options.inject({}) { |acc, apple_option|
|
101
|
+
acc.update(apple_option['name'] => apple_option.content.strip)
|
102
|
+
}
|
103
|
+
|
104
|
+
if apple_hash['fixed-layout'] == 'true'
|
105
|
+
book_properties['rendition-layout'] = 'pre-paginated'
|
106
|
+
end
|
107
|
+
|
108
|
+
if apple_hash['orientation-lock'] == 'landscape-only'
|
109
|
+
book_properties['rendition-orientation'] = 'landscape'
|
110
|
+
elsif apple_hash['orientation-lock'] == 'portrait-only'
|
111
|
+
book_properties['rendition-orientation'] = 'portrait'
|
112
|
+
end
|
113
|
+
|
114
|
+
# FIXME: not really a 1-to-1 mapping - should we ignore this option?
|
115
|
+
# UPDATE: yes, disabled for now.
|
116
|
+
# if apple_hash['open-to-spread'] == 'true'
|
117
|
+
# book_properties['rendition-spread'] = 'both'
|
118
|
+
# end
|
119
|
+
}
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
# Looks for book rendition properties in the OPF <metadata>
|
125
|
+
# tag, returning a hash with zero or more such properties.
|
126
|
+
#
|
127
|
+
# Book-level rendition properties are:
|
128
|
+
#
|
129
|
+
# <meta property="rendition:layout">reflowable</meta>
|
130
|
+
# <meta property="rendition:spread">auto</meta>
|
131
|
+
# <meta property="rendition:orientation">landscape</meta>
|
132
|
+
#
|
133
|
+
def book_properties_from_opf(package)
|
134
|
+
{}.tap { |book_properties|
|
135
|
+
spent = []
|
136
|
+
package.map['metadata'].each_pair { |key, value|
|
137
|
+
if RENDITION_DEFAULT_PROPERTIES.keys.include?(key)
|
138
|
+
book_properties.update(key.sub(':', '-') => value.first['@'])
|
139
|
+
spent.push(key)
|
140
|
+
end
|
141
|
+
}
|
142
|
+
spent.each { |key| package.map['metadata'].delete(key) }
|
143
|
+
if vpt_str = book_properties['rendition-viewport']
|
144
|
+
if vpt = parse_viewport_string(vpt_str)
|
145
|
+
book_properties['rendition-viewport'] = vpt
|
146
|
+
else
|
147
|
+
package.warn("Viewport string unparseable: #{vpt_str}")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
# Iterates over each spine component looking for component-specific
|
155
|
+
# rendition properties, which are merged over the top of the default
|
156
|
+
# book-level properties, then applied directly to the map's component.
|
157
|
+
#
|
158
|
+
def component_rendition_properties_from_opf(package, book_properties)
|
159
|
+
opf_doc = package.file(:opf).document('r')
|
160
|
+
opf_doc.each('opf|spine opf|itemref') { |itemref|
|
161
|
+
cmpt = component_for_itemref(package, itemref)
|
162
|
+
cmpt_properties = book_properties.clone
|
163
|
+
(itemref['properties'] || '').split.each { |itemref_prop|
|
164
|
+
if match = itemref_prop.match(/^rendition:flow-(.+)/)
|
165
|
+
cmpt_properties['rendition-flow'] = match[1]
|
166
|
+
end
|
167
|
+
if match = itemref_prop.match(/^rendition:layout-(.+)/)
|
168
|
+
cmpt_properties['rendition-layout'] = match[1]
|
169
|
+
end
|
170
|
+
if match = itemref_prop.match(/^rendition:spread-(.+)/)
|
171
|
+
cmpt_properties['rendition-spread'] = match[1]
|
172
|
+
end
|
173
|
+
if match = itemref_prop.match(/^rendition:orientation-(.+)/)
|
174
|
+
cmpt_properties['rendition-spread'] = match[1]
|
175
|
+
end
|
176
|
+
if match = itemref_prop.match(/^(rendition:)?page-spread-(\w+)/)
|
177
|
+
cmpt_properties['rendition-position'] = match[2]
|
178
|
+
end
|
179
|
+
if match = itemref_prop.match(/^rendition:align-x-center$/)
|
180
|
+
cmpt_properties['rendition-align-x-center'] = true
|
181
|
+
end
|
182
|
+
}
|
183
|
+
|
184
|
+
# The rendition:viewport should ONLY be set on the component if
|
185
|
+
# it is pre-paginated, and we should prefer the values in the
|
186
|
+
# HTML file itself over anything we find in the OPF.
|
187
|
+
book_vpt = cmpt_properties.delete('rendition-viewport')
|
188
|
+
if cmpt_properties['rendition-layout'] == 'pre-paginated'
|
189
|
+
cmpt_vpt = retrieve_icb_from_component(package.file(cmpt['path']))
|
190
|
+
if cmpt_vpt || book_vpt
|
191
|
+
cmpt_properties['rendition-viewport'] = cmpt_vpt || book_vpt
|
192
|
+
else
|
193
|
+
package.warn("Pre-paginated: viewport not found: #{cmpt['path']}")
|
194
|
+
puts("Expected viewport, found nothing")
|
195
|
+
cmpt_properties.delete('rendition-layout')
|
196
|
+
end
|
197
|
+
end
|
198
|
+
cmpt.update(cmpt_properties)
|
199
|
+
}
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
# Given an itemref tag, finds the map's component hash that
|
204
|
+
# corresponds to it. (Look-up is performed using href, via
|
205
|
+
# the manifest item tag.)
|
206
|
+
#
|
207
|
+
def component_for_itemref(package, itemref)
|
208
|
+
package.map['spine'].detect { |cmpt|
|
209
|
+
cmpt['id'] == itemref['idref']
|
210
|
+
}
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
# Finds the "initial containing block" (in EPUB3 parlance) for
|
215
|
+
# the given component document. This is either the SVG viewBox
|
216
|
+
# attribute, or the HTML <meta name="viewport"> content attribute.
|
217
|
+
#
|
218
|
+
def retrieve_icb_from_component(cmpt_file)
|
219
|
+
cmpt_doc = cmpt_file.document('r')
|
220
|
+
if cmpt_file.media_type.match(/svg/)
|
221
|
+
icb = cmpt_doc.root['viewBox'].split
|
222
|
+
{ 'width' => icb[0], 'height' => icb[1] }
|
223
|
+
elsif viewport_meta = cmpt_doc.find('meta[name="viewport"]')
|
224
|
+
parse_viewport_string(viewport_meta['content'])
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
|
229
|
+
def parse_viewport_string(icb)
|
230
|
+
{
|
231
|
+
'width' => icb.match(/width\s*=\s*(\d+)/)[1].to_i,
|
232
|
+
'height' => icb.match(/height\s*=\s*(\d+)/)[1].to_i
|
233
|
+
}
|
234
|
+
rescue
|
235
|
+
# package.warn("Viewport string unparseable: #{icb}")
|
236
|
+
nil
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
# Creates a book-level meta tag for the given rendition:* property,
|
241
|
+
# inserting it into the OPF document.
|
242
|
+
#
|
243
|
+
def book_meta_tag(opf_doc, property, content)
|
244
|
+
opf_doc.new_node('meta', :append => 'opf|metadata') { |meta_tag|
|
245
|
+
meta_tag['property'] = property
|
246
|
+
meta_tag.content = content
|
247
|
+
}
|
248
|
+
end
|
249
|
+
|
250
|
+
|
251
|
+
# Updates the viewBox or meta-viewport declaration in the
|
252
|
+
# component document (SVG or HTML respectively), creating
|
253
|
+
# tags/attributes as necessary.
|
254
|
+
#
|
255
|
+
def add_icb_to_component(cmpt_file, icb)
|
256
|
+
cmpt_doc = cmpt_file.document
|
257
|
+
if cmpt_file.media_type.match(/svg/)
|
258
|
+
cmpt_doc.root['viewBox'] = icb.join(' ')
|
259
|
+
elsif viewport_meta = cmpt_doc.find('meta[name="viewport"]')
|
260
|
+
vpc = viewport_meta['content']
|
261
|
+
vpc.gsub!(/width\s*=\s*\d+/, "width=#{icb['width']}")
|
262
|
+
vpc.gsub!(/height\s*=\s*\d+/, "height=#{icb['height']}")
|
263
|
+
viewport_meta['content'] = vpc
|
264
|
+
else
|
265
|
+
cmpt_doc.new_node('meta', :append => 'head') { |viewport_meta_tag|
|
266
|
+
viewport_meta_tag['name'] = 'viewport'
|
267
|
+
viewport_meta_tag['content'] =
|
268
|
+
"width=#{icb['width']},height=#{icb['height']}"
|
269
|
+
}
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class BbEPUB::Transform::Resources < Bookbinder::Transform
|
2
|
+
|
3
|
+
def dependencies
|
4
|
+
[BbEPUB::Transform::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
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class BbEPUB::Transform::Spine < Bookbinder::Transform
|
2
|
+
|
3
|
+
def dependencies
|
4
|
+
[BbEPUB::Transform::Resources]
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
def to_map(package)
|
9
|
+
opf_doc = package.file(:opf).document('r')
|
10
|
+
itemrefs = opf_doc.search('opf|spine > opf|itemref')
|
11
|
+
package.map['spine'] = itemrefs.collect { |itemref|
|
12
|
+
cmpt = package.map['resources'].detect { |r| r['id'] == itemref['idref'] }
|
13
|
+
if cmpt
|
14
|
+
package.map['resources'].delete(cmpt)
|
15
|
+
cmpt['linear'] = itemref['linear'] == 'no' ? false : true
|
16
|
+
cmpt
|
17
|
+
else
|
18
|
+
package.warn("No manifest item for spine idref: #{itemref['idref']}")
|
19
|
+
end
|
20
|
+
}.compact
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def from_map(package)
|
25
|
+
opf_doc = package.file(:opf).document
|
26
|
+
|
27
|
+
package.map['spine'].each { |cmpt|
|
28
|
+
# Convert component to valid XHTML
|
29
|
+
cmpt_file = package.file(cmpt['path'])
|
30
|
+
cmpt_doc = cmpt_file.document
|
31
|
+
# Add the EPUB namespace:
|
32
|
+
# FIXME: only add the EPUB namespace if there are any epub:* attrs?
|
33
|
+
cmpt_doc.add_namespace('epub')
|
34
|
+
|
35
|
+
# Update the OPF manifest
|
36
|
+
opf_doc.new_node('item', :append => 'opf|manifest') { |manifest_item_tag|
|
37
|
+
manifest_item_tag['href'] = package.make_href(cmpt['path'])
|
38
|
+
manifest_item_tag['id'] = cmpt['id']
|
39
|
+
manifest_item_tag['media-type'] = cmpt['media-type']
|
40
|
+
props = component_properties(cmpt, cmpt_doc)
|
41
|
+
manifest_item_tag['properties'] = props.join(' ') if props.any?
|
42
|
+
}
|
43
|
+
|
44
|
+
# Update the OPF spine
|
45
|
+
opf_doc.new_node('itemref', :append => 'opf|spine') { |spine_item_tag|
|
46
|
+
spine_item_tag['idref'] = cmpt['id']
|
47
|
+
spine_item_tag['linear'] = 'no' unless cmpt['linear']
|
48
|
+
}
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
# Assemble the component properties by inspecting the HTML...
|
56
|
+
# because some reading systems can't do this themselves, I guess.
|
57
|
+
#
|
58
|
+
# NB: the 'cover-image' and 'nav' properties will be set in
|
59
|
+
# other tranforms.
|
60
|
+
#
|
61
|
+
def component_properties(cmpt, cmpt_doc)
|
62
|
+
[].tap { |props|
|
63
|
+
# 'scripted': check whether there are any script tags in the component.
|
64
|
+
props.push('scripted') if cmpt_doc.find('script')
|
65
|
+
|
66
|
+
# 'mathml': look for a math element
|
67
|
+
props.push('mathml') if cmpt_doc.find('math, mathml:math')
|
68
|
+
|
69
|
+
# 'remote-resources': TODO
|
70
|
+
|
71
|
+
# 'svg': look for an svg element
|
72
|
+
props.push('svg') if cmpt_doc.find('svg, svg:svg')
|
73
|
+
|
74
|
+
# 'switch': look for an epub:switch element
|
75
|
+
props.push('switch') if cmpt_doc.find('epub|switch')
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# EPUB2 spec:
|
2
|
+
#
|
3
|
+
# http://www.idpf.org/epub/20/spec/OPF_2.0.1_draft.htm#Section2.2.1
|
4
|
+
#
|
5
|
+
# EPUB3 spec:
|
6
|
+
#
|
7
|
+
# http://www.idpf.org/epub/30/spec/epub30-publications.html#sec-opf-dctitle
|
8
|
+
#
|
9
|
+
# On title types:
|
10
|
+
#
|
11
|
+
# "When the title-type value is drawn from a code list or other formal
|
12
|
+
# enumeration, the scheme attribute should be attached to identify its source.
|
13
|
+
# When a scheme is not specified, Reading Systems should recognize the
|
14
|
+
# following title type values: main, subtitle, short, collection, edition
|
15
|
+
# and expanded."
|
16
|
+
#
|
17
|
+
class BbEPUB::Transform::Title < Bookbinder::Transform
|
18
|
+
|
19
|
+
TITLE_TYPES = %w[main subtitle short collection edition expanded]
|
20
|
+
|
21
|
+
|
22
|
+
def dependencies
|
23
|
+
[BbEPUB::Transform::Metadata, BbEPUB::Transform::NCX]
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def to_map(package)
|
28
|
+
package.map['title'] = titles = {}
|
29
|
+
return unless package.map['metadata']
|
30
|
+
return unless metadata_array = package.map['metadata']['title']
|
31
|
+
title_data = metadata_array.sort { |a, b|
|
32
|
+
if a['display-seq'] && b['display-seq']
|
33
|
+
a['display-seq']['@'].to_i <=> b['display-seq']['@'].to_i
|
34
|
+
else
|
35
|
+
0
|
36
|
+
end
|
37
|
+
}
|
38
|
+
title_data.each { |tdata|
|
39
|
+
t = tdata['@']
|
40
|
+
type = tdata['title-type'] ? tdata['title-type']['@'] : 'main'
|
41
|
+
next unless TITLE_TYPES.include?(type)
|
42
|
+
if titles[type]
|
43
|
+
package.warn("Existing title for '#{type}' - discarding '#{t}'")
|
44
|
+
else
|
45
|
+
titles[type] = t
|
46
|
+
metadata_array.delete(tdata)
|
47
|
+
end
|
48
|
+
}
|
49
|
+
# Now that we have "used" the raw metadata for titles, remove it.
|
50
|
+
package.map['metadata'].delete('title') if metadata_array.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def from_map(package)
|
55
|
+
titles = package.map['title']
|
56
|
+
opf_doc = package.file(:opf).document
|
57
|
+
metadata_tag = opf_doc.find('opf|metadata')
|
58
|
+
seq = 0
|
59
|
+
titles.each_pair { |type, title|
|
60
|
+
title_tag = opf_doc.new_node('dc:title', :append => metadata_tag)
|
61
|
+
title_tag.content = title
|
62
|
+
if titles.length > 1
|
63
|
+
seq += 1
|
64
|
+
title_id = "dc-title-metadata-#{seq}"
|
65
|
+
title_tag['id'] = title_id
|
66
|
+
opf_doc.new_node('meta', :append => metadata_tag) { |type_meta_tag|
|
67
|
+
type_meta_tag.content = type
|
68
|
+
type_meta_tag['property'] = 'title-type'
|
69
|
+
type_meta_tag['refines'] = '#'+title_id
|
70
|
+
}
|
71
|
+
opf_doc.new_node('meta', :append => metadata_tag) { |seq_meta_tag|
|
72
|
+
seq_meta_tag.content = seq
|
73
|
+
seq_meta_tag['property'] = 'display-seq'
|
74
|
+
seq_meta_tag['refines'] = '#'+title_id
|
75
|
+
}
|
76
|
+
end
|
77
|
+
}
|
78
|
+
|
79
|
+
# Add it to the NCX if that file exists in the package
|
80
|
+
package.if_file(:ncx) { |ncx_file|
|
81
|
+
ncx_title = [titles['main'], titles['subtitle']].compact.join(': ')
|
82
|
+
ncx_doc = ncx_file.document
|
83
|
+
ncx_doc.new_node('docTitle') { |doc_title_tag|
|
84
|
+
ncx_doc.new_node('text', :append => doc_title_tag) { |text_tag|
|
85
|
+
text_tag.content = ncx_title
|
86
|
+
}
|
87
|
+
ncx_doc.find('ncx|head').add_next_sibling(doc_title_tag)
|
88
|
+
}
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# See https://itunesconnect.apple.com/docs/iBooksAssetGuide5.1Revision2.pdf
|
2
|
+
# pages 22 and 23.
|
3
|
+
#
|
4
|
+
# "The version of your book is specified within a `meta` element
|
5
|
+
# in the Package Document. The `meta` element has a property value
|
6
|
+
# of `ibooks:version':
|
7
|
+
#
|
8
|
+
# <meta property="ibooks:version">1.1.2</meta>
|
9
|
+
#
|
10
|
+
class BbEPUB::Transform::Version < Bookbinder::Transform
|
11
|
+
|
12
|
+
def dependencies
|
13
|
+
[BbEPUB::Transform::Metadata]
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def to_map(package)
|
18
|
+
ver_hashes = package.map['metadata'].delete('ibooks:version')
|
19
|
+
if ver_hashes && ver_hashes.any?
|
20
|
+
package.map['version'] = ver_hashes.first['@']
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def from_map(package)
|
26
|
+
if package.map['version']
|
27
|
+
# Add the ibooks prefix to the package root.
|
28
|
+
opf_doc = package.file(:opf).document
|
29
|
+
opf_doc.add_prefix('ibooks')
|
30
|
+
|
31
|
+
# Create the meta node and append it to <metadata>
|
32
|
+
opf_doc.new_node('meta', :append => 'opf|metadata') { |ver_tag|
|
33
|
+
ver_tag['property'] = 'ibooks:version'
|
34
|
+
ver_tag.content = package.map['version']
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|