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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +87 -0
- data/Rakefile +22 -0
- data/eeepub.gemspec +27 -0
- data/examples/files/bar.html +42 -0
- data/examples/files/foo.html +42 -0
- data/examples/simple_epub.rb +25 -0
- data/lib/eeepub.rb +15 -0
- data/lib/eeepub/container_item.rb +108 -0
- data/lib/eeepub/easy.rb +100 -0
- data/lib/eeepub/maker.rb +156 -0
- data/lib/eeepub/ncx.rb +68 -0
- data/lib/eeepub/ocf.rb +129 -0
- data/lib/eeepub/opf.rb +150 -0
- data/spec/eeepub/container_item_spec.rb +20 -0
- data/spec/eeepub/easy_spec.rb +71 -0
- data/spec/eeepub/maker_spec.rb +132 -0
- data/spec/eeepub/ncx_spec.rb +80 -0
- data/spec/eeepub/ocf_spec.rb +50 -0
- data/spec/eeepub/opf_spec.rb +267 -0
- data/spec/eeepub_spec.rb +4 -0
- data/spec/spec_helper.rb +13 -0
- metadata +193 -0
data/lib/eeepub/maker.rb
ADDED
@@ -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
|