eeepub3 0.0.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.
@@ -0,0 +1,67 @@
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.ncx 'xmlns:ncx' => 'http://www.daisy.org/z3986/2005/ncx/', :version => '2005-1', :xmlns => 'http://www.daisy.org/z3986/2005/ncx/', 'xml:lang' => 'en' do
20
+ build_head(builder)
21
+ builder.docTitle { builder.text doc_title }
22
+ build_nav_map(builder)
23
+ end
24
+ end
25
+
26
+ def build_head(builder)
27
+ builder.head do
28
+ {
29
+ :uid => uid,
30
+ :depth => depth,
31
+ :totalPageCount => total_page_count,
32
+ :maxPageNumber => max_page_number
33
+ }.each do |k, v|
34
+ builder.meta :name => "dtb:#{k}", :content => v
35
+ end
36
+ end
37
+ end
38
+
39
+ def build_nav_map(builder)
40
+ builder.navMap do
41
+ builder_nav_point(builder, nav_map)
42
+ end
43
+ end
44
+
45
+ def builder_nav_point(builder, nav_point, play_order = 1)
46
+ case nav_point
47
+ when Array
48
+ nav_point.each do |point|
49
+ play_order = builder_nav_point(builder, point, play_order)
50
+ end
51
+ when Hash
52
+ id = nav_point[:id] || "navPoint-#{play_order}"
53
+ builder.navPoint :id => id, :playOrder => play_order do
54
+ builder.navLabel { builder.text nav_point[:label] }
55
+ builder.content :src => nav_point[:content]
56
+ play_order += 1
57
+ if nav_point[:nav]
58
+ play_order = builder_nav_point(builder, nav_point[:nav], play_order)
59
+ end
60
+ end
61
+ else
62
+ raise "nav_point must be Array or Hash"
63
+ end
64
+ play_order
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,131 @@
1
+ require '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
+ FileUtils.remove_entry_secure dir
106
+ end
107
+
108
+ # Stream OCF
109
+ #
110
+ # @return [String] streaming output of the zip/epub file.
111
+ def render
112
+ create_epub do
113
+ temp_file = Tempfile.new("ocf")
114
+ self.save(temp_file.path)
115
+ return temp_file.read
116
+ end
117
+ end
118
+
119
+ private
120
+ def create_epub
121
+ FileUtils.chdir(dir) do
122
+ meta_inf = 'META-INF'
123
+ FileUtils.mkdir_p(meta_inf)
124
+
125
+ container.save(File.join(meta_inf, 'container.xml'))
126
+ yield
127
+ end
128
+
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,154 @@
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' => 'uid',
52
+ 'xml:lang' => 'en-US',
53
+ 'prefix'=> 'cc: http://creativecommons.org/ns#',
54
+ 'version' => '3.0' do
55
+
56
+ build_metadata(builder)
57
+ build_manifest(builder)
58
+ build_spine(builder)
59
+ build_guide(builder)
60
+ end
61
+ end
62
+
63
+ def build_metadata(builder)
64
+ builder.metadata 'xmlns:dc' => "http://purl.org/dc/elements/1.1/" do
65
+ identifier.each do |i|
66
+ attrs = {}
67
+ attrs['opf:scheme'] = i[:scheme] if i[:scheme]
68
+ attrs[:id] = i[:id] if i[:id]
69
+ builder.dc :identifier, i[:value], attrs
70
+ end
71
+
72
+ [:title, :language, :subject, :description, :relation, :creator, :publisher, :date, :rights].each do |i|
73
+ value = self.send(i)
74
+ next unless value
75
+
76
+ [value].flatten.each do |v|
77
+ case v
78
+ when Hash
79
+ builder.dc i, v[:value], convert_to_xml_attributes(v.reject {|k, v| k == :value})
80
+ else
81
+ builder.dc i, v
82
+ end
83
+ end
84
+ end
85
+ builder.meta({:property => 'dcterms:modified'}, Time.now.strftime('%FT%TZ'))
86
+ builder.meta(:name => 'cover', :content => self.cover) if self.cover
87
+ end
88
+ end
89
+
90
+ def build_manifest(builder)
91
+ builder.manifest do
92
+ complete_manifest.each do |i|
93
+ if i[:media_type] == 'application/xhtml+xml'
94
+ builder.item :id => i[:id], :href => i[:href], 'media-type' => i[:media_type], 'properties' => i[:properties] || 'scripted'
95
+ else
96
+ builder.item :id => i[:id], :href => i[:href], 'media-type' => i[:media_type]
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ def build_spine(builder)
103
+ builder.spine :toc => toc do
104
+ spine.each do |i|
105
+ builder.itemref :idref => i
106
+ end
107
+ end
108
+ end
109
+
110
+ def build_guide(builder)
111
+ return if guide.nil? || guide.empty?
112
+
113
+ builder.guide do
114
+ guide.each do |i|
115
+ builder.reference convert_to_xml_attributes(i)
116
+ end
117
+ end
118
+ end
119
+
120
+ def complete_manifest
121
+ item_id_cache = {}
122
+
123
+ result = manifest.map do |i|
124
+ case i
125
+ when String
126
+ id = create_unique_item_id(i, item_id_cache)
127
+ href = i
128
+ media_type = guess_media_type(i)
129
+ when Hash
130
+ id = i[:id] || create_unique_item_id(i[:href], item_id_cache)
131
+ href = i[:href]
132
+ media_type = i[:media_type] || guess_media_type(i[:href])
133
+ end
134
+ {:id => id, :href => href, :media_type => media_type}
135
+ end
136
+
137
+ result += [{:id => 'nav', :href => 'nav.html', :media_type => 'application/xhtml+xml', :properties => 'nav'}]
138
+ result += [{:id => 'ncx', :href => ncx, :media_type => 'application/x-dtbncx+xml'}] if ncx
139
+ result
140
+ end
141
+
142
+ def create_unique_item_id(filename, id_cache)
143
+ basename = File.basename(filename)
144
+ unless id_cache[basename]
145
+ id_cache[basename] = 0
146
+ name = basename
147
+ else
148
+ name = "#{basename}-#{id_cache[basename]}"
149
+ end
150
+ id_cache[basename] += 1
151
+ name
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "EeePub::ContainerItem" do
4
+
5
+ context 'guess media type' do
6
+
7
+ before :each do
8
+ @container_item = EeePub::ContainerItem.new []
9
+ end
10
+
11
+ it 'should be application/xhtml+xml' do
12
+ media_type = 'application/xhtml+xml'
13
+ ['test.htm', 'test.html', 'test.xhtm', 'test.xhtml'].each do |file_name|
14
+ @container_item.send(:guess_media_type, file_name).should == media_type
15
+ end
16
+ @container_item.send(:guess_media_type, "test.xml").should_not == media_type
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,66 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "EeePub::Easy" do
4
+ before do
5
+ @easy = EeePub::Easy.new do
6
+ title 'sample'
7
+ creator 'jugyo'
8
+ identifier 'http://example.com/book/foo', :scheme => 'URL'
9
+ uid 'http://example.com/book/foo'
10
+ end
11
+
12
+ @easy.sections << ['1. foo', <<HTML]
13
+ <?xml version="1.0" encoding="UTF-8"?>
14
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
15
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
16
+ <head>
17
+ <title>foo</title>
18
+ </head>
19
+ <body>
20
+ <p>
21
+ foo foo foo foo foo foo
22
+ </p>
23
+ </body>
24
+ </html>
25
+ HTML
26
+
27
+ @easy.sections << ['2. bar', <<HTML]
28
+ <?xml version="1.0" encoding="UTF-8"?>
29
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
30
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
31
+ <head>
32
+ <title>bar</title>
33
+ </head>
34
+ <body>
35
+ <p>
36
+ bar bar bar bar bar bar
37
+ </p>
38
+ </body>
39
+ </html>
40
+ HTML
41
+
42
+ @easy.assets << 'image.png'
43
+ end
44
+
45
+ it 'spec for prepare' do
46
+ Dir.mktmpdir do |dir|
47
+ mock(FileUtils).cp('image.png', dir)
48
+
49
+ @easy.send(:prepare, dir)
50
+
51
+ file1 = File.join(dir, 'section_0.html')
52
+ file2 = File.join(dir, 'section_1.html')
53
+ File.exists?(file1).should be_true
54
+ File.exists?(file2).should be_true
55
+ File.read(file1).should == @easy.sections[0][1]
56
+ File.read(file2).should == @easy.sections[1][1]
57
+
58
+ @easy.instance_variable_get(:@nav).should == [
59
+ {:label => '1. foo', :content => 'section_0.html'},
60
+ {:label => '2. bar', :content => 'section_1.html'}
61
+ ]
62
+
63
+ @easy.instance_variable_get(:@files).should == [file1, file2, 'image.png']
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,131 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe "EeePub::Maker" do
4
+ before do
5
+ @maker = EeePub::Maker.new do
6
+ title 'sample'
7
+ creator 'jugyo'
8
+ publisher 'jugyo.org'
9
+ date "2010-05-06"
10
+ language 'en'
11
+ subject 'epub sample'
12
+ description 'this is epub sample'
13
+ rights 'xxx'
14
+ relation 'xxx'
15
+ identifier 'http://example.com/book/foo', :scheme => 'URL'
16
+ uid 'http://example.com/book/foo'
17
+ ncx_file 'toc.ncx'
18
+ opf_file 'content.opf'
19
+ cover 'cover.jpg'
20
+ files ['foo.html', 'bar.html']
21
+ nav [
22
+ {:label => '1. foo', :content => 'foo.html'},
23
+ {:label => '1. bar', :content => 'bar.html'}
24
+ ]
25
+ end
26
+ end
27
+
28
+ it { @maker.instance_variable_get(:@titles).should == ['sample'] }
29
+ it { @maker.instance_variable_get(:@creators).should == ['jugyo'] }
30
+ it { @maker.instance_variable_get(:@publishers).should == ['jugyo.org'] }
31
+ it { @maker.instance_variable_get(:@dates).should == ["2010-05-06"] }
32
+ it { @maker.instance_variable_get(:@identifiers).should == [{:value => 'http://example.com/book/foo', :scheme => 'URL', :id => nil}] }
33
+ it { @maker.instance_variable_get(:@uid).should == 'http://example.com/book/foo' }
34
+ it { @maker.instance_variable_get(:@ncx_file).should == 'toc.ncx' }
35
+ it { @maker.instance_variable_get(:@opf_file).should == 'content.opf' }
36
+ it { @maker.instance_variable_get(:@cover).should == 'cover.jpg' }
37
+ it { @maker.instance_variable_get(:@files).should == ['foo.html', 'bar.html'] }
38
+ it {
39
+ @maker.instance_variable_get(:@nav).should == [
40
+ {:label => '1. foo', :content => 'foo.html'},
41
+ {:label => '1. bar', :content => 'bar.html'}
42
+ ]
43
+ }
44
+
45
+ it 'should save' do
46
+ stub(FileUtils).cp.with_any_args
47
+ mock(Dir).mktmpdir { '/tmp' }
48
+ mock(EeePub::NCX).new(
49
+ :title => "sample",
50
+ :nav => [
51
+ {:label => '1. foo', :content => 'foo.html'},
52
+ {:label => '1. bar', :content => 'bar.html'}
53
+ ],
54
+ :uid=>{:value=>"http://example.com/book/foo", :scheme=>"URL", :id=>"http://example.com/book/foo"}
55
+ ) { stub!.save }
56
+ mock(EeePub::OPF).new(
57
+ :title => ["sample"],
58
+ :creator => ["jugyo"],
59
+ :date => ["2010-05-06"],
60
+ :language => ['en'],
61
+ :subject => ['epub sample'],
62
+ :description => ['this is epub sample'],
63
+ :rights => ['xxx'],
64
+ :relation => ['xxx'],
65
+ :ncx => "toc.ncx",
66
+ :cover => 'cover.jpg',
67
+ :publisher => ["jugyo.org"],
68
+ :unique_identifier=>"http://example.com/book/foo",
69
+ :identifier => [{:value => "http://example.com/book/foo", :scheme => "URL", :id => "http://example.com/book/foo"}],
70
+ :manifest => ['foo.html', 'bar.html']
71
+ ) { stub!.save }
72
+ mock(EeePub::OCF).new(
73
+ :container => "content.opf",
74
+ :dir => '/tmp'
75
+ ) { stub!.save }
76
+
77
+ @maker.save('test.epub')
78
+ end
79
+
80
+ describe "files as hash" do
81
+ before do
82
+ @maker = EeePub::Maker.new do
83
+ title 'sample'
84
+ creator 'jugyo'
85
+ publisher 'jugyo.org'
86
+ date "2010-05-06"
87
+ language 'en'
88
+ subject 'epub sample'
89
+ description 'this is epub sample'
90
+ rights 'xxx'
91
+ relation 'xxx'
92
+ identifier 'http://example.com/book/foo', :scheme => 'URL'
93
+ uid 'http://example.com/book/foo'
94
+ ncx_file 'toc.ncx'
95
+ opf_file 'content.opf'
96
+ cover 'cover.jpg'
97
+ files [{'foo.html' => 'foo/bar'}, {'bar.html' => 'foo/bar/baz'}]
98
+ nav [
99
+ {:label => '1. foo', :content => 'foo.html'},
100
+ {:label => '1. bar', :content => 'bar.html'}
101
+ ]
102
+ end
103
+ end
104
+
105
+ it 'should save' do
106
+ stub(FileUtils).cp.with_any_args
107
+ stub(FileUtils).mkdir_p.with_any_args
108
+ mock(Dir).mktmpdir { '/tmp' }
109
+ mock(EeePub::NCX).new.with_any_args { stub!.save }
110
+ mock(EeePub::OPF).new(
111
+ :title => ["sample"],
112
+ :creator => ["jugyo"],
113
+ :date => ["2010-05-06"],
114
+ :language => ['en'],
115
+ :subject => ['epub sample'],
116
+ :description => ['this is epub sample'],
117
+ :rights => ['xxx'],
118
+ :relation => ['xxx'],
119
+ :ncx => "toc.ncx",
120
+ :cover => 'cover.jpg',
121
+ :publisher => ["jugyo.org"],
122
+ :unique_identifier=>"http://example.com/book/foo",
123
+ :identifier => [{:value => "http://example.com/book/foo", :scheme => "URL", :id=>"http://example.com/book/foo"}],
124
+ :manifest => ["foo/bar/foo.html", "foo/bar/baz/bar.html"]
125
+ ) { stub!.save }
126
+ mock(EeePub::OCF).new.with_any_args { stub!.save }
127
+
128
+ @maker.save('test.epub')
129
+ end
130
+ end
131
+ end