eeepub-with-cover-support 0.8.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,168 @@
1
+ require 'tmpdir'
2
+ require 'fileutils'
3
+
4
+ module EeePub
5
+ # The class to make ePub easily
6
+ #
7
+ # Note on unique identifiers:
8
+ #
9
+ # At least one 'identifier' must be the unique identifer represented by the name
10
+ # given to 'uid' and set via the hash option :id => {name}. The default name for
11
+ # uid is 'BookId' and doesn't need to be specified explicitly. If no identifier is
12
+ # marked as the unique identifier, the first one give will be chosen.
13
+ #
14
+ # @example
15
+ # epub = EeePub.make do
16
+ # title 'sample'
17
+ # creator 'jugyo'
18
+ # publisher 'jugyo.org'
19
+ # date '2010-05-06'
20
+ # uid 'BookId'
21
+ # identifier 'http://example.com/book/foo', :scheme => 'URL', :id => 'BookId'
22
+ # toc_page '/path/to/toc.html', :title => 'Contents'
23
+ # cover_page '/path/to/cover.jpg', :title => 'Cover Image'
24
+ #
25
+ # files ['/path/to/foo.html', '/path/to/bar.html']
26
+ # nav [
27
+ # {:label => '1. foo', :content => 'foo.html', :nav => [
28
+ # {:label => '1.1 foo-1', :content => 'foo.html#foo-1'}
29
+ # ]},
30
+ # {:label => '1. bar', :content => 'bar.html'}
31
+ # ]
32
+ # end
33
+ # epub.save('sample.epub')
34
+ class Maker
35
+ [
36
+ :title,
37
+ :creator,
38
+ :publisher,
39
+ :date,
40
+ :language,
41
+ :subject,
42
+ :description,
43
+ :rights,
44
+ :relation,
45
+ ].each do |name|
46
+ class_eval <<-DELIM
47
+ def #{name}(value)
48
+ @#{name}s ||= []
49
+ @#{name}s << value
50
+ end
51
+ DELIM
52
+ end
53
+
54
+ [
55
+ :uid,
56
+ :files,
57
+ :nav,
58
+ :ncx_file,
59
+ :opf_file,
60
+ :toc_page,
61
+ :cover_page,
62
+ ].each do |name|
63
+ define_method(name) do |arg|
64
+ instance_variable_set("@#{name}", arg)
65
+ end
66
+ end
67
+
68
+ def identifier(id, options)
69
+ @identifiers ||= []
70
+ @identifiers << {:value => id, :scheme => options[:scheme], :id => options[:id]}
71
+ end
72
+
73
+ # @param [Proc] block the block for initialize
74
+ def initialize(&block)
75
+ @files ||= []
76
+ @nav ||= []
77
+ @ncx_file ||= 'toc.ncx'
78
+ @opf_file ||= 'content.opf'
79
+
80
+ instance_eval(&block) if block_given?
81
+ end
82
+
83
+ # Save as ePub file
84
+ #
85
+ # @param [String] filename the ePub file name to save
86
+ def save(filename)
87
+ create_epub.save(filename)
88
+ end
89
+
90
+ # instead of saving to file, output the file contents.
91
+ # important for serving on-the-fly doc creation from
92
+ # web interface where we don't want to allow file system
93
+ # writes (Heroku, et al})
94
+ def render
95
+ create_epub.render
96
+ end
97
+
98
+ private
99
+
100
+ def create_epub
101
+ @uid ||= 'BookId'
102
+ @identifiers ||= []
103
+ unique_identifier = @identifiers.select{ |i| i[:id] == @uid }.first
104
+ unless unique_identifier
105
+ unique_identifier = @identifiers.first
106
+ unique_identifier[:id] = @uid
107
+ end
108
+ dir = Dir.mktmpdir
109
+ if @cover_page
110
+ @files.push({ @cover_page => '', :id => 'cover'})
111
+ end
112
+ if @toc_page
113
+ @files.push({ @toc_page => '', :id => 'toc'})
114
+ end
115
+ @files.each do |file|
116
+ case file
117
+ when String
118
+ FileUtils.cp(file, dir)
119
+ when Hash
120
+ file_path, dir_path = *file.first
121
+ dest_dir = File.join(dir, dir_path)
122
+ FileUtils.mkdir_p(dest_dir)
123
+ FileUtils.cp(file_path, dest_dir)
124
+ end
125
+ end
126
+
127
+ NCX.new(
128
+ :uid => @identifiers.select{ |i| i[:id] == @uid }.first,
129
+ :title => @titles[0],
130
+ :nav => @nav
131
+ ).save(File.join(dir, @ncx_file))
132
+
133
+ OPF.new(
134
+ :title => @titles,
135
+ :unique_identifier => @uid,
136
+ :identifier => @identifiers,
137
+ :creator => @creators,
138
+ :publisher => @publishers,
139
+ :date => @dates,
140
+ :language => @languages,
141
+ :subject => @subjects,
142
+ :description => @descriptions,
143
+ :rights => @rightss,
144
+ :relation => @relations,
145
+ :guide => @guide,
146
+ :manifest => @files.map{|file|
147
+ case file
148
+ when String
149
+ File.basename(file)
150
+ when Hash
151
+ file_path, dir_path = *file.first
152
+ {
153
+ :href => File.join(dir_path, File.basename(file_path)).gsub(/^\//,''),
154
+ :id => file[:id],
155
+ :media_type => file[:media_type]
156
+ }
157
+ end
158
+ },
159
+ :ncx => @ncx_file
160
+ ).save(File.join(dir, @opf_file))
161
+
162
+ OCF.new(
163
+ :dir => dir,
164
+ :container => @opf_file
165
+ )
166
+ end
167
+ end
168
+ end
data/lib/eeepub/ncx.rb ADDED
@@ -0,0 +1,68 @@
1
+ module EeePub
2
+ class NCX < ContainerItem
3
+ attr_accessor :uid,
4
+ :depth,
5
+ :total_page_count,
6
+ :max_page_number,
7
+ :doc_title,
8
+ :nav_map
9
+
10
+ attr_alias :title, :doc_title
11
+ attr_alias :nav, :nav_map
12
+
13
+ default_value :depth, 1
14
+ default_value :total_page_count, 0
15
+ default_value :max_page_number, 0
16
+ default_value :doc_title, 'Untitled'
17
+
18
+ def build_xml(builder)
19
+ builder.declare! :DOCTYPE, :ncx, :PUBLIC, "-//NISO//DTD ncx 2005-1//EN", "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd"
20
+ builder.ncx :xmlns => "http://www.daisy.org/z3986/2005/ncx/", :version => "2005-1" do
21
+ build_head(builder)
22
+ builder.docTitle { builder.text doc_title }
23
+ build_nav_map(builder)
24
+ end
25
+ end
26
+
27
+ def build_head(builder)
28
+ builder.head do
29
+ {
30
+ :uid => uid,
31
+ :depth => depth,
32
+ :totalPageCount => total_page_count,
33
+ :maxPageNumber => max_page_number
34
+ }.each do |k, v|
35
+ builder.meta :name => "dtb:#{k}", :content => v
36
+ end
37
+ end
38
+ end
39
+
40
+ def build_nav_map(builder)
41
+ builder.navMap do
42
+ builder_nav_point(builder, nav_map)
43
+ end
44
+ end
45
+
46
+ def builder_nav_point(builder, nav_point, play_order = 1)
47
+ case nav_point
48
+ when Array
49
+ nav_point.each do |point|
50
+ play_order = builder_nav_point(builder, point, play_order)
51
+ end
52
+ when Hash
53
+ id = nav_point[:id] || "navPoint-#{play_order}"
54
+ builder.navPoint :id => id, :playOrder => play_order do
55
+ builder.navLabel { builder.text nav_point[:label] }
56
+ builder.content :src => nav_point[:content]
57
+ play_order += 1
58
+ if nav_point[:nav]
59
+ play_order = builder_nav_point(builder, nav_point[:nav], play_order)
60
+ end
61
+ end
62
+ else
63
+ raise "nav_point must be Array or Hash"
64
+ end
65
+ play_order
66
+ end
67
+ end
68
+ end
data/lib/eeepub/ocf.rb ADDED
@@ -0,0 +1,130 @@
1
+ require 'zip/zip'
2
+
3
+ module EeePub
4
+ # Class to create OCF
5
+ class OCF
6
+ # Class for 'container.xml' of OCF
7
+ class Container < ContainerItem
8
+ attr_accessor :rootfiles
9
+
10
+ # @param [String or Array or Hash]
11
+ #
12
+ # @example
13
+ # # with String
14
+ # EeePub::OCF::Container.new('container.opf')
15
+ #
16
+ # @example
17
+ # # with Array
18
+ # EeePub::OCF::Container.new(['container.opf', 'other.opf'])
19
+ #
20
+ # @example
21
+ # # with Hash
22
+ # EeePub::OCF::Container.new(
23
+ # :rootfiles => [
24
+ # {:full_path => 'container.opf', :media_type => 'application/oebps-package+xml'}
25
+ # ]
26
+ # )
27
+ def initialize(arg)
28
+ case arg
29
+ when String
30
+ set_values(
31
+ :rootfiles => [
32
+ {:full_path => arg, :media_type => guess_media_type(arg)}
33
+ ]
34
+ )
35
+ when Array
36
+ # TODO: spec
37
+ set_values(
38
+ :rootfiles => arg.keys.map { |k|
39
+ filename = arg[k]
40
+ {:full_path => filename, :media_type => guess_media_type(filename)}
41
+ }
42
+ )
43
+ when Hash
44
+ set_values(arg)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def build_xml(builder)
51
+ builder.container :xmlns => "urn:oasis:names:tc:opendocument:xmlns:container", :version => "1.0" do
52
+ builder.rootfiles do
53
+ rootfiles.each do |i|
54
+ builder.rootfile convert_to_xml_attributes(i)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ attr_accessor :dir, :container
62
+
63
+ # @param [Hash<Symbol, Object>] values the values of symbols and objects for OCF
64
+ #
65
+ # @example
66
+ # EeePub::OCF.new(
67
+ # :dir => '/path/to/dir',
68
+ # :container => 'container.opf'
69
+ # )
70
+ def initialize(values)
71
+ values.each do |k, v|
72
+ self.send(:"#{k}=", v)
73
+ end
74
+ end
75
+
76
+ # Set container
77
+ #
78
+ # @param [EeePub::OCF::Container or args for EeePub::OCF::Container]
79
+ def container=(arg)
80
+ if arg.is_a?(EeePub::OCF::Container)
81
+ @container = arg
82
+ else
83
+ # TODO: spec
84
+ @container = EeePub::OCF::Container.new(arg)
85
+ end
86
+ end
87
+
88
+ # Save as OCF
89
+ #
90
+ # @param [String] output_path the output file path of ePub
91
+ def save(output_path)
92
+ output_path = File.expand_path(output_path)
93
+
94
+ create_epub do
95
+ mimetype = Zip::ZipOutputStream::open(output_path) do |os|
96
+ os.put_next_entry("mimetype", nil, nil, Zip::ZipEntry::STORED, Zlib::NO_COMPRESSION)
97
+ os << "application/epub+zip"
98
+ end
99
+ zipfile = Zip::ZipFile.open(output_path)
100
+ Dir.glob('**/*').each do |path|
101
+ zipfile.add(path, path)
102
+ end
103
+ zipfile.commit
104
+ end
105
+ end
106
+
107
+ # Stream OCF
108
+ #
109
+ # @return [String] streaming output of the zip/epub file.
110
+ def render
111
+ create_epub do
112
+ temp_file = Tempfile.new("ocf")
113
+ self.save(temp_file.path)
114
+ return temp_file.read
115
+ end
116
+ end
117
+
118
+ private
119
+ def create_epub
120
+ FileUtils.chdir(dir) do
121
+ meta_inf = 'META-INF'
122
+ FileUtils.mkdir_p(meta_inf)
123
+
124
+ container.save(File.join(meta_inf, 'container.xml'))
125
+ yield
126
+ end
127
+
128
+ end
129
+ end
130
+ end
data/lib/eeepub/opf.rb ADDED
@@ -0,0 +1,172 @@
1
+ module EeePub
2
+ class OPF < ContainerItem
3
+ attr_accessor :unique_identifier,
4
+ :title,
5
+ :language,
6
+ :identifier,
7
+ :date,
8
+ :subject,
9
+ :description,
10
+ :relation,
11
+ :creator,
12
+ :publisher,
13
+ :rights,
14
+ :manifest,
15
+ :spine,
16
+ :guide,
17
+ :ncx,
18
+ :toc
19
+
20
+ default_value :toc, 'ncx'
21
+ default_value :unique_identifier, 'BookId'
22
+ default_value :title, 'Untitled'
23
+ default_value :language, 'en'
24
+
25
+ attr_alias :files, :manifest
26
+
27
+ def identifier
28
+ case @identifier
29
+ when Array
30
+ @identifier
31
+ when String
32
+ [{:value => @identifier, :id => unique_identifier}]
33
+ when Hash
34
+ @identifier[:id] = unique_identifier
35
+ [@identifier]
36
+ else
37
+ @identifier
38
+ end
39
+ end
40
+
41
+ def spine
42
+ @spine ||
43
+ complete_manifest.
44
+ select { |i| i[:media_type] == 'application/xhtml+xml' and i[:id] != 'toc' }.
45
+ map { |i| i[:id]}
46
+ end
47
+
48
+ def build_xml(builder)
49
+ builder.package :xmlns => "http://www.idpf.org/2007/opf",
50
+ 'unique-identifier' => unique_identifier,
51
+ 'version' => "2.0" do
52
+
53
+ build_metadata(builder)
54
+ build_manifest(builder)
55
+ build_spine(builder)
56
+ build_guide(builder)
57
+ end
58
+ end
59
+
60
+ def build_metadata(builder)
61
+ builder.metadata 'xmlns:dc' => "http://purl.org/dc/elements/1.1/",
62
+ 'xmlns:dcterms' => "http://purl.org/dc/terms/",
63
+ 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
64
+ 'xmlns:opf' => "http://www.idpf.org/2007/opf" do
65
+
66
+ identifier.each do |i|
67
+ attrs = {}
68
+ attrs['opf:scheme'] = i[:scheme] if i[:scheme]
69
+ attrs[:id] = i[:id] if i[:id]
70
+ builder.dc :identifier, i[:value], attrs
71
+ end
72
+
73
+ [:title, :language, :subject, :description, :relation, :creator, :publisher, :date, :rights].each do |i|
74
+ value = self.send(i)
75
+ next unless value
76
+
77
+ [value].flatten.each do |v|
78
+ case v
79
+ when Hash
80
+ builder.dc i, v[:value], convert_to_xml_attributes(v.reject {|k, v| k == :value})
81
+ else
82
+ builder.dc i, v
83
+ end
84
+ end
85
+ end
86
+ builder.meta :name => 'cover', :content => 'cover'
87
+ end
88
+ end
89
+
90
+ def build_manifest(builder)
91
+ builder.manifest do
92
+ complete_manifest.each do |i|
93
+ builder.item :id => i[:id], :href => i[:href], 'media-type' => i[:media_type]
94
+ end
95
+ end
96
+ end
97
+
98
+ def build_spine(builder)
99
+ builder.spine :toc => toc do
100
+ if cover_page
101
+ builder.itemref :idref => 'cover', :linear => 'no'
102
+ end
103
+ if toc_page
104
+ builder.itemref :idref => 'toc'
105
+ end
106
+ spine.each do |i|
107
+ builder.itemref :idref => i
108
+ end
109
+ end
110
+ end
111
+
112
+ def build_guide(builder)
113
+ return if (guide.nil? || guide.empty?) && !cover_page && !toc_page
114
+
115
+ builder.guide do
116
+ if toc_page
117
+ builder.reference :type => 'toc', :title => 'Table of Contents', :href => toc_page[:href]
118
+ end
119
+ if cover_page
120
+ builder.reference :type => 'cover', :title => 'Cover', :href => cover_page[:href]
121
+ end
122
+ builder.reference :type => 'text', :title => 'Welcome', :href => complete_manifest.first[:href]
123
+ if guide
124
+ guide.each do |i|
125
+ builder.reference convert_to_xml_attributes(i)
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ def complete_manifest
132
+ item_id_cache = {}
133
+
134
+ result = manifest.map do |i|
135
+ case i
136
+ when String
137
+ id = create_unique_item_id(i, item_id_cache)
138
+ href = i
139
+ media_type = guess_media_type(i)
140
+ when Hash
141
+ id = i[:id] || create_unique_item_id(i[:href], item_id_cache)
142
+ href = i[:href]
143
+ media_type = i[:media_type] || guess_media_type(i[:href])
144
+ end
145
+ {:id => id, :href => href, :media_type => media_type}
146
+ end
147
+
148
+ result += [{:id => 'ncx', :href => ncx, :media_type => 'application/x-dtbncx+xml'}] if ncx
149
+ result
150
+ end
151
+
152
+ def create_unique_item_id(filename, id_cache)
153
+ basename = File.basename(filename)
154
+ unless id_cache[basename]
155
+ id_cache[basename] = 0
156
+ name = basename
157
+ else
158
+ name = "#{basename}-#{id_cache[basename]}"
159
+ end
160
+ id_cache[basename] += 1
161
+ name
162
+ end
163
+
164
+ def cover_page
165
+ complete_manifest.find{ |i| i[:id] == 'cover' }
166
+ end
167
+
168
+ def toc_page
169
+ complete_manifest.find{ |i| i[:id] == 'toc' }
170
+ end
171
+ end
172
+ end