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.
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