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
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
class BbEPUB::Package < Bookbinder::Package
|
2
|
+
|
3
|
+
require_transforms(File.join(File.dirname(__FILE__), 'transform'))
|
4
|
+
|
5
|
+
DEFAULT_TRANSFORMS = [
|
6
|
+
BbEPUB::Transform::PackageIdentifier,
|
7
|
+
BbEPUB::Transform::Title,
|
8
|
+
BbEPUB::Transform::Creator,
|
9
|
+
BbEPUB::Transform::Contributor,
|
10
|
+
BbEPUB::Transform::Language,
|
11
|
+
BbEPUB::Transform::CoverImage,
|
12
|
+
BbEPUB::Transform::Description,
|
13
|
+
BbEPUB::Transform::Version,
|
14
|
+
BbEPUB::Transform::Spine,
|
15
|
+
BbEPUB::Transform::Resources,
|
16
|
+
BbEPUB::Transform::NavToc,
|
17
|
+
BbEPUB::Transform::CoverPage,
|
18
|
+
BbEPUB::Transform::Rendition,
|
19
|
+
BbEPUB::Transform::AudioOverlay,
|
20
|
+
Bookbinder::Transform::Organizer,
|
21
|
+
Bookbinder::Transform::Generator
|
22
|
+
]
|
23
|
+
|
24
|
+
DEFAULT_CONTENT_ROOT = 'EPUB'
|
25
|
+
|
26
|
+
|
27
|
+
def self.recognize(path)
|
28
|
+
return (
|
29
|
+
File.extname(path).downcase == '.epub' ||
|
30
|
+
File.directory?(File.join(path, 'META-INF'))
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def self.transforms
|
36
|
+
@transforms ||= DEFAULT_TRANSFORMS
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def make_id(path)
|
41
|
+
path.gsub(/[^\w]/, '-')
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def make_path(href)
|
46
|
+
CGI.unescape(href)
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def make_href(path)
|
51
|
+
CGI.escape(path)
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
# Overriding this Package method to inject EPUB's mimetype file.
|
58
|
+
#
|
59
|
+
def write_to_file_system(dest_file_system)
|
60
|
+
dest_file_system.write('mimetype', 'application/epub+zip')
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
# EPUB 3 only. Spec:
|
2
|
+
#
|
3
|
+
# http://www.idpf.org/epub/301/spec/epub-mediaoverlays.html#sec-package-metadata
|
4
|
+
#
|
5
|
+
# Each spine item's manifest item may have a media-overlay attribute, which
|
6
|
+
# is an idref pointing at a SMIL manifest item.
|
7
|
+
#
|
8
|
+
# Other properties to manage:
|
9
|
+
#
|
10
|
+
# <meta property="media:active-class">-epub-media-overlay-active</meta>
|
11
|
+
# <meta property="media:playback-active-class">-epub-media-overlay-playing</meta>
|
12
|
+
# <meta property="media:duration" refines="#ch1_audio">0:32:29</meta>
|
13
|
+
# <meta property="media:duration">1:36:20</meta>
|
14
|
+
# <meta property="media:narrator">Bill Speaker</meta>
|
15
|
+
#
|
16
|
+
# Note: "The Package Document must include the duration of each
|
17
|
+
# Media Overlay as well as of the entire Publication."
|
18
|
+
#
|
19
|
+
class BbEPUB::Transform::AudioOverlay < Bookbinder::Transform
|
20
|
+
|
21
|
+
def dependencies
|
22
|
+
[
|
23
|
+
BbEPUB::Transform::Metadata,
|
24
|
+
BbEPUB::Transform::Resources,
|
25
|
+
BbEPUB::Transform::Spine
|
26
|
+
]
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def to_map(package)
|
31
|
+
meta_to_prop(
|
32
|
+
package,
|
33
|
+
'media:active-class',
|
34
|
+
'audio-overlay-active-class'
|
35
|
+
)
|
36
|
+
meta_to_prop(
|
37
|
+
package,
|
38
|
+
'media:playback-active-class',
|
39
|
+
'audio-overlay-playback-active-class'
|
40
|
+
)
|
41
|
+
book_overlay_duration = meta_to_prop(
|
42
|
+
package,
|
43
|
+
'media:duration',
|
44
|
+
'audio-overlay-duration'
|
45
|
+
) { |hashes|
|
46
|
+
hashes.detect { |hash|
|
47
|
+
unless hash.has_key?('refines')
|
48
|
+
hash['@'] = smil_clock_value_to_seconds(hash['@'])
|
49
|
+
true
|
50
|
+
end
|
51
|
+
}
|
52
|
+
}
|
53
|
+
# TODO: check that book does have a duration if there are any overlays.
|
54
|
+
meta_to_prop(
|
55
|
+
package,
|
56
|
+
'media:narrator',
|
57
|
+
'audio-overlay-narrator'
|
58
|
+
) { |hashes|
|
59
|
+
hashes.detect { |hash| !hash.has_key?('refines') }
|
60
|
+
}
|
61
|
+
# Find each manifest item (SI) with media-overlay attribute:
|
62
|
+
#
|
63
|
+
# - find the map['resources'] item that matches the media-overlay id
|
64
|
+
# - find the map['spine'] item that matches the SI id
|
65
|
+
# - set 'audio-overlay' to the AO path
|
66
|
+
# - set 'audio-overlay-duration' to the refined duration
|
67
|
+
# - set 'audio-overlay-narrator' to the refined narrator
|
68
|
+
#
|
69
|
+
opf_doc = package.file(:opf).document('r')
|
70
|
+
opf_doc.each('opf|manifest > opf|item[media-overlay]') { |cmpt_item|
|
71
|
+
cmpt_id = cmpt_item['id']
|
72
|
+
cmpt = package.map['spine'].detect { |c| c['id'] == cmpt_id }
|
73
|
+
# TODO: nil check cmpt
|
74
|
+
ao_id = cmpt_item['media-overlay']
|
75
|
+
rsrc = package.map['resources'].detect { |r| r['id'] == ao_id }
|
76
|
+
# TODO: nil check rsrc
|
77
|
+
cmpt['audio-overlay'] = rsrc['path']
|
78
|
+
cmpt_overlay_duration = meta_to_prop(
|
79
|
+
package,
|
80
|
+
'media:duration',
|
81
|
+
'audio-overlay-duration',
|
82
|
+
cmpt
|
83
|
+
) { |hashes|
|
84
|
+
hashes.detect { |hash|
|
85
|
+
if hash['refines']['@'] == '#'+ao_id
|
86
|
+
hash['@'] = smil_clock_value_to_seconds(hash['@'])
|
87
|
+
true
|
88
|
+
end
|
89
|
+
}
|
90
|
+
}
|
91
|
+
# TODO: nil check cmpt_overlay_duration
|
92
|
+
meta_to_prop(
|
93
|
+
package,
|
94
|
+
'media:narrator',
|
95
|
+
'audio-overlay-narrator',
|
96
|
+
cmpt
|
97
|
+
) { |hashes|
|
98
|
+
hash['refines']['@'] == '#'+ao_id
|
99
|
+
}
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
# Create a meta tag for:
|
105
|
+
# audio-overlay-active-class => media:active-class
|
106
|
+
# audio-overlay-playback-active-class => media:playback-active-class
|
107
|
+
# audio-overlay-duration => media:duration
|
108
|
+
# audio-overlay-narrator => media:narrator
|
109
|
+
#
|
110
|
+
# For each spine item with a 'media-overlay' key:
|
111
|
+
#
|
112
|
+
# Find the corresponding map resource
|
113
|
+
# Find the corresponding manifest item for component:
|
114
|
+
# - set 'media-overlay' to rsrc['id']
|
115
|
+
# Create a top-level meta tag:
|
116
|
+
# cmpt['audio-overlay-duration'] => meta[property='media:duration'][refines='rsrc["id"]]
|
117
|
+
# Also create a top-level meta tag for media:narrator if cmpt has 'audio-overlay-narrator'
|
118
|
+
#
|
119
|
+
def from_map(package)
|
120
|
+
opf_doc = package.file(:opf).document
|
121
|
+
metadata_tag = opf_doc.find('opf|metadata')
|
122
|
+
prop_to_meta(
|
123
|
+
package.map,
|
124
|
+
metadata_tag,
|
125
|
+
'audio-overlay-active-class',
|
126
|
+
'media:active-class'
|
127
|
+
)
|
128
|
+
prop_to_meta(
|
129
|
+
package.map,
|
130
|
+
metadata_tag,
|
131
|
+
'audio-overlay-playback-active-class',
|
132
|
+
'media:playback-active-class'
|
133
|
+
)
|
134
|
+
prop_to_meta(
|
135
|
+
package.map,
|
136
|
+
metadata_tag,
|
137
|
+
'audio-overlay-duration',
|
138
|
+
'media:duration'
|
139
|
+
)
|
140
|
+
prop_to_meta(
|
141
|
+
package.map,
|
142
|
+
metadata_tag,
|
143
|
+
'audio-overlay-narrator',
|
144
|
+
'media:narrator'
|
145
|
+
)
|
146
|
+
package.map['spine'].each { |cmpt|
|
147
|
+
next unless cmpt['audio-overlay']
|
148
|
+
rsrc = package.map['resources'].detect { |r|
|
149
|
+
r['path'] == cmpt['audio-overlay']
|
150
|
+
}
|
151
|
+
cmpt_manifest_item = opf_doc.find("opf|manifest > opf|item##{cmpt['id']}")
|
152
|
+
cmpt_manifest_item['media-overlay'] = rsrc['id']
|
153
|
+
duration_meta_tag = prop_to_meta(
|
154
|
+
cmpt,
|
155
|
+
metadata_tag,
|
156
|
+
'audio-overlay-duration',
|
157
|
+
'media:duration'
|
158
|
+
)
|
159
|
+
duration_meta_tag['refines'] = '#'+rsrc['id']
|
160
|
+
narrator_meta_tag = prop_to_meta(
|
161
|
+
cmpt,
|
162
|
+
metadata_tag,
|
163
|
+
'audio-overlay-narrator',
|
164
|
+
'media:narrator'
|
165
|
+
)
|
166
|
+
narrator_meta_tag['refines'] = '#'+rsrc['id'] if narrator_meta_tag
|
167
|
+
}
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
protected
|
172
|
+
|
173
|
+
def meta_to_prop(package, meta_name, prop_name, target = nil)
|
174
|
+
return unless package.map['metadata']
|
175
|
+
hashes = package.map['metadata'][meta_name]
|
176
|
+
return unless hashes && hashes.any?
|
177
|
+
hash = block_given? ? yield(hashes) : hashes.first
|
178
|
+
if hash
|
179
|
+
hashes.delete(hash)
|
180
|
+
package.map['metadata'].delete(meta_name) if hashes.empty?
|
181
|
+
(target || package.map)[prop_name] = hash['@']
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
|
186
|
+
# 5:34:31.396 = 5 hours, 34 minutes, 31 seconds and 396 milliseconds
|
187
|
+
# 124:59:36 = 124 hours, 59 minutes and 36 seconds
|
188
|
+
# 0:05:01.2 = 5 minutes, 1 second and 200 milliseconds
|
189
|
+
# 0:00:04 = 4 seconds
|
190
|
+
# 09:58 = 9 minutes and 58 seconds
|
191
|
+
# 00:56.78 = 56 seconds and 780 milliseconds
|
192
|
+
# 76.2s = 76.2 seconds = 76 seconds and 200 milliseconds
|
193
|
+
# 7.75h = 7.75 hours = 7 hours and 45 minutes
|
194
|
+
# 13min = 13 minutes
|
195
|
+
# 2345ms = 2345 milliseconds
|
196
|
+
# 12.345 = 12 seconds and 345 milliseconds
|
197
|
+
def smil_clock_value_to_seconds(clock)
|
198
|
+
return clock.to_f if clock.kind_of?(Numeric)
|
199
|
+
clock = clock.to_s
|
200
|
+
if match = clock.match(/(\d+:)?(\d+:)(\d+\.?\d*)/)
|
201
|
+
h = match[1].to_s.to_f
|
202
|
+
m = match[2].to_s.to_f
|
203
|
+
s = match[3].to_s.to_f
|
204
|
+
h*60*60 + m*60 + s
|
205
|
+
elsif match = clock.match(/^(\d+\.?\d*)h$/)
|
206
|
+
match[1].to_f*60*60
|
207
|
+
elsif match = clock.match(/^(\d+\.?\d*)min$/)
|
208
|
+
match[1].to_f*60
|
209
|
+
elsif match = clock.match(/^(\d+\.?\d*)s?$/)
|
210
|
+
clock.to_f
|
211
|
+
elsif match = clock.match(/^(\d+\.?\d*)ms$/)
|
212
|
+
clock.to_f / 1000.0
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
def prop_to_meta(scope, metadata_tag, prop_name, meta_name)
|
218
|
+
if scope[prop_name]
|
219
|
+
meta_tag = Nokogiri::XML::Node.new('meta', metadata_tag)
|
220
|
+
meta_tag['property'] = meta_name
|
221
|
+
meta_tag.content = scope[prop_name]
|
222
|
+
metadata_tag.add_child(meta_tag)
|
223
|
+
return meta_tag
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# For the specification, see the iBooks Asset Guide, specifically the
|
2
|
+
# section titled "Ambient Soundtrack" in version 5.1:
|
3
|
+
#
|
4
|
+
# https://itunesconnect.apple.com/docs/iBooksAssetGuide5.1Revision2.pdf
|
5
|
+
#
|
6
|
+
#
|
7
|
+
class BbEPUB::Transform::AudioSoundtrack < Bookbinder::Transform
|
8
|
+
|
9
|
+
def dependencies
|
10
|
+
[BbEPUB::Transform::Spine]
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
# Iterate through each spine item, looking for
|
15
|
+
#
|
16
|
+
# <audio epub:type="ibooks:soundtrack" src="..." />
|
17
|
+
#
|
18
|
+
def to_map(package)
|
19
|
+
package.map['spine'].each { |cmpt|
|
20
|
+
find_soundtrack_in_component(cmpt, package.file(cmpt['path']))
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
# Iterate through each spine item, adding an audio tag to components
|
26
|
+
# that don't have one, or setting the src if the audio tag exists.
|
27
|
+
#
|
28
|
+
def from_map(package)
|
29
|
+
package.map['spine'].each { |cmpt|
|
30
|
+
if cmpt['audio-soundtrack']
|
31
|
+
add_soundtrack_to_component(cmpt, package.file(cmpt['path']))
|
32
|
+
end
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
def find_soundtrack_in_component(cmpt, cmpt_file)
|
40
|
+
soundtrack_tag = soundtrack_tag_in_document(cmpt_file.document)
|
41
|
+
cmpt['audio-soundtrack'] = soundtrack_tag ? soundtrack_tag['src'] : nil
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def add_soundtrack_to_component(cmpt, cmpt_file)
|
46
|
+
cmpt_doc = cmpt_file.document
|
47
|
+
unless soundtrack_tag = soundtrack_tag_in_document(cmpt_doc)
|
48
|
+
cmpt_doc.add_namespace('epub')
|
49
|
+
cmpt_doc.add_prefix('ibooks', 'epub:prefix')
|
50
|
+
|
51
|
+
soundtrack_tag = cmpt_doc.new_node('audio', :append => 'body')
|
52
|
+
soundtrack_tag['epub:type'] = 'ibooks:soundtrack'
|
53
|
+
|
54
|
+
cmpt_doc.new_node('style', :append => 'head') { |style_tag|
|
55
|
+
style_tag['type'] = 'text/css'
|
56
|
+
style_tag['id'] = 'BB_HIDE_AUDIO_SOUNDTRACK'
|
57
|
+
style_tag.content = [
|
58
|
+
'audio[epub|type="ibooks:soundtrack"] {',
|
59
|
+
'position: absolute;',
|
60
|
+
'top: -100px;',
|
61
|
+
'}'
|
62
|
+
].join
|
63
|
+
}
|
64
|
+
end
|
65
|
+
soundtrack_tag['src'] = cmpt['audio-soundtrack']
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def soundtrack_tag_in_document(cmpt_doc)
|
70
|
+
cmpt_doc.find('audio[epub|type="ibooks:soundtrack"]')
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# The best source of information about wading through the EPUB
|
2
|
+
# cover image quagmire has always been Keith's article on the
|
3
|
+
# Threepress blog:
|
4
|
+
#
|
5
|
+
# http://blog.safaribooksonline.com/2009/11/20/best-practices-in-epub-cover-images/
|
6
|
+
#
|
7
|
+
# He added an update for EPUB3, which follows the spec but is
|
8
|
+
# a bit easier to grok:
|
9
|
+
#
|
10
|
+
# http://blog.safaribooksonline.com/2011/05/26/covers-in-epub3/
|
11
|
+
#
|
12
|
+
class BbEPUB::Transform::CoverImage < Bookbinder::Transform
|
13
|
+
|
14
|
+
def dependencies
|
15
|
+
[
|
16
|
+
BbEPUB::Transform::Metadata,
|
17
|
+
BbEPUB::Transform::CoverPage
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# If it's EPUB3, the cover will be in the 'properties' attribute
|
23
|
+
# of the manifest item: 'cover-image'
|
24
|
+
#
|
25
|
+
# Otherwise, look for a manifest item with an 'id' of 'cover-image'.
|
26
|
+
#
|
27
|
+
# Or, look for a meta tag with a 'name' of 'cover', then find the
|
28
|
+
# manifest item that has the 'id' that matches meta's 'content'.
|
29
|
+
#
|
30
|
+
# Set map['cover'] to this item (and remove it from map['resources']).
|
31
|
+
#
|
32
|
+
def to_map(package)
|
33
|
+
package.map['cover'] = {}
|
34
|
+
opf_doc = package.file(:opf).document('r')
|
35
|
+
cover_item = cover_item_from_manifest(package, opf_doc) ||
|
36
|
+
cover_item_from_metadata(package, opf_doc) ||
|
37
|
+
cover_item_from_cover_page(package)
|
38
|
+
if cover_resource = cover_resource_from_item(package, cover_item)
|
39
|
+
package.map['resources'].delete(cover_resource)
|
40
|
+
package.map['cover'].update("front" => cover_resource)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Belt and braces: give the manifest item a property of
|
46
|
+
# 'cover-image', an 'id' of 'cover-image' (updating any
|
47
|
+
# idrefs) and create a meta tag with 'name'='cover' and
|
48
|
+
# 'content'='cover-image'.
|
49
|
+
#
|
50
|
+
def from_map(package)
|
51
|
+
return unless package.map['cover'] && cover = package.map['cover']['front']
|
52
|
+
opf_doc = package.file(:opf).document
|
53
|
+
|
54
|
+
opf_doc.new_node('item', :append => 'opf|manifest') { |manifest_item_tag|
|
55
|
+
manifest_item_tag['href'] = package.make_href(cover['path'])
|
56
|
+
manifest_item_tag['media-type'] = cover['media-type']
|
57
|
+
manifest_item_tag['id'] = 'cover-image'
|
58
|
+
manifest_item_tag['properties'] = 'cover-image'
|
59
|
+
}
|
60
|
+
|
61
|
+
cover_id = package.make_id(cover['path'])
|
62
|
+
opf_doc.each('[idref="'+cover_id+'"]') { |idref|
|
63
|
+
idref['idref'] = cover_id
|
64
|
+
}
|
65
|
+
|
66
|
+
opf_doc.new_node('meta', :append => 'opf|metadata') { |cover_meta_tag|
|
67
|
+
cover_meta_tag['name'] = 'cover'
|
68
|
+
cover_meta_tag['content'] = 'cover-image'
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
def cover_item_from_manifest(package, opf_doc)
|
74
|
+
opf_doc.find('opf|manifest > opf|item[properties~="cover-image"]') ||
|
75
|
+
opf_doc.find('opf|manifest > opf|item[id="cover-image"]') ||
|
76
|
+
opf_doc.find('opf|manifest > opf|item[id="cover_image"]')
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
def cover_item_from_metadata(package, opf_doc)
|
81
|
+
cover_meta_props = (package.map['metadata'] || {}).delete('cover')
|
82
|
+
if cover_meta_props && cover_meta_props.any?
|
83
|
+
cover_image_id = cover_meta_props.first['content']['@']
|
84
|
+
opf_doc.find('opf|manifest > opf|item[id="'+cover_image_id+'"]')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def cover_item_from_cover_page(package)
|
90
|
+
if (nav = package.map['nav']) && (landmarks = nav['landmarks'])
|
91
|
+
if landmark = landmarks.detect { |it| it['type'] == 'cover' }
|
92
|
+
return package.map['spine'].detect { |c| c['path'] == landmark['path'] }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
# TODO: support SVG images too.
|
99
|
+
#
|
100
|
+
def cover_item_from_component(package, cmpt)
|
101
|
+
package.if_file(cmpt['path']) { |cmpt_file|
|
102
|
+
return unless cmpt_doc = cmpt_file.document('r')
|
103
|
+
return unless img_tag = cmpt_doc.find('body img')
|
104
|
+
opf_doc = package.file(:opf).document('r')
|
105
|
+
opf_doc.find('opf|manifest > opf|item[href="'+img_tag['src']+'"]')
|
106
|
+
}
|
107
|
+
rescue
|
108
|
+
nil
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def cover_resource_from_item(package, cover_item)
|
113
|
+
return nil unless cover_item && cover_item['id']
|
114
|
+
if cmpt = package.map['spine'].detect { |c| c['id'] == cover_item['id'] }
|
115
|
+
unless cover_item = cover_item_from_component(package, cmpt)
|
116
|
+
package.warn("Did not discover cover image in #{cmpt['path']}. SVG?")
|
117
|
+
return nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
package.map['resources'].detect { |r| r['id'] == cover_item['id'] }
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
class BbEPUB::Transform::CoverPage < Bookbinder::Transform
|
2
|
+
|
3
|
+
def dependencies
|
4
|
+
[BbEPUB::Transform::Spine]
|
5
|
+
end
|
6
|
+
|
7
|
+
|
8
|
+
# A: look in the Nav (if it exists) for a landmark li with an
|
9
|
+
# epub:type of 'cover', and find the spine item with that href.
|
10
|
+
#
|
11
|
+
# B: look for an OPF <guide><reference type="cover"> and find
|
12
|
+
# the spine item with the same href.
|
13
|
+
#
|
14
|
+
# C: look at the first spine item:
|
15
|
+
# - is it have /cover/ in the filename?
|
16
|
+
# - no? does it have an image and no body text?
|
17
|
+
# - no? does it have an svg and no body text?
|
18
|
+
#
|
19
|
+
# -> If found, add to map['nav']['landmarks'] with a 'type'
|
20
|
+
# of 'cover'.
|
21
|
+
#
|
22
|
+
def to_map(package)
|
23
|
+
cover_page_item =
|
24
|
+
cover_page_item_from_nav(package) ||
|
25
|
+
cover_page_item_from_opf_guide(package) ||
|
26
|
+
cover_page_item_from_id(package) ||
|
27
|
+
cover_page_item_from_first_spine_item(package)
|
28
|
+
|
29
|
+
if cover_page_item
|
30
|
+
package.map['nav'] ||= {}
|
31
|
+
package.map['nav']['landmarks'] ||= []
|
32
|
+
package.map['nav']['landmarks'].unshift(cover_page_item)
|
33
|
+
package.map['spine'].each { |item|
|
34
|
+
if item['path'] == cover_page_item['path']
|
35
|
+
item['linear'] = false
|
36
|
+
end
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Do nothing unless we have a map['nav']['landmark'] type='cover'.
|
43
|
+
#
|
44
|
+
# In the Nav (if it exists), create a landmark with an
|
45
|
+
# epub:type of 'cover'. Actually, don't -- let the landmarks feature
|
46
|
+
# handle this.
|
47
|
+
#
|
48
|
+
# In the OPF, create a <guide> element if it doesn't exist,
|
49
|
+
# and create a <reference type="cover" title="Cover" href="..."> tag
|
50
|
+
# within it.
|
51
|
+
#
|
52
|
+
def from_map(package)
|
53
|
+
return unless package.map['nav'] && package.map['nav']['landmarks']
|
54
|
+
cover_page_item = package.map['nav']['landmarks'].detect { |item|
|
55
|
+
item['type'] == 'cover'
|
56
|
+
}
|
57
|
+
return unless cover_page_item
|
58
|
+
|
59
|
+
opf_doc = package.file(:opf).document
|
60
|
+
unless guide_tag = opf_doc.find('opf|guide')
|
61
|
+
guide_tag = opf_doc.new_node('guide', :append => opf_doc.root)
|
62
|
+
end
|
63
|
+
|
64
|
+
opf_doc.new_node('reference', :append => guide_tag) { |ref_tag|
|
65
|
+
ref_tag['type'] = 'cover'
|
66
|
+
ref_tag['href'] = package.make_href(cover_page_item['path'])
|
67
|
+
ref_tag['title'] = cover_page_item['title']
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
# Look for an EPUB3 landmark with type 'cover'.
|
75
|
+
#
|
76
|
+
def cover_page_item_from_nav(package)
|
77
|
+
return unless nav_file = package.file(:nav)
|
78
|
+
nav_doc = nav_file.document('r')
|
79
|
+
if li = nav_doc.find('nav[epub|type="landmark"] li[epub|type="cover"]')
|
80
|
+
href_to_cover_page_item(
|
81
|
+
package,
|
82
|
+
li['href'],
|
83
|
+
li['title'] || li.content.strip
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
# Look for a guide reference with type 'cover' in the OPF.
|
90
|
+
#
|
91
|
+
def cover_page_item_from_opf_guide(package)
|
92
|
+
opf_doc = package.file(:opf).document('r')
|
93
|
+
if guide_ref_tag = opf_doc.find('opf|guide > opf|reference[type="cover"]')
|
94
|
+
href_to_cover_page_item(package, guide_ref_tag['href'])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
def cover_page_item_from_id(package)
|
100
|
+
opf_doc = package.file(:opf).document('r')
|
101
|
+
if manifest_tag = opf_doc.find('opf|manifest > opf|item[id^="cover"]')
|
102
|
+
if manifest_tag['media-type'].match(/ml$/)
|
103
|
+
href_to_cover_page_item(package, manifest_tag['href'])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
# Investigate whether the first spine item is a cover page.
|
110
|
+
#
|
111
|
+
def cover_page_item_from_first_spine_item(package)
|
112
|
+
spine = package.map['spine']
|
113
|
+
if spine.any? && file_path = package.map['spine'].first['path']
|
114
|
+
file_href = package.make_href(file_path)
|
115
|
+
if file_path.match(/cover/)
|
116
|
+
return href_to_cover_page_item(package, file_href)
|
117
|
+
end
|
118
|
+
package.if_file(file_path) { |cmpt_file|
|
119
|
+
if cmpt_doc = cmpt_file.document('r')
|
120
|
+
body = cmpt_doc.find('body')
|
121
|
+
nodes = body.xpath('.//text()[normalize-space()]')
|
122
|
+
# If the body has no text...
|
123
|
+
if nodes.empty?
|
124
|
+
# ...and it has an <img> or an <svg>...
|
125
|
+
if cmpt_doc.find('body img') || cmpt_doc.find('body svg')
|
126
|
+
# ...we'll treat it as a cover page.
|
127
|
+
return href_to_cover_page_item(package, file_href)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
# Given a href for a cover page, create the cover page item
|
137
|
+
# that will go into the landmarks array in the package map.
|
138
|
+
#
|
139
|
+
def href_to_cover_page_item(package, cover_href, page_title = nil)
|
140
|
+
cover_href = cover_href.sub(/#.*$/, '')
|
141
|
+
cover_page_path = package.make_path(cover_href)
|
142
|
+
package.if_file(cover_page_path) { |cover_page_file|
|
143
|
+
return unless doc = cover_page_file.document('r')
|
144
|
+
if page_title.nil? || page_title.empty?
|
145
|
+
title_tag = cover_page_file.document('r').find('head > title')
|
146
|
+
page_title = title_tag ? title_tag.content.strip : 'Cover page'
|
147
|
+
end
|
148
|
+
{
|
149
|
+
'type' => 'cover',
|
150
|
+
'path' => cover_page_path,
|
151
|
+
'title' => page_title,
|
152
|
+
# FIXME: acquire the real media-type from manifest item?
|
153
|
+
'media-type' => cover_page_file.media_type
|
154
|
+
}
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|