eeepub_ext 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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