sudothinker-eeepub 0.6.1
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.
- data/LICENSE +20 -0
- data/README.md +76 -0
- data/Rakefile +45 -0
- data/VERSION +1 -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 +108 -0
- data/lib/eeepub/maker.rb +125 -0
- data/lib/eeepub/ncx.rb +68 -0
- data/lib/eeepub/ocf.rb +107 -0
- data/lib/eeepub/opf.rb +148 -0
- data/spec/eeepub/easy_spec.rb +66 -0
- data/spec/eeepub/maker_spec.rb +124 -0
- data/spec/eeepub/ncx_spec.rb +80 -0
- data/spec/eeepub/ocf_spec.rb +43 -0
- data/spec/eeepub/opf_spec.rb +262 -0
- data/spec/eeepub_spec.rb +4 -0
- data/spec/spec_helper.rb +12 -0
- metadata +137 -0
data/lib/eeepub/maker.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module EeePub
|
5
|
+
# The class to make ePub easily
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# epub = EeePub.make do
|
9
|
+
# title 'sample'
|
10
|
+
# creator 'jugyo'
|
11
|
+
# publisher 'jugyo.org'
|
12
|
+
# date '2010-05-06'
|
13
|
+
# identifier 'http://example.com/book/foo', :scheme => 'URL'
|
14
|
+
# uid 'http://example.com/book/foo'
|
15
|
+
#
|
16
|
+
# files ['/path/to/foo.html', '/path/to/bar.html']
|
17
|
+
# nav [
|
18
|
+
# {:label => '1. foo', :content => 'foo.html', :nav => [
|
19
|
+
# {:label => '1.1 foo-1', :content => 'foo.html#foo-1'}
|
20
|
+
# ]},
|
21
|
+
# {:label => '1. bar', :content => 'bar.html'}
|
22
|
+
# ]
|
23
|
+
# end
|
24
|
+
# epub.save('sample.epub')
|
25
|
+
class Maker
|
26
|
+
[
|
27
|
+
:title,
|
28
|
+
:creator,
|
29
|
+
:publisher,
|
30
|
+
:date,
|
31
|
+
:language,
|
32
|
+
:subject,
|
33
|
+
:description,
|
34
|
+
:rights,
|
35
|
+
:relation
|
36
|
+
].each do |name|
|
37
|
+
class_eval <<-DELIM
|
38
|
+
def #{name}(value)
|
39
|
+
@#{name}s ||= []
|
40
|
+
@#{name}s << value
|
41
|
+
end
|
42
|
+
DELIM
|
43
|
+
end
|
44
|
+
|
45
|
+
[
|
46
|
+
:uid,
|
47
|
+
:files,
|
48
|
+
:nav,
|
49
|
+
:ncx_file,
|
50
|
+
:opf_file
|
51
|
+
].each do |name|
|
52
|
+
define_method(name) do |arg|
|
53
|
+
instance_variable_set("@#{name}", arg)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def identifier(id, options)
|
58
|
+
@identifiers ||= []
|
59
|
+
@identifiers << {:value => id, :scheme => options[:scheme]}
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param [Proc] block the block for initialize
|
63
|
+
def initialize(&block)
|
64
|
+
@files ||= []
|
65
|
+
@nav ||= []
|
66
|
+
@ncx_file ||= 'toc.ncx'
|
67
|
+
@opf_file ||= 'content.opf'
|
68
|
+
|
69
|
+
instance_eval(&block) if block_given?
|
70
|
+
end
|
71
|
+
|
72
|
+
# Save as ePub file
|
73
|
+
#
|
74
|
+
# @param [String] filename the ePub file name to save
|
75
|
+
def save(filename)
|
76
|
+
Dir.mktmpdir do |dir|
|
77
|
+
@files.each do |file|
|
78
|
+
case file
|
79
|
+
when String
|
80
|
+
FileUtils.cp(file, dir)
|
81
|
+
when Hash
|
82
|
+
file_path, dir_path = *file.first
|
83
|
+
dest_dir = File.join(dir, dir_path)
|
84
|
+
FileUtils.mkdir_p(dest_dir)
|
85
|
+
FileUtils.cp(file_path, dest_dir)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
NCX.new(
|
90
|
+
:uid => @uid,
|
91
|
+
:title => @titles[0],
|
92
|
+
:nav => @nav
|
93
|
+
).save(File.join(dir, @ncx_file))
|
94
|
+
|
95
|
+
OPF.new(
|
96
|
+
:title => @titles,
|
97
|
+
:identifier => @identifiers,
|
98
|
+
:creator => @creators,
|
99
|
+
:publisher => @publishers,
|
100
|
+
:date => @dates,
|
101
|
+
:language => @languages,
|
102
|
+
:subject => @subjects,
|
103
|
+
:description => @descriptions,
|
104
|
+
:rights => @rightss,
|
105
|
+
:relation => @relations,
|
106
|
+
:manifest => @files.map{|file|
|
107
|
+
case file
|
108
|
+
when String
|
109
|
+
File.basename(file)
|
110
|
+
when Hash
|
111
|
+
file_path, dir_path = *file.first
|
112
|
+
File.join(dir_path, File.basename(file_path))
|
113
|
+
end
|
114
|
+
},
|
115
|
+
:ncx => @ncx_file
|
116
|
+
).save(File.join(dir, @opf_file))
|
117
|
+
|
118
|
+
OCF.new(
|
119
|
+
:dir => dir,
|
120
|
+
:container => @opf_file
|
121
|
+
).save(filename)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
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,107 @@
|
|
1
|
+
module EeePub
|
2
|
+
# Class to create OCF
|
3
|
+
class OCF
|
4
|
+
# Class for 'container.xml' of OCF
|
5
|
+
class Container < ContainerItem
|
6
|
+
attr_accessor :rootfiles
|
7
|
+
|
8
|
+
# @param [String or Array or Hash]
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # with String
|
12
|
+
# EeePub::OCF::Container.new('container.opf')
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# # with Array
|
16
|
+
# EeePub::OCF::Container.new(['container.opf', 'other.opf'])
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# # with Hash
|
20
|
+
# EeePub::OCF::Container.new(
|
21
|
+
# :rootfiles => [
|
22
|
+
# {:full_path => 'container.opf', :media_type => 'application/oebps-package+xml'}
|
23
|
+
# ]
|
24
|
+
# )
|
25
|
+
def initialize(arg)
|
26
|
+
case arg
|
27
|
+
when String
|
28
|
+
set_values(
|
29
|
+
:rootfiles => [
|
30
|
+
{:full_path => arg, :media_type => guess_media_type(arg)}
|
31
|
+
]
|
32
|
+
)
|
33
|
+
when Array
|
34
|
+
# TODO: spec
|
35
|
+
set_values(
|
36
|
+
:rootfiles => arg.keys.map { |k|
|
37
|
+
filename = arg[k]
|
38
|
+
{:full_path => filename, :media_type => guess_media_type(filename)}
|
39
|
+
}
|
40
|
+
)
|
41
|
+
when Hash
|
42
|
+
set_values(arg)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def build_xml(builder)
|
49
|
+
builder.container :xmlns => "urn:oasis:names:tc:opendocument:xmlns:container", :version => "1.0" do
|
50
|
+
builder.rootfiles do
|
51
|
+
rootfiles.each do |i|
|
52
|
+
builder.rootfile convert_to_xml_attributes(i)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_accessor :dir, :container
|
60
|
+
|
61
|
+
# @param [Hash<Symbol, Object>] values the values of symbols and objects for OCF
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
# EeePub::OCF.new(
|
65
|
+
# :dir => '/path/to/dir',
|
66
|
+
# :container => 'container.opf'
|
67
|
+
# )
|
68
|
+
def initialize(values)
|
69
|
+
values.each do |k, v|
|
70
|
+
self.send(:"#{k}=", v)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Set container
|
75
|
+
#
|
76
|
+
# @param [EeePub::OCF::Container or args for EeePub::OCF::Container]
|
77
|
+
def container=(arg)
|
78
|
+
if arg.is_a?(EeePub::OCF::Container)
|
79
|
+
@container = arg
|
80
|
+
else
|
81
|
+
# TODO: spec
|
82
|
+
@container = EeePub::OCF::Container.new(arg)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Save as OCF
|
87
|
+
#
|
88
|
+
# @param [String] output_path the output file path of ePub
|
89
|
+
def save(output_path)
|
90
|
+
output_path = File.expand_path(output_path)
|
91
|
+
|
92
|
+
FileUtils.chdir(dir) do
|
93
|
+
File.open('mimetype', 'w') do |f|
|
94
|
+
f << 'application/epub+zip'
|
95
|
+
end
|
96
|
+
|
97
|
+
meta_inf = 'META-INF'
|
98
|
+
FileUtils.mkdir_p(meta_inf)
|
99
|
+
|
100
|
+
container.save(File.join(meta_inf, 'container.xml'))
|
101
|
+
|
102
|
+
%x(zip -X9 \"#{output_path}\" mimetype)
|
103
|
+
%x(zip -Xr9D \"#{output_path}\" * -xi mimetype)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/eeepub/opf.rb
ADDED
@@ -0,0 +1,148 @@
|
|
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' }.
|
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
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def build_manifest(builder)
|
90
|
+
builder.manifest do
|
91
|
+
complete_manifest.each do |i|
|
92
|
+
builder.item :id => i[:id], :href => i[:href], 'media-type' => i[:media_type]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_spine(builder)
|
98
|
+
builder.spine :toc => toc do
|
99
|
+
spine.each do |i|
|
100
|
+
builder.itemref :idref => i
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_guide(builder)
|
106
|
+
return if guide.nil? || guide.empty?
|
107
|
+
|
108
|
+
builder.guide do
|
109
|
+
guide.each do |i|
|
110
|
+
builder.reference convert_to_xml_attributes(i)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def complete_manifest
|
116
|
+
item_id_cache = {}
|
117
|
+
|
118
|
+
result = manifest.map do |i|
|
119
|
+
case i
|
120
|
+
when String
|
121
|
+
id = create_unique_item_id(i, item_id_cache)
|
122
|
+
href = i
|
123
|
+
media_type = guess_media_type(i)
|
124
|
+
when Hash
|
125
|
+
id = i[:id] || create_unique_item_id(i[:href], item_id_cache)
|
126
|
+
href = i[:href]
|
127
|
+
media_type = i[:media_type] || guess_media_type(i[:href])
|
128
|
+
end
|
129
|
+
{:id => id, :href => href, :media_type => media_type}
|
130
|
+
end
|
131
|
+
|
132
|
+
result += [{:id => 'ncx', :href => ncx, :media_type => 'application/x-dtbncx+xml'}] if ncx
|
133
|
+
result
|
134
|
+
end
|
135
|
+
|
136
|
+
def create_unique_item_id(filename, id_cache)
|
137
|
+
basename = File.basename(filename)
|
138
|
+
unless id_cache[basename]
|
139
|
+
id_cache[basename] = 0
|
140
|
+
name = basename
|
141
|
+
else
|
142
|
+
name = "#{basename}-#{id_cache[basename]}"
|
143
|
+
end
|
144
|
+
id_cache[basename] += 1
|
145
|
+
name
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|