bookbinder 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/README.md +97 -0
  2. data/Rakefile +12 -0
  3. data/bin/bookbinder +17 -0
  4. data/lib/bookbinder/document_proxy.rb +171 -0
  5. data/lib/bookbinder/file.rb +149 -0
  6. data/lib/bookbinder/file_system/directory.rb +62 -0
  7. data/lib/bookbinder/file_system/memory.rb +57 -0
  8. data/lib/bookbinder/file_system/zip_file.rb +106 -0
  9. data/lib/bookbinder/file_system.rb +35 -0
  10. data/lib/bookbinder/media_type.rb +17 -0
  11. data/lib/bookbinder/operations.rb +59 -0
  12. data/lib/bookbinder/package/epub.rb +69 -0
  13. data/lib/bookbinder/package/openbook.rb +33 -0
  14. data/lib/bookbinder/package.rb +295 -0
  15. data/lib/bookbinder/transform/epub/audio_overlay.rb +227 -0
  16. data/lib/bookbinder/transform/epub/audio_soundtrack.rb +73 -0
  17. data/lib/bookbinder/transform/epub/contributor.rb +11 -0
  18. data/lib/bookbinder/transform/epub/cover_image.rb +80 -0
  19. data/lib/bookbinder/transform/epub/cover_page.rb +148 -0
  20. data/lib/bookbinder/transform/epub/creator.rb +67 -0
  21. data/lib/bookbinder/transform/epub/description.rb +43 -0
  22. data/lib/bookbinder/transform/epub/language.rb +29 -0
  23. data/lib/bookbinder/transform/epub/metadata.rb +140 -0
  24. data/lib/bookbinder/transform/epub/nav.rb +60 -0
  25. data/lib/bookbinder/transform/epub/nav_toc.rb +177 -0
  26. data/lib/bookbinder/transform/epub/ncx.rb +63 -0
  27. data/lib/bookbinder/transform/epub/ocf.rb +33 -0
  28. data/lib/bookbinder/transform/epub/opf.rb +22 -0
  29. data/lib/bookbinder/transform/epub/package_identifier.rb +87 -0
  30. data/lib/bookbinder/transform/epub/rendition.rb +265 -0
  31. data/lib/bookbinder/transform/epub/resources.rb +38 -0
  32. data/lib/bookbinder/transform/epub/spine.rb +79 -0
  33. data/lib/bookbinder/transform/epub/title.rb +92 -0
  34. data/lib/bookbinder/transform/epub/version.rb +39 -0
  35. data/lib/bookbinder/transform/generator.rb +8 -0
  36. data/lib/bookbinder/transform/openbook/json.rb +15 -0
  37. data/lib/bookbinder/transform/organizer.rb +41 -0
  38. data/lib/bookbinder/transform.rb +7 -0
  39. data/lib/bookbinder/version.rb +5 -0
  40. data/lib/bookbinder.rb +29 -0
  41. 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