epuber 0.7.4 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +10 -2
- data/LICENSE.txt +1 -1
- data/README.md +4 -3
- data/epuber.gemspec +11 -16
- data/lib/epuber/book/contributor.rb +10 -7
- data/lib/epuber/book/file_request.rb +3 -3
- data/lib/epuber/book/target.rb +30 -33
- data/lib/epuber/book/toc_item.rb +2 -4
- data/lib/epuber/book.rb +21 -21
- data/lib/epuber/checker/bookspec_checker.rb +26 -0
- data/lib/epuber/checker/text_checker.rb +16 -7
- data/lib/epuber/checker.rb +16 -2
- data/lib/epuber/checker_transformer_base.rb +3 -6
- data/lib/epuber/command/build.rb +40 -25
- data/lib/epuber/command/from_file.rb +39 -0
- data/lib/epuber/command/init.rb +34 -32
- data/lib/epuber/command/server.rb +3 -3
- data/lib/epuber/command.rb +18 -20
- data/lib/epuber/compiler/compilation_context.rb +10 -8
- data/lib/epuber/compiler/file_database.rb +2 -4
- data/lib/epuber/compiler/file_finders/abstract.rb +36 -26
- data/lib/epuber/compiler/file_finders/imaginary.rb +40 -35
- data/lib/epuber/compiler/file_resolver.rb +79 -89
- data/lib/epuber/compiler/file_stat.rb +4 -4
- data/lib/epuber/compiler/file_types/abstract_file.rb +4 -7
- data/lib/epuber/compiler/file_types/bade_file.rb +20 -15
- data/lib/epuber/compiler/file_types/coffee_script_file.rb +1 -1
- data/lib/epuber/compiler/file_types/css_file.rb +103 -0
- data/lib/epuber/compiler/file_types/generated_file.rb +1 -1
- data/lib/epuber/compiler/file_types/image_file.rb +4 -2
- data/lib/epuber/compiler/file_types/nav_file.rb +0 -1
- data/lib/epuber/compiler/file_types/opf_file.rb +0 -1
- data/lib/epuber/compiler/file_types/source_file.rb +8 -3
- data/lib/epuber/compiler/file_types/stylus_file.rb +4 -3
- data/lib/epuber/compiler/file_types/xhtml_file.rb +67 -13
- data/lib/epuber/compiler/generator.rb +1 -2
- data/lib/epuber/compiler/meta_inf_generator.rb +1 -1
- data/lib/epuber/compiler/nav_generator.rb +10 -11
- data/lib/epuber/compiler/opf_generator.rb +26 -27
- data/lib/epuber/compiler/problem.rb +12 -21
- data/lib/epuber/compiler/xhtml_processor.rb +63 -32
- data/lib/epuber/compiler.rb +77 -25
- data/lib/epuber/config.rb +16 -10
- data/lib/epuber/dsl/attribute.rb +17 -18
- data/lib/epuber/dsl/attribute_support.rb +7 -7
- data/lib/epuber/dsl/object.rb +19 -17
- data/lib/epuber/dsl/tree_object.rb +2 -3
- data/lib/epuber/epubcheck.rb +15 -0
- data/lib/epuber/from_file/bookspec_generator.rb +371 -0
- data/lib/epuber/from_file/encryption_handler.rb +146 -0
- data/lib/epuber/from_file/from_file_executor.rb +140 -0
- data/lib/epuber/from_file/nav_file.rb +163 -0
- data/lib/epuber/from_file/opf_file.rb +219 -0
- data/lib/epuber/helper.rb +0 -1
- data/lib/epuber/lockfile.rb +7 -9
- data/lib/epuber/plugin.rb +2 -3
- data/lib/epuber/ruby_extensions/match_data.rb +1 -1
- data/lib/epuber/ruby_extensions/thread.rb +1 -0
- data/lib/epuber/server/base.styl +0 -1
- data/lib/epuber/server/basic.styl +1 -30
- data/lib/epuber/server/handlers.rb +1 -1
- data/lib/epuber/server.rb +81 -80
- data/lib/epuber/third_party/bower.rb +5 -5
- data/lib/epuber/transformer/book_transformer.rb +108 -0
- data/lib/epuber/transformer/text_transformer.rb +4 -2
- data/lib/epuber/transformer.rb +4 -2
- data/lib/epuber/user_interface.rb +49 -38
- data/lib/epuber/vendor/hash_binding.rb +9 -2
- data/lib/epuber/vendor/ruby_templater.rb +4 -8
- data/lib/epuber/vendor/version.rb +12 -12
- data/lib/epuber/version.rb +1 -1
- metadata +79 -100
- data/lib/epuber/server/fonts/AvenirNext/AvenirNext-Bold.ttf +0 -0
- data/lib/epuber/server/fonts/AvenirNext/AvenirNext-BoldItalic.ttf +0 -0
- data/lib/epuber/server/fonts/AvenirNext/AvenirNext-Italic.ttf +0 -0
- data/lib/epuber/server/fonts/AvenirNext/AvenirNext-Regular.ttf +0 -0
@@ -10,7 +10,7 @@ module Epuber
|
|
10
10
|
def initialize(parent = nil)
|
11
11
|
super()
|
12
12
|
|
13
|
-
@parent
|
13
|
+
@parent = parent
|
14
14
|
@sub_items = []
|
15
15
|
|
16
16
|
parent.sub_items << self unless parent.nil?
|
@@ -94,8 +94,7 @@ module Epuber
|
|
94
94
|
|
95
95
|
protected
|
96
96
|
|
97
|
-
attr_writer :parent
|
98
|
-
attr_writer :sub_items
|
97
|
+
attr_writer :parent, :sub_items
|
99
98
|
end
|
100
99
|
end
|
101
100
|
end
|
@@ -0,0 +1,371 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module Epuber
|
6
|
+
class BookspecGenerator
|
7
|
+
class TocItem
|
8
|
+
# @return [String]
|
9
|
+
#
|
10
|
+
attr_accessor :href
|
11
|
+
|
12
|
+
# @return [String, nil]
|
13
|
+
#
|
14
|
+
attr_accessor :title
|
15
|
+
|
16
|
+
# @return [Array<TocItem>]
|
17
|
+
#
|
18
|
+
attr_accessor :children
|
19
|
+
|
20
|
+
# @return [Array<Symbol>]
|
21
|
+
#
|
22
|
+
attr_accessor :landmarks
|
23
|
+
|
24
|
+
def initialize(href, title = nil, landmarks = [], children = [])
|
25
|
+
@href = href
|
26
|
+
@title = title
|
27
|
+
@landmarks = landmarks
|
28
|
+
@children = children
|
29
|
+
end
|
30
|
+
|
31
|
+
def attribs
|
32
|
+
[href.inspect, title&.inspect, *landmarks.map(&:inspect)].compact.join(', ')
|
33
|
+
end
|
34
|
+
|
35
|
+
def ==(other)
|
36
|
+
other.is_a?(TocItem) &&
|
37
|
+
href == other.href &&
|
38
|
+
title == other.title &&
|
39
|
+
landmarks == other.landmarks &&
|
40
|
+
children == other.children
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_s(level = 0)
|
44
|
+
indent = ' ' * level
|
45
|
+
children_str = children.map { |c| c.to_s(level + 2) }.join("\n")
|
46
|
+
%(#{indent}#{href.inspect} #{title.inspect} #{landmarks.inspect}\n#{children_str})
|
47
|
+
end
|
48
|
+
|
49
|
+
def inspect
|
50
|
+
%(#<#{self.class.name} #{self}>)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param [Epuber::OpfFile] opf
|
55
|
+
# @param [Epuber::NavFile, nil] nav
|
56
|
+
#
|
57
|
+
def initialize(opf, nav)
|
58
|
+
@opf = opf
|
59
|
+
@nav = nav
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [String]
|
63
|
+
#
|
64
|
+
def generate_bookspec
|
65
|
+
@indent = 0
|
66
|
+
@bookspec = []
|
67
|
+
|
68
|
+
add_code('Epuber::Book.new do |book|', after: 'end') do
|
69
|
+
generate_titles
|
70
|
+
generate_authors
|
71
|
+
add_empty_line
|
72
|
+
generate_id
|
73
|
+
generate_language
|
74
|
+
generate_published
|
75
|
+
generate_publisher
|
76
|
+
add_empty_line
|
77
|
+
generate_cover
|
78
|
+
add_empty_line
|
79
|
+
generate_toc
|
80
|
+
end
|
81
|
+
add_empty_line
|
82
|
+
|
83
|
+
@bookspec.join("\n")
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [void]
|
87
|
+
#
|
88
|
+
def generate_titles
|
89
|
+
titles = @opf.metadata.css('title')
|
90
|
+
titles.each do |title|
|
91
|
+
is_main = titles.count == 1
|
92
|
+
|
93
|
+
id = title['id']
|
94
|
+
is_main = @opf.find_refines(id, 'title-type') == 'main' if id && !is_main
|
95
|
+
|
96
|
+
add_comment('alternate title found from original EPUB file (Epuber supports only one title)') unless is_main
|
97
|
+
add_setting_property(:title, title.text.inspect, commented: !is_main)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [void]
|
102
|
+
#
|
103
|
+
def generate_authors
|
104
|
+
authors = @opf.metadata.css('creator')
|
105
|
+
if authors.empty?
|
106
|
+
add_setting_property(:author, nil)
|
107
|
+
elsif authors.count == 1
|
108
|
+
add_setting_property(:author, format_author(authors.first))
|
109
|
+
else
|
110
|
+
add_setting_property(:authors, authors.map { |auth| format_author(auth) })
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [String]
|
115
|
+
#
|
116
|
+
def format_author(author_node)
|
117
|
+
name = author_node.text.strip
|
118
|
+
id = author_node['id']
|
119
|
+
|
120
|
+
if id
|
121
|
+
role = @opf.find_refines(id, 'role')
|
122
|
+
file_as = @opf.find_refines(id, 'file-as')
|
123
|
+
end
|
124
|
+
|
125
|
+
role ||= author_node['opf:role']
|
126
|
+
file_as ||= author_node['opf:file-as']
|
127
|
+
|
128
|
+
role_is_default = role.nil? || role == 'aut'
|
129
|
+
file_as_is_default = file_as.nil? || contributor_file_as_eq?(Book::Contributor.from_obj(name).file_as, file_as)
|
130
|
+
|
131
|
+
if role_is_default && file_as_is_default
|
132
|
+
name.inspect
|
133
|
+
elsif role_is_default && !file_as_is_default
|
134
|
+
%({ pretty_name: #{name.inspect}, file_as: #{file_as.inspect} })
|
135
|
+
elsif !role_is_default && file_as_is_default
|
136
|
+
%({ name: #{name.inspect}, role: #{role.inspect} })
|
137
|
+
else
|
138
|
+
%({ pretty_name: #{name.inspect}, file_as: #{file_as.inspect}, role: #{role.inspect} })
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# @return [void]
|
143
|
+
#
|
144
|
+
def generate_id
|
145
|
+
nodes = @opf.metadata.css('identifier')
|
146
|
+
|
147
|
+
nodes.each do |id_node|
|
148
|
+
value = id_node.text.strip
|
149
|
+
|
150
|
+
is_main = @opf.package['unique-identifier'] == id_node['id']
|
151
|
+
is_isbn = value.start_with?('urn:isbn:')
|
152
|
+
value = value.sub(/^urn:isbn:/, '').strip if is_isbn
|
153
|
+
key = is_isbn ? :isbn : :identifier
|
154
|
+
|
155
|
+
if is_main
|
156
|
+
add_setting_property(key, value.inspect)
|
157
|
+
else
|
158
|
+
add_comment('alternate identifier found from original EPUB file (Epuber supports only one identifier)')
|
159
|
+
add_setting_property(key, value.inspect, commented: true)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# @return [void]
|
165
|
+
#
|
166
|
+
def generate_language
|
167
|
+
language = @opf.metadata.at_css('language')
|
168
|
+
add_setting_property(:language, language.text.strip.inspect) if language
|
169
|
+
end
|
170
|
+
|
171
|
+
# @return [void]
|
172
|
+
#
|
173
|
+
def generate_published
|
174
|
+
published = @opf.metadata.at_css('date')
|
175
|
+
add_setting_property(:published, published.text.strip.inspect) if published
|
176
|
+
end
|
177
|
+
|
178
|
+
# @return [void]
|
179
|
+
#
|
180
|
+
def generate_publisher
|
181
|
+
publisher = @opf.metadata.at_css('publisher')
|
182
|
+
add_setting_property(:publisher, publisher.text.strip.inspect) if publisher
|
183
|
+
end
|
184
|
+
|
185
|
+
# @return [void]
|
186
|
+
#
|
187
|
+
def generate_cover
|
188
|
+
cover_property = Compiler::OPFGenerator::PROPERTIES_MAP[:cover_image]
|
189
|
+
|
190
|
+
cover_id = @opf.manifest_items.find { |_, item| item.properties&.include?(cover_property) }&.last&.id
|
191
|
+
cover_id ||= @opf.metadata.at_css('meta[name="cover"]')&.[]('content')
|
192
|
+
cover = @opf.manifest_file_by_id(cover_id) if cover_id
|
193
|
+
return unless cover
|
194
|
+
|
195
|
+
href = cover.href.sub(/#{Regexp.escape(File.extname(cover.href))}$/, '')
|
196
|
+
add_setting_property(:cover_image, href.inspect)
|
197
|
+
end
|
198
|
+
|
199
|
+
# @return [void]
|
200
|
+
#
|
201
|
+
def generate_toc
|
202
|
+
items = calculate_toc_items
|
203
|
+
|
204
|
+
render_toc_item = lambda do |item|
|
205
|
+
if item.children.empty?
|
206
|
+
add_code(%(toc.file #{item.attribs}))
|
207
|
+
else
|
208
|
+
add_code(%(toc.file #{item.attribs} do), after: 'end') do
|
209
|
+
item.children.each do |child|
|
210
|
+
render_toc_item.call(child)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
add_code('book.toc do |toc, target|', after: 'end') do
|
217
|
+
items.each do |toc_item|
|
218
|
+
render_toc_item.call(toc_item)
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
# Add code to final file. When block is given, it will be indented.
|
226
|
+
#
|
227
|
+
# @param [String] code code to print into final file
|
228
|
+
# @param [Boolean] commented should be code commented?
|
229
|
+
# @param [String, nil] after code to print after this code
|
230
|
+
#
|
231
|
+
# @return [void]
|
232
|
+
#
|
233
|
+
def add_code(code, commented: false, after: nil)
|
234
|
+
indent_str = ' ' * @indent
|
235
|
+
comment_prefix = commented ? '# ' : ''
|
236
|
+
|
237
|
+
@bookspec << %(#{indent_str}#{comment_prefix}#{code})
|
238
|
+
|
239
|
+
if block_given?
|
240
|
+
increase_indent
|
241
|
+
yield
|
242
|
+
decrease_indent
|
243
|
+
end
|
244
|
+
|
245
|
+
add_code(after, commented: commented) if after
|
246
|
+
end
|
247
|
+
|
248
|
+
def add_setting_property(name, value, commented: false)
|
249
|
+
if value.is_a?(Array)
|
250
|
+
add_code(%(book.#{name} = [), commented: commented, after: ']') do
|
251
|
+
value.each do |v|
|
252
|
+
add_code(%(#{v},), commented: commented)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
else
|
256
|
+
add_code(%(book.#{name} = #{value}), commented: commented)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def add_comment(text)
|
261
|
+
add_code(text, commented: true)
|
262
|
+
end
|
263
|
+
|
264
|
+
def add_empty_line
|
265
|
+
return if @bookspec.last == ''
|
266
|
+
|
267
|
+
@bookspec << ''
|
268
|
+
end
|
269
|
+
|
270
|
+
def increase_indent
|
271
|
+
@indent += 2
|
272
|
+
end
|
273
|
+
|
274
|
+
def decrease_indent
|
275
|
+
@indent -= 2
|
276
|
+
@indent = 0 if @indent.negative?
|
277
|
+
end
|
278
|
+
|
279
|
+
# Compares two contributors by file_as, it will return true if file_as is same even when case is different
|
280
|
+
#
|
281
|
+
# @param [String] file_as_a
|
282
|
+
# @param [String] file_as_b
|
283
|
+
#
|
284
|
+
# @return [Boolean]
|
285
|
+
#
|
286
|
+
def contributor_file_as_eq?(file_as_a, file_as_b)
|
287
|
+
file_as_a == file_as_b || file_as_a.mb_chars.downcase == file_as_b.mb_chars.downcase
|
288
|
+
end
|
289
|
+
|
290
|
+
# @param [String] href
|
291
|
+
#
|
292
|
+
# @return [Array<Symbol>]
|
293
|
+
#
|
294
|
+
def landmark_for_href(href)
|
295
|
+
landmarks = @nav&.landmarks
|
296
|
+
landmark_map = NavFile::LANDMARKS_MAP unless landmarks.nil?
|
297
|
+
|
298
|
+
if landmarks.nil?
|
299
|
+
landmarks = @opf.guide_items
|
300
|
+
landmark_map = OpfFile::LANDMARKS_MAP
|
301
|
+
end
|
302
|
+
|
303
|
+
found = landmarks.select { |l| l.href == href }
|
304
|
+
.map(&:type)
|
305
|
+
.compact
|
306
|
+
.map { |t| landmark_map[t] }
|
307
|
+
.compact
|
308
|
+
|
309
|
+
return found unless found.empty?
|
310
|
+
|
311
|
+
# try to find by href without fragment
|
312
|
+
if href.include?('#')
|
313
|
+
href = href.split('#').first
|
314
|
+
return landmark_for_href(href)
|
315
|
+
end
|
316
|
+
|
317
|
+
[]
|
318
|
+
end
|
319
|
+
|
320
|
+
# @return [Array<Epuber::BookspecGenerator::TocItem>]
|
321
|
+
#
|
322
|
+
def calculate_toc_items
|
323
|
+
spine_items = @opf.spine_items
|
324
|
+
idrefs = spine_items.map(&:idref)
|
325
|
+
landmarks = Set.new
|
326
|
+
|
327
|
+
# @param [ManifestItem, nil] manifest_item
|
328
|
+
# @param [NavItem, nil] toc_item
|
329
|
+
#
|
330
|
+
# @return [Array<Epuber::BookspecGenerator::TocItem>]
|
331
|
+
#
|
332
|
+
render_toc_item = lambda do |manifest_item, toc_item|
|
333
|
+
# ignore this item when it was already rendered
|
334
|
+
next nil if manifest_item && !idrefs.include?(manifest_item.id)
|
335
|
+
|
336
|
+
manifest_item ||= @opf.manifest_file_by_href(toc_item.href)
|
337
|
+
|
338
|
+
href = toc_item&.href || manifest_item.href
|
339
|
+
toc_item ||= @nav&.find_by_href(href) || @nav&.find_by_href(href, ignore_fragment: true)
|
340
|
+
href = toc_item&.href || manifest_item.href
|
341
|
+
|
342
|
+
href_output = href.sub(/\.x?html$/, '')
|
343
|
+
.sub(/\.x?html#/, '#')
|
344
|
+
|
345
|
+
landmarks_for_this = landmark_for_href(href)
|
346
|
+
unless landmarks_for_this.empty?
|
347
|
+
diff = Set.new(landmarks_for_this) - landmarks
|
348
|
+
landmarks += diff
|
349
|
+
|
350
|
+
landmarks_for_this = diff.to_a
|
351
|
+
end
|
352
|
+
|
353
|
+
item = TocItem.new(href_output, toc_item&.title, landmarks_for_this)
|
354
|
+
item.children = toc_item&.children&.map do |child|
|
355
|
+
render_toc_item.call(nil, child)
|
356
|
+
end || []
|
357
|
+
|
358
|
+
idrefs.delete(manifest_item.id) if manifest_item
|
359
|
+
|
360
|
+
item
|
361
|
+
end
|
362
|
+
|
363
|
+
spine_items.map do |itemref|
|
364
|
+
idref = itemref.idref
|
365
|
+
next unless idref
|
366
|
+
|
367
|
+
render_toc_item.call(@opf.manifest_file_by_id(idref), nil)
|
368
|
+
end.compact
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'uuidtools'
|
5
|
+
require 'digest'
|
6
|
+
|
7
|
+
module Epuber
|
8
|
+
class EncryptionHandler
|
9
|
+
class EncryptionItem
|
10
|
+
# Encryption algorithm (probably EncryptionHandler::ADOBE_OBFUSCATION or EncryptionHandler::IDPF_OBFUSCATION)
|
11
|
+
#
|
12
|
+
# @return [String]
|
13
|
+
#
|
14
|
+
attr_accessor :algorithm
|
15
|
+
|
16
|
+
# Absolute path to file (from root of EPUB)
|
17
|
+
#
|
18
|
+
# @return [String]
|
19
|
+
#
|
20
|
+
attr_accessor :file_path
|
21
|
+
|
22
|
+
# Encryption key for this file
|
23
|
+
#
|
24
|
+
# @return [String, nil]
|
25
|
+
#
|
26
|
+
attr_accessor :key
|
27
|
+
|
28
|
+
# @param [String] algorithm
|
29
|
+
# @param [String] file_path
|
30
|
+
#
|
31
|
+
def initialize(algorithm, file_path)
|
32
|
+
@algorithm = algorithm
|
33
|
+
@file_path = file_path
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
ADOBE_OBFUSCATION = 'http://ns.adobe.com/pdf/enc#RC'
|
38
|
+
IDPF_OBFUSCATION = 'http://www.idpf.org/2008/embedding'
|
39
|
+
|
40
|
+
# @return [Hash<String, EncryptionItem>] key is abs file path (from root of EPUB), value is EncryptionItem
|
41
|
+
#
|
42
|
+
attr_reader :encryption_items
|
43
|
+
|
44
|
+
# @param [String] encryption_file contents of META-INF/encryption.xml file
|
45
|
+
# @param [Epuber::OpfFile] opf
|
46
|
+
#
|
47
|
+
def initialize(encryption_file, opf)
|
48
|
+
@opf = opf
|
49
|
+
@encryption_items = _prepare_items(encryption_file)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param [String] path
|
53
|
+
# @param [String] data
|
54
|
+
def process_file(path, data)
|
55
|
+
enc_item = @encryption_items[path]
|
56
|
+
data = EncryptionHandler.decrypt_data(enc_item.key, data, enc_item.algorithm) if enc_item
|
57
|
+
|
58
|
+
data
|
59
|
+
end
|
60
|
+
|
61
|
+
# Decrypt data with given key and algorithm
|
62
|
+
#
|
63
|
+
# @param [String] key
|
64
|
+
# @param [String] data
|
65
|
+
# @param [String] algorithm
|
66
|
+
#
|
67
|
+
def self.decrypt_data(key, data, algorithm)
|
68
|
+
is_adobe = algorithm == ADOBE_OBFUSCATION
|
69
|
+
crypt_len = is_adobe ? 1024 : 1040
|
70
|
+
crypt = data.byteslice(0, crypt_len)
|
71
|
+
.bytes
|
72
|
+
key_cycle = key.bytes
|
73
|
+
.cycle
|
74
|
+
decrypt = crypt.each_with_object([]) { |x, acc| acc << (x ^ key_cycle.next) }
|
75
|
+
.pack('C*')
|
76
|
+
decrypt + data.byteslice(crypt_len..-1)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Parse IDPF key from unique identifier (main identifier from OPF file)
|
80
|
+
#
|
81
|
+
# @param [String] raw_unique_identifier
|
82
|
+
#
|
83
|
+
# @return [String, nil]
|
84
|
+
#
|
85
|
+
def self.parse_idpf_key(raw_unique_identifier)
|
86
|
+
key = raw_unique_identifier.strip.gsub(/[\u0020\u0009\u000d\u000a]/, '')
|
87
|
+
Digest::SHA1.digest(key)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param [String] raw_unique_identifier
|
91
|
+
# @param [Array<Nokogiri::XML::Node>] identifiers
|
92
|
+
#
|
93
|
+
# @return [String, nil]
|
94
|
+
#
|
95
|
+
def self.find_and_parse_encryption_key(identifiers)
|
96
|
+
raw_identifier = identifiers.find do |i|
|
97
|
+
i['scheme']&.downcase == 'uuid' || i.text.strip.start_with?('urn:uuid:')
|
98
|
+
end&.text&.strip
|
99
|
+
return nil unless raw_identifier
|
100
|
+
|
101
|
+
uuid_str = raw_identifier.sub(/^urn:uuid:/, '')
|
102
|
+
UUIDTools::UUID.parse(uuid_str).raw
|
103
|
+
end
|
104
|
+
|
105
|
+
# Parse META-INF/encryption.xml file
|
106
|
+
#
|
107
|
+
# @return [Array<EncryptionItem>, nil]
|
108
|
+
#
|
109
|
+
def self.parse_encryption_file(string)
|
110
|
+
doc = Nokogiri::XML(string)
|
111
|
+
doc.remove_namespaces!
|
112
|
+
|
113
|
+
encryption_node = doc.at_css('encryption')
|
114
|
+
return nil unless encryption_node
|
115
|
+
|
116
|
+
encryption_node.css('EncryptedData')
|
117
|
+
.map do |encrypted_data_node|
|
118
|
+
algorithm = encrypted_data_node.at_css('EncryptionMethod')['Algorithm']
|
119
|
+
file_path = encrypted_data_node.at_css('CipherData CipherReference')['URI']
|
120
|
+
|
121
|
+
EncryptionItem.new(algorithm, file_path)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Prepare encryption items with correct keys
|
126
|
+
#
|
127
|
+
# @param [String] encryption_file
|
128
|
+
#
|
129
|
+
# @return [Hash<String, EncryptionItem>]
|
130
|
+
#
|
131
|
+
def _prepare_items(encryption_file)
|
132
|
+
idpf_key = EncryptionHandler.parse_idpf_key(@opf.raw_unique_identifier)
|
133
|
+
adobe_key = EncryptionHandler.find_and_parse_encryption_key(@opf.identifiers)
|
134
|
+
|
135
|
+
items = EncryptionHandler.parse_encryption_file(encryption_file)
|
136
|
+
items.each do |i|
|
137
|
+
if i.algorithm == EncryptionHandler::IDPF_OBFUSCATION
|
138
|
+
i.key = idpf_key
|
139
|
+
elsif i.algorithm == EncryptionHandler::ADOBE_OBFUSCATION
|
140
|
+
i.key = adobe_key
|
141
|
+
end
|
142
|
+
end
|
143
|
+
items.map { |i| [i.file_path, i] }.to_h
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zip'
|
4
|
+
|
5
|
+
require_relative 'bookspec_generator'
|
6
|
+
require_relative 'opf_file'
|
7
|
+
require_relative 'nav_file'
|
8
|
+
require_relative 'encryption_handler'
|
9
|
+
|
10
|
+
module Epuber
|
11
|
+
class FromFileExecutor
|
12
|
+
MIMETYPE_PATH = 'mimetype'
|
13
|
+
|
14
|
+
ENCRYPTION_PATH = 'META-INF/encryption.xml'
|
15
|
+
CONTAINER_PATH = 'META-INF/container.xml'
|
16
|
+
|
17
|
+
# @param [String] filepath path to EPUB file
|
18
|
+
#
|
19
|
+
def initialize(filepath)
|
20
|
+
@filepath = filepath
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
UI.puts "📖 Loading EPUB file #{@filepath}"
|
25
|
+
|
26
|
+
Zip::File.open(@filepath) do |zip_file|
|
27
|
+
@zip_file = zip_file
|
28
|
+
|
29
|
+
validate_mimetype
|
30
|
+
|
31
|
+
@opf_path = content_opf_path
|
32
|
+
UI.puts " Parsing OPF file at #{@opf_path}"
|
33
|
+
@opf = OpfFile.new(zip_file.read(@opf_path))
|
34
|
+
|
35
|
+
if zip_file.find_entry(ENCRYPTION_PATH)
|
36
|
+
UI.puts ' Parsing encryption.xml file'
|
37
|
+
@encryption_handler = EncryptionHandler.new(zip_file.read(ENCRYPTION_PATH), @opf)
|
38
|
+
end
|
39
|
+
|
40
|
+
UI.puts ' Generating bookspec file'
|
41
|
+
basename = File.basename(@filepath, File.extname(@filepath))
|
42
|
+
File.write("#{basename}.bookspec", generate_bookspec)
|
43
|
+
|
44
|
+
export_files
|
45
|
+
|
46
|
+
UI.puts '' # empty line
|
47
|
+
UI.puts <<~TEXT.rstrip.ansi.green
|
48
|
+
🎉 Project initialized.
|
49
|
+
Please review generated #{basename}.bookspec file and start using Epuber.
|
50
|
+
|
51
|
+
For more information about Epuber, please visit https://github.com/epuber-io/epuber/tree/master/docs.
|
52
|
+
TEXT
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param [Zip::File] zip_file
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
#
|
60
|
+
def validate_mimetype
|
61
|
+
entry = @zip_file.find_entry(MIMETYPE_PATH)
|
62
|
+
UI.error! "This is not valid EPUB file (#{MIMETYPE_PATH} file is missing)" if entry.nil?
|
63
|
+
|
64
|
+
mimetype = @zip_file.read(entry)
|
65
|
+
|
66
|
+
return if mimetype == 'application/epub+zip'
|
67
|
+
|
68
|
+
UI.error! <<~MSG
|
69
|
+
This is not valid EPUB file (#{MIMETYPE_PATH} file does not contain required application/epub+zip, it is #{mimetype} instead)
|
70
|
+
MSG
|
71
|
+
end
|
72
|
+
|
73
|
+
# @param [Zip::File] zip_file
|
74
|
+
#
|
75
|
+
# @return [String]
|
76
|
+
def content_opf_path
|
77
|
+
entry = @zip_file.find_entry(CONTAINER_PATH)
|
78
|
+
UI.error! "This is not valid EPUB file (#{CONTAINER_PATH} file is missing)" if entry.nil?
|
79
|
+
|
80
|
+
doc = Nokogiri::XML(@zip_file.read(entry))
|
81
|
+
doc.remove_namespaces!
|
82
|
+
|
83
|
+
rootfile = doc.at_xpath('//rootfile')
|
84
|
+
if rootfile.nil?
|
85
|
+
UI.error! "This is not valid EPUB file (#{CONTAINER_PATH} file does not contain any <rootfile> element)"
|
86
|
+
end
|
87
|
+
|
88
|
+
rootfile['full-path']
|
89
|
+
end
|
90
|
+
|
91
|
+
# @param [Nokogiri::XML::Document] opf
|
92
|
+
# @param [Zip::File] zip_file
|
93
|
+
#
|
94
|
+
# @return [String]
|
95
|
+
def generate_bookspec
|
96
|
+
nav_node, nav_mode = @opf.find_nav
|
97
|
+
if nav_node
|
98
|
+
nav_path = Pathname.new(@opf_path)
|
99
|
+
.dirname
|
100
|
+
.join(nav_node.href)
|
101
|
+
.to_s
|
102
|
+
nav = NavFile.new(@zip_file.read(nav_path), nav_mode)
|
103
|
+
end
|
104
|
+
|
105
|
+
BookspecGenerator.new(@opf, nav).generate_bookspec
|
106
|
+
end
|
107
|
+
|
108
|
+
def export_files
|
109
|
+
@opf.manifest_items.each_value do |item|
|
110
|
+
# ignore text files which are not in spine
|
111
|
+
text_file_extensions = %w[.xhtml .html]
|
112
|
+
extension = File.extname(item.href).downcase
|
113
|
+
if text_file_extensions.include?(extension) &&
|
114
|
+
@opf.spine_items.none? { |spine_item| spine_item.idref == item.id }
|
115
|
+
UI.puts " Skipping #{item.href} (not in spine)"
|
116
|
+
next
|
117
|
+
end
|
118
|
+
|
119
|
+
# ignore ncx file
|
120
|
+
if item.media_type == 'application/x-dtbncx+xml'
|
121
|
+
UI.puts " Skipping #{item.href} (ncx file)"
|
122
|
+
next
|
123
|
+
end
|
124
|
+
|
125
|
+
full_path = Pathname.new(@opf_path)
|
126
|
+
.dirname
|
127
|
+
.join(item.href)
|
128
|
+
.to_s
|
129
|
+
|
130
|
+
UI.puts " Exporting #{item.href} (from #{full_path})"
|
131
|
+
|
132
|
+
contents = @zip_file.read(full_path)
|
133
|
+
contents = @encryption_handler.process_file(full_path, contents) if @encryption_handler
|
134
|
+
|
135
|
+
FileUtils.mkdir_p(File.dirname(item.href))
|
136
|
+
File.write(item.href, contents)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|