epuber 0.7.4 → 0.9.0
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 +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
|