eeepub_ext 0.8.2

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.
@@ -0,0 +1,156 @@
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
+ #
23
+ # files ['/path/to/foo.html', '/path/to/bar.html']
24
+ # nav [
25
+ # {:label => '1. foo', :content => 'foo.html', :nav => [
26
+ # {:label => '1.1 foo-1', :content => 'foo.html#foo-1'}
27
+ # ]},
28
+ # {:label => '1. bar', :content => 'bar.html'}
29
+ # ]
30
+ # end
31
+ # epub.save('sample.epub')
32
+ class Maker
33
+ [
34
+ :title,
35
+ :creator,
36
+ :publisher,
37
+ :date,
38
+ :language,
39
+ :subject,
40
+ :description,
41
+ :rights,
42
+ :relation
43
+ ].each do |name|
44
+ class_eval <<-DELIM
45
+ def #{name}(value)
46
+ @#{name}s ||= []
47
+ @#{name}s << value
48
+ end
49
+ DELIM
50
+ end
51
+
52
+ [
53
+ :uid,
54
+ :files,
55
+ :nav,
56
+ :cover,
57
+ :ncx_file,
58
+ :opf_file,
59
+ :guide
60
+ ].each do |name|
61
+ define_method(name) do |arg|
62
+ instance_variable_set("@#{name}", arg)
63
+ end
64
+ end
65
+
66
+ def identifier(id, options)
67
+ @identifiers ||= []
68
+ @identifiers << {:value => id, :scheme => options[:scheme], :id => options[:id]}
69
+ end
70
+
71
+ # @param [Proc] block the block for initialize
72
+ def initialize(&block)
73
+ @files ||= []
74
+ @nav ||= []
75
+ @ncx_file ||= 'toc.ncx'
76
+ @opf_file ||= 'content.opf'
77
+
78
+ instance_eval(&block) if block_given?
79
+ end
80
+
81
+ # Save as ePub file
82
+ #
83
+ # @param [String] filename the ePub file name to save
84
+ def save(filename)
85
+ create_epub.save(filename)
86
+ end
87
+
88
+ # instead of saving to file, output the file contents.
89
+ # important for serving on-the-fly doc creation from
90
+ # web interface where we don't want to allow file system
91
+ # writes (Heroku, et al.)
92
+ def render
93
+ create_epub.render
94
+ end
95
+
96
+ private
97
+
98
+ def create_epub
99
+ @uid ||= 'BookId'
100
+ unique_identifier = @identifiers.select{ |i| i[:id] == @uid }.first
101
+ unless unique_identifier
102
+ unique_identifier = @identifiers.first
103
+ unique_identifier[:id] = @uid
104
+ end
105
+ dir = Dir.mktmpdir
106
+ @files.each do |file|
107
+ case file
108
+ when String
109
+ FileUtils.cp(file, dir)
110
+ when Hash
111
+ file_path, dir_path = *file.first
112
+ dest_dir = File.join(dir, dir_path)
113
+ FileUtils.mkdir_p(dest_dir)
114
+ FileUtils.cp(file_path, dest_dir)
115
+ end
116
+ end
117
+
118
+ NCX.new(
119
+ :uid => @identifiers.select{ |i| i[:id] == @uid }.first,
120
+ :title => @titles[0],
121
+ :nav => @nav
122
+ ).save(File.join(dir, @ncx_file))
123
+
124
+ OPF.new(
125
+ :title => @titles,
126
+ :unique_identifier => @uid,
127
+ :identifier => @identifiers,
128
+ :creator => @creators,
129
+ :publisher => @publishers,
130
+ :date => @dates,
131
+ :language => @languages,
132
+ :subject => @subjects,
133
+ :description => @descriptions,
134
+ :rights => @rightss,
135
+ :cover => @cover,
136
+ :relation => @relations,
137
+ :manifest => @files.map{|file|
138
+ case file
139
+ when String
140
+ File.basename(file)
141
+ when Hash
142
+ file_path, dir_path = *file.first
143
+ File.join(dir_path, File.basename(file_path))
144
+ end
145
+ },
146
+ :ncx => @ncx_file,
147
+ :guide => @guide
148
+ ).save(File.join(dir, @opf_file))
149
+
150
+ OCF.new(
151
+ :dir => dir,
152
+ :container => @opf_file
153
+ )
154
+ end
155
+ end
156
+ 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,129 @@
1
+ require 'zip'
2
+ module EeePub
3
+ # Class to create OCF
4
+ class OCF
5
+ # Class for 'container.xml' of OCF
6
+ class Container < ContainerItem
7
+ attr_accessor :rootfiles
8
+
9
+ # @param [String or Array or Hash]
10
+ #
11
+ # @example
12
+ # # with String
13
+ # EeePub::OCF::Container.new('container.opf')
14
+ #
15
+ # @example
16
+ # # with Array
17
+ # EeePub::OCF::Container.new(['container.opf', 'other.opf'])
18
+ #
19
+ # @example
20
+ # # with Hash
21
+ # EeePub::OCF::Container.new(
22
+ # :rootfiles => [
23
+ # {:full_path => 'container.opf', :media_type => 'application/oebps-package+xml'}
24
+ # ]
25
+ # )
26
+ def initialize(arg)
27
+ case arg
28
+ when String
29
+ set_values(
30
+ :rootfiles => [
31
+ {:full_path => arg, :media_type => guess_media_type(arg)}
32
+ ]
33
+ )
34
+ when Array
35
+ # TODO: spec
36
+ set_values(
37
+ :rootfiles => arg.keys.map { |k|
38
+ filename = arg[k]
39
+ {:full_path => filename, :media_type => guess_media_type(filename)}
40
+ }
41
+ )
42
+ when Hash
43
+ set_values(arg)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def build_xml(builder)
50
+ builder.container :xmlns => "urn:oasis:names:tc:opendocument:xmlns:container", :version => "1.0" do
51
+ builder.rootfiles do
52
+ rootfiles.each do |i|
53
+ builder.rootfile convert_to_xml_attributes(i)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ attr_accessor :dir, :container
61
+
62
+ # @param [Hash<Symbol, Object>] values the values of symbols and objects for OCF
63
+ #
64
+ # @example
65
+ # EeePub::OCF.new(
66
+ # :dir => '/path/to/dir',
67
+ # :container => 'container.opf'
68
+ # )
69
+ def initialize(values)
70
+ values.each do |k, v|
71
+ self.send(:"#{k}=", v)
72
+ end
73
+ end
74
+
75
+ # Set container
76
+ #
77
+ # @param [EeePub::OCF::Container or args for EeePub::OCF::Container]
78
+ def container=(arg)
79
+ if arg.is_a?(EeePub::OCF::Container)
80
+ @container = arg
81
+ else
82
+ # TODO: spec
83
+ @container = EeePub::OCF::Container.new(arg)
84
+ end
85
+ end
86
+
87
+ # Save as OCF
88
+ #
89
+ # @param [String] output_path the output file path of ePub
90
+ def save(output_path)
91
+ output_path = File.expand_path(output_path)
92
+ create_epub do
93
+ mimetype = Zip::OutputStream::open(output_path) do |os|
94
+ os.put_next_entry("mimetype", nil, nil, Zip::Entry::STORED, Zlib::NO_COMPRESSION)
95
+ os << "application/epub+zip"
96
+ end
97
+ zipfile = Zip::File.open(output_path)
98
+ Dir.glob('**/*').each do |path|
99
+ zipfile.add(path, path)
100
+ end
101
+ zipfile.commit
102
+ end
103
+ FileUtils.remove_entry_secure dir
104
+ end
105
+
106
+ # Stream OCF
107
+ #
108
+ # @return [String] streaming output of the zip/epub file.
109
+ def render
110
+ create_epub do
111
+ temp_file = Tempfile.new("ocf")
112
+ self.save(temp_file.path)
113
+ return temp_file.read
114
+ end
115
+ end
116
+
117
+ private
118
+ def create_epub
119
+ FileUtils.chdir(dir) do
120
+ meta_inf = 'META-INF'
121
+ FileUtils.mkdir_p(meta_inf)
122
+
123
+ container.save(File.join(meta_inf, 'container.xml'))
124
+ yield
125
+ end
126
+
127
+ end
128
+ end
129
+ end
data/lib/eeepub/opf.rb ADDED
@@ -0,0 +1,150 @@
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
+ :cover,
18
+ :ncx,
19
+ :toc
20
+
21
+ default_value :toc, 'ncx'
22
+ default_value :unique_identifier, 'BookId'
23
+ default_value :title, 'Untitled'
24
+ default_value :language, 'en'
25
+
26
+ attr_alias :files, :manifest
27
+
28
+ def identifier
29
+ case @identifier
30
+ when Array
31
+ @identifier
32
+ when String
33
+ [{:value => @identifier, :id => unique_identifier}]
34
+ when Hash
35
+ @identifier[:id] = unique_identifier
36
+ [@identifier]
37
+ else
38
+ @identifier
39
+ end
40
+ end
41
+
42
+ def spine
43
+ @spine ||
44
+ complete_manifest.
45
+ select { |i| i[:media_type] == 'application/xhtml+xml' }.
46
+ map { |i| i[:id]}
47
+ end
48
+
49
+ def build_xml(builder)
50
+ builder.package :xmlns => "http://www.idpf.org/2007/opf",
51
+ 'unique-identifier' => unique_identifier,
52
+ 'version' => "2.0" do
53
+
54
+ build_metadata(builder)
55
+ build_manifest(builder)
56
+ build_spine(builder)
57
+ build_guide(builder)
58
+ end
59
+ end
60
+
61
+ def build_metadata(builder)
62
+ builder.metadata 'xmlns:dc' => "http://purl.org/dc/elements/1.1/",
63
+ 'xmlns:dcterms' => "http://purl.org/dc/terms/",
64
+ 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
65
+ 'xmlns:opf' => "http://www.idpf.org/2007/opf" do
66
+
67
+ identifier.each do |i|
68
+ attrs = {}
69
+ attrs['opf:scheme'] = i[:scheme] if i[:scheme]
70
+ attrs[:id] = i[:id] if i[:id]
71
+ builder.dc :identifier, i[:value], attrs
72
+ end
73
+
74
+ [:title, :language, :subject, :description, :relation, :creator, :publisher, :date, :rights].each do |i|
75
+ value = self.send(i)
76
+ next unless value
77
+
78
+ [value].flatten.each do |v|
79
+ case v
80
+ when Hash
81
+ builder.dc i, v[:value], convert_to_xml_attributes(v.reject {|k, v| k == :value})
82
+ else
83
+ builder.dc i, v
84
+ end
85
+ end
86
+ end
87
+ builder.meta(:name => 'cover', :content => self.cover) if self.cover
88
+ end
89
+ end
90
+
91
+ def build_manifest(builder)
92
+ builder.manifest do
93
+ complete_manifest.each do |i|
94
+ builder.item :id => i[:id], :href => i[:href], 'media-type' => i[:media_type]
95
+ end
96
+ end
97
+ end
98
+
99
+ def build_spine(builder)
100
+ builder.spine :toc => toc do
101
+ spine.each do |i|
102
+ builder.itemref :idref => i
103
+ end
104
+ end
105
+ end
106
+
107
+ def build_guide(builder)
108
+ return if guide.nil? || guide.empty?
109
+
110
+ builder.guide do
111
+ guide.each do |i|
112
+ builder.reference convert_to_xml_attributes(i)
113
+ end
114
+ end
115
+ end
116
+
117
+ def complete_manifest
118
+ item_id_cache = {}
119
+
120
+ result = manifest.map do |i|
121
+ case i
122
+ when String
123
+ id = create_unique_item_id(i, item_id_cache)
124
+ href = i
125
+ media_type = guess_media_type(i)
126
+ when Hash
127
+ id = i[:id] || create_unique_item_id(i[:href], item_id_cache)
128
+ href = i[:href]
129
+ media_type = i[:media_type] || guess_media_type(i[:href])
130
+ end
131
+ {:id => id, :href => href, :media_type => media_type}
132
+ end
133
+
134
+ result += [{:id => 'ncx', :href => ncx, :media_type => 'application/x-dtbncx+xml'}] if ncx
135
+ result
136
+ end
137
+
138
+ def create_unique_item_id(filename, id_cache)
139
+ basename = File.basename(filename)
140
+ unless id_cache[basename]
141
+ id_cache[basename] = 0
142
+ name = basename
143
+ else
144
+ name = "#{basename}-#{id_cache[basename]}"
145
+ end
146
+ id_cache[basename] += 1
147
+ name
148
+ end
149
+ end
150
+ end