nbk2pdf 1.0.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 +7 -0
- data/CHANGELOG.md +58 -0
- data/COPYING +674 -0
- data/README.md +490 -0
- data/exe/nbk2pdf +8 -0
- data/lib/kindle_notebook/cli.rb +274 -0
- data/lib/kindle_notebook/container.rb +82 -0
- data/lib/kindle_notebook/converter.rb +112 -0
- data/lib/kindle_notebook/device.rb +145 -0
- data/lib/kindle_notebook/document.rb +133 -0
- data/lib/kindle_notebook/document_renderer.rb +247 -0
- data/lib/kindle_notebook/ion.rb +243 -0
- data/lib/kindle_notebook/krds.rb +109 -0
- data/lib/kindle_notebook/notebook.rb +442 -0
- data/lib/kindle_notebook/pdf_renderer.rb +232 -0
- data/lib/kindle_notebook/stroke_decoder.rb +90 -0
- data/lib/kindle_notebook/titles.rb +90 -0
- data/lib/kindle_notebook/version.rb +10 -0
- data/lib/nbk2pdf.rb +22 -0
- metadata +109 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Sergey Avseyev
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require "prawn"
|
|
6
|
+
|
|
7
|
+
module KindleNotebook
|
|
8
|
+
# Draws decoded pages as vector graphics. Canvas units are sub-pixels at the
|
|
9
|
+
# page's normalised PPI (2520 on a Scribe); PDF user space is points, and its
|
|
10
|
+
# Y axis points the other way, so every coordinate is scaled and flipped.
|
|
11
|
+
class PdfRenderer
|
|
12
|
+
HIGHLIGHTER_ALPHA = 0.2
|
|
13
|
+
MIN_LINE_WIDTH = 0.15
|
|
14
|
+
|
|
15
|
+
# Pages go on US Letter unless asked otherwise, since the size the device
|
|
16
|
+
# records matches no standard sheet and print dialogs then offer to scale.
|
|
17
|
+
DEFAULT_PAPER = "letter"
|
|
18
|
+
|
|
19
|
+
# Asking for this size keeps each page exactly as the device recorded it.
|
|
20
|
+
NATIVE_PAPER = "native"
|
|
21
|
+
|
|
22
|
+
# Resolves a paper name to [width, height] in points, or nil for the size
|
|
23
|
+
# the notebook itself records. No name at all means the default.
|
|
24
|
+
def self.paper(name = nil)
|
|
25
|
+
name = DEFAULT_PAPER if name.nil?
|
|
26
|
+
return nil if name.to_s.casecmp(NATIVE_PAPER).zero?
|
|
27
|
+
|
|
28
|
+
PDF::Core::PageGeometry::SIZES[name.to_s.upcase] ||
|
|
29
|
+
raise(Error, "unknown paper size #{name}; use #{NATIVE_PAPER} or one of " \
|
|
30
|
+
"#{PDF::Core::PageGeometry::SIZES.keys.sort.join(", ")}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The pen reports a 12-bit pressure per point and the device narrows the
|
|
34
|
+
# line where the pen was lighter. The curve is not documented, so it was
|
|
35
|
+
# fitted against the thumbnails the device renders for the same pages; the
|
|
36
|
+
# fit says width follows how pressure varies *within* a stroke rather than
|
|
37
|
+
# its absolute level, since the stroke's overall width is already carried by
|
|
38
|
+
# nmdl.thickness. See "Stroke taper" in the README for the measurements.
|
|
39
|
+
PRESSURE_MAX = 4095
|
|
40
|
+
TAPER_SPAN = 1.15
|
|
41
|
+
MIN_WIDTH_FRACTION = 0.15
|
|
42
|
+
|
|
43
|
+
# Widths are rounded to this many steps so a stroke becomes a handful of
|
|
44
|
+
# sub-paths instead of one per sample point.
|
|
45
|
+
WIDTH_LEVELS = 8
|
|
46
|
+
|
|
47
|
+
# +paper+ is a resolved [width, height], or nil for the device's own size.
|
|
48
|
+
def initialize(render_template: true, paper: self.class.paper)
|
|
49
|
+
@render_template = render_template
|
|
50
|
+
@paper = paper
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# +destination+ may be a path or any IO that responds to #write.
|
|
54
|
+
def render(pages, destination, title: nil)
|
|
55
|
+
raise Error, "notebook has no pages" if pages.empty?
|
|
56
|
+
|
|
57
|
+
document = new_document(pages.first, title)
|
|
58
|
+
pages.each_with_index do |page, index|
|
|
59
|
+
document.start_new_page(size: page_size(page), margin: 0) unless index.zero?
|
|
60
|
+
draw_page(document, page)
|
|
61
|
+
end
|
|
62
|
+
emit(document, destination)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Draws one stroke in the page's own coordinates, points with the origin at
|
|
66
|
+
# the bottom left. +document+ is a Prawn document, or anything that answers
|
|
67
|
+
# the same few drawing calls.
|
|
68
|
+
def draw_stroke(document, page, stroke)
|
|
69
|
+
scale = POINTS_PER_INCH / page.ppi
|
|
70
|
+
nominal = [stroke.thickness * scale, MIN_LINE_WIDTH].max
|
|
71
|
+
points = stroke_points(page, stroke, scale)
|
|
72
|
+
|
|
73
|
+
document.stroke_color(rgb_hex(stroke.color))
|
|
74
|
+
document.fill_color(rgb_hex(stroke.color))
|
|
75
|
+
|
|
76
|
+
with_alpha(document, stroke_alpha(stroke)) do
|
|
77
|
+
if stroke.dot?
|
|
78
|
+
document.line_width(nominal)
|
|
79
|
+
document.fill_circle(points.first, nominal / 2)
|
|
80
|
+
else
|
|
81
|
+
draw_runs(document, points, segment_widths(stroke, nominal))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def new_document(first_page, title)
|
|
89
|
+
Prawn::Document.new(
|
|
90
|
+
page_size: page_size(first_page),
|
|
91
|
+
margin: 0,
|
|
92
|
+
info: {
|
|
93
|
+
Title: title,
|
|
94
|
+
Creator: "#{PROGRAM} #{VERSION}",
|
|
95
|
+
Producer: "#{PROGRAM} #{VERSION}"
|
|
96
|
+
}.compact
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def emit(document, destination)
|
|
101
|
+
if destination.respond_to?(:write)
|
|
102
|
+
destination.write(document.render)
|
|
103
|
+
else
|
|
104
|
+
document.render_file(destination)
|
|
105
|
+
end
|
|
106
|
+
destination
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def page_size(page) = @paper || [page.width_in_points, page.height_in_points]
|
|
110
|
+
|
|
111
|
+
def draw_page(document, page)
|
|
112
|
+
document.cap_style(:round)
|
|
113
|
+
document.join_style(:round)
|
|
114
|
+
fitted(document, page) do
|
|
115
|
+
draw_template(document, page) if @render_template
|
|
116
|
+
page.strokes.each { |stroke| draw_stroke(document, page, stroke) }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# On a chosen paper size the canvas is scaled to fit and centred, keeping
|
|
121
|
+
# its proportions, so nothing is stretched or cropped.
|
|
122
|
+
def fitted(document, page, &)
|
|
123
|
+
return yield if @paper.nil?
|
|
124
|
+
|
|
125
|
+
width = page.width_in_points
|
|
126
|
+
height = page.height_in_points
|
|
127
|
+
scale = [@paper.first / width, @paper.last / height].min
|
|
128
|
+
document.transformation_matrix(scale, 0, 0, scale,
|
|
129
|
+
(@paper.first - (width * scale)) / 2,
|
|
130
|
+
(@paper.last - (height * scale)) / 2, &)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def draw_template(document, page)
|
|
134
|
+
template = page.template
|
|
135
|
+
return if template.nil? || template.empty?
|
|
136
|
+
return if template.width <= 0 || template.height <= 0
|
|
137
|
+
|
|
138
|
+
# A template draws itself in its own space, which is stretched onto the page.
|
|
139
|
+
scale = [page.width_in_points / template.width, page.height_in_points / template.height]
|
|
140
|
+
template.shapes.each { |shape| draw_shape(document, page, shape, scale) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def draw_shape(document, page, shape, scale)
|
|
144
|
+
alpha = alpha_of(shape.color)
|
|
145
|
+
return if alpha.zero?
|
|
146
|
+
|
|
147
|
+
scale_x, scale_y = scale
|
|
148
|
+
height = page.height_in_points
|
|
149
|
+
document.stroke_color(rgb_hex(shape.color))
|
|
150
|
+
document.line_width([shape.width * scale_x, MIN_LINE_WIDTH].max)
|
|
151
|
+
with_alpha(document, alpha) do
|
|
152
|
+
shape.segments.each do |segment|
|
|
153
|
+
polyline(document, segment.map { |x, y| [x * scale_x, height - (y * scale_y)] })
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# One sub-path per run of equal width. Runs share their junction point, so
|
|
159
|
+
# with round caps and joins the seam between them is invisible.
|
|
160
|
+
def draw_runs(document, points, widths)
|
|
161
|
+
index = 0
|
|
162
|
+
while index < widths.size
|
|
163
|
+
width = widths[index]
|
|
164
|
+
last = index
|
|
165
|
+
last += 1 while last + 1 < widths.size && widths[last + 1] == width
|
|
166
|
+
|
|
167
|
+
document.line_width(width)
|
|
168
|
+
polyline(document, points[index..(last + 1)])
|
|
169
|
+
index = last + 1
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# One width per segment, taken from the pressure at the segment's start and
|
|
174
|
+
# measured against the average pressure of the stroke it belongs to.
|
|
175
|
+
def segment_widths(stroke, nominal)
|
|
176
|
+
count = stroke.xs.size - 1
|
|
177
|
+
levels = taper_levels(stroke, count)
|
|
178
|
+
return Array.new(count, nominal) unless levels
|
|
179
|
+
|
|
180
|
+
mean = levels.sum / levels.size
|
|
181
|
+
levels.map { |level| width_for(level - mean, nominal) }
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The square root maps the raw reading onto something nearer to how wide the
|
|
185
|
+
# nib sits at that pressure.
|
|
186
|
+
#
|
|
187
|
+
# Only the pen tapers. The highlighter records pressure too, but it barely
|
|
188
|
+
# varies along a stroke while differing a lot between strokes, so following
|
|
189
|
+
# it would give each highlighter band an arbitrary thickness rather than the
|
|
190
|
+
# fixed one the device draws.
|
|
191
|
+
def taper_levels(stroke, count)
|
|
192
|
+
pressures = stroke.pressures
|
|
193
|
+
return nil if stroke.highlighter?
|
|
194
|
+
return nil unless pressures && pressures.size == stroke.xs.size
|
|
195
|
+
|
|
196
|
+
pressures.first(count).map { |p| Math.sqrt(p.clamp(0, PRESSURE_MAX) / PRESSURE_MAX.to_f) }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def width_for(offset, nominal)
|
|
200
|
+
factor = 1.0 + (TAPER_SPAN * offset)
|
|
201
|
+
factor = (factor * WIDTH_LEVELS).round / WIDTH_LEVELS.to_f
|
|
202
|
+
[nominal * factor, nominal * MIN_WIDTH_FRACTION, MIN_LINE_WIDTH].max
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def stroke_points(page, stroke, scale)
|
|
206
|
+
height = page.height_in_points
|
|
207
|
+
stroke.xs.each_with_index.map { |x, index| [x * scale, height - (stroke.ys[index] * scale)] }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def stroke_alpha(stroke) = stroke.highlighter? ? HIGHLIGHTER_ALPHA : alpha_of(stroke.color)
|
|
211
|
+
|
|
212
|
+
def polyline(document, points)
|
|
213
|
+
document.move_to(points.first)
|
|
214
|
+
points.drop(1).each { |point| document.line_to(point) }
|
|
215
|
+
document.stroke
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def with_alpha(document, alpha, &)
|
|
219
|
+
alpha < 1.0 ? document.transparent(alpha, &) : yield
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# A colour is a packed integer; the alpha byte is only meaningful when the
|
|
223
|
+
# value actually carries one (templates do, pen colours do not).
|
|
224
|
+
def alpha_of(color)
|
|
225
|
+
return 1.0 if color <= 0xFF_FFFF
|
|
226
|
+
|
|
227
|
+
((color >> 24) & 0xFF) / 255.0
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def rgb_hex(color) = format("%06X", color & 0xFF_FFFF)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Sergey Avseyev
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
module KindleNotebook
|
|
6
|
+
# Every per-point array inside a stroke (x, y, pressure, tilt, ...) is stored
|
|
7
|
+
# as a second-order delta stream: the firmware records the change *of the
|
|
8
|
+
# change* between successive samples, which stays close to zero for a smooth
|
|
9
|
+
# pen movement and therefore packs into very few bits.
|
|
10
|
+
#
|
|
11
|
+
# [0x01 0x01] magic
|
|
12
|
+
# [uint32-LE] number of encoded values
|
|
13
|
+
# [nibble stream] one 4-bit instruction per value, two per byte
|
|
14
|
+
# [operand bytes] variable-width operands referenced by the nibbles
|
|
15
|
+
#
|
|
16
|
+
# Instruction nibble layout:
|
|
17
|
+
#
|
|
18
|
+
# bits 0-1 (n) operand width: 0 = none, 1..3 = that many little-endian bytes
|
|
19
|
+
# bit 2 the increment is +n itself; no operand bytes are consumed
|
|
20
|
+
# bit 3 negate the increment
|
|
21
|
+
module StrokeDecoder
|
|
22
|
+
MAGIC = "\x01\x01".b
|
|
23
|
+
HEADER_SIZE = 6
|
|
24
|
+
|
|
25
|
+
class DecodeError < Error; end
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def decode(data, count)
|
|
30
|
+
return [] if count <= 0
|
|
31
|
+
|
|
32
|
+
data = data.to_s.b
|
|
33
|
+
raise DecodeError, "expected #{count} values in a #{data.bytesize}-byte blob" if data.bytesize < HEADER_SIZE
|
|
34
|
+
unless data.byteslice(0, 2) == MAGIC
|
|
35
|
+
raise DecodeError, "bad stroke signature #{data.byteslice(0, 2).unpack1("H*")}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
instructions, operand_offset = read_instructions(data, count)
|
|
39
|
+
accumulate(data, instructions, operand_offset, count)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def read_instructions(data, count)
|
|
43
|
+
nibbles = []
|
|
44
|
+
offset = HEADER_SIZE
|
|
45
|
+
while nibbles.size < count && offset < data.bytesize
|
|
46
|
+
byte = data.getbyte(offset)
|
|
47
|
+
offset += 1
|
|
48
|
+
nibbles << (byte >> 4) << (byte & 0x0F)
|
|
49
|
+
end
|
|
50
|
+
nibbles.pop if nibbles.size > count
|
|
51
|
+
[nibbles, offset]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def accumulate(data, instructions, offset, count)
|
|
55
|
+
values = Array.new(count)
|
|
56
|
+
change = 0
|
|
57
|
+
value = 0
|
|
58
|
+
|
|
59
|
+
count.times do |index|
|
|
60
|
+
instruction = instructions[index] || 0
|
|
61
|
+
width = instruction & 0x03
|
|
62
|
+
increment, offset =
|
|
63
|
+
if instruction.anybits?(0x04)
|
|
64
|
+
[width, offset]
|
|
65
|
+
else
|
|
66
|
+
read_operand(data, offset, width)
|
|
67
|
+
end
|
|
68
|
+
increment = -increment if instruction.anybits?(0x08)
|
|
69
|
+
|
|
70
|
+
if index.zero?
|
|
71
|
+
change = 0
|
|
72
|
+
value = increment
|
|
73
|
+
else
|
|
74
|
+
change += increment
|
|
75
|
+
value += change
|
|
76
|
+
end
|
|
77
|
+
values[index] = value
|
|
78
|
+
end
|
|
79
|
+
values
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def read_operand(data, offset, width)
|
|
83
|
+
return [0, offset] if width.zero?
|
|
84
|
+
raise DecodeError, "stroke stream truncated" if offset + width > data.bytesize
|
|
85
|
+
|
|
86
|
+
value = data.byteslice(offset, width).each_byte.with_index.sum { |b, i| b << (8 * i) }
|
|
87
|
+
[value, offset + width]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Sergey Avseyev
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module KindleNotebook
|
|
8
|
+
# The names you gave your notebooks on the device.
|
|
9
|
+
#
|
|
10
|
+
# A Kindle keeps them on its system partition, which it does not share over
|
|
11
|
+
# MTP, and they are not in the notebook files either, so they have to be
|
|
12
|
+
# supplied once by hand. Notebooks that still have no name can be appended to
|
|
13
|
+
# the file for filling in, which is what `sync` does.
|
|
14
|
+
class Titles
|
|
15
|
+
HEADER = "# Names for your Kindle notebooks, which the device does not share over MTP.\n" \
|
|
16
|
+
"# Fill in a name between the quotes and it will be used from then on."
|
|
17
|
+
private_constant :HEADER
|
|
18
|
+
|
|
19
|
+
attr_reader :path
|
|
20
|
+
|
|
21
|
+
def self.load(path)
|
|
22
|
+
return new if path.nil?
|
|
23
|
+
return new(path: path) unless File.file?(path)
|
|
24
|
+
|
|
25
|
+
new(path: path, names: parse(File.read(path), path))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.parse(text, path)
|
|
29
|
+
data = json?(path) ? JSON.parse(text) : yaml(text)
|
|
30
|
+
raise Error, "#{path}: a title map must be a mapping of uuid to name" unless data.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
data.transform_keys(&:to_s).transform_values(&:to_s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.yaml(text)
|
|
36
|
+
require "yaml"
|
|
37
|
+
YAML.safe_load(text, permitted_classes: [], aliases: false) || {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.json?(path) = File.extname(path).casecmp(".json").zero?
|
|
41
|
+
|
|
42
|
+
def initialize(path: nil, names: {})
|
|
43
|
+
@path = path
|
|
44
|
+
@names = names
|
|
45
|
+
@missing = {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def [](key) = @names[key]
|
|
49
|
+
|
|
50
|
+
# True once a uuid appears in the file at all, even without a name yet, so
|
|
51
|
+
# it is not offered for filling in twice.
|
|
52
|
+
def known?(uuid) = @names.key?(uuid)
|
|
53
|
+
|
|
54
|
+
# Remembers a notebook that has no name, with a note to recognise it by.
|
|
55
|
+
def note_missing(uuid, note)
|
|
56
|
+
@missing[uuid] ||= note unless known?(uuid)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def missing = @missing.size
|
|
60
|
+
|
|
61
|
+
# Adds the remembered notebooks to the file, and returns how many.
|
|
62
|
+
def record_missing
|
|
63
|
+
return 0 if @path.nil? || @missing.empty?
|
|
64
|
+
|
|
65
|
+
self.class.json?(@path) ? write_json : append_yaml
|
|
66
|
+
count = @missing.size
|
|
67
|
+
@missing.each_key { |uuid| @names[uuid] = "" }
|
|
68
|
+
@missing.clear
|
|
69
|
+
count
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def append_yaml
|
|
75
|
+
File.open(@path, "a") do |file|
|
|
76
|
+
file.puts(HEADER) unless File.size?(@path)
|
|
77
|
+
@missing.each do |uuid, note|
|
|
78
|
+
file.puts
|
|
79
|
+
file.puts("# #{note}")
|
|
80
|
+
file.puts(%(#{uuid}: ""))
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def write_json
|
|
86
|
+
merged = @names.merge(@missing.transform_values { "" })
|
|
87
|
+
File.write(@path, "#{JSON.pretty_generate(merged)}\n")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/nbk2pdf.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 Sergey Avseyev
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# Reads Kindle Scribe notebooks (`nbk` files) and renders them as vector PDFs.
|
|
6
|
+
module KindleNotebook
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require "kindle_notebook/version"
|
|
11
|
+
require "kindle_notebook/container"
|
|
12
|
+
require "kindle_notebook/ion"
|
|
13
|
+
require "kindle_notebook/stroke_decoder"
|
|
14
|
+
require "kindle_notebook/krds"
|
|
15
|
+
require "kindle_notebook/notebook"
|
|
16
|
+
require "kindle_notebook/document"
|
|
17
|
+
require "kindle_notebook/pdf_renderer"
|
|
18
|
+
require "kindle_notebook/document_renderer"
|
|
19
|
+
require "kindle_notebook/titles"
|
|
20
|
+
require "kindle_notebook/device"
|
|
21
|
+
require "kindle_notebook/converter"
|
|
22
|
+
require "kindle_notebook/cli"
|
metadata
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: nbk2pdf
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sergey Avseyev
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: combine_pdf
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: prawn
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.5'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.5'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: sqlite3
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
description: |
|
|
55
|
+
Reads the `nbk` files a Kindle Scribe writes and renders them as vector PDFs,
|
|
56
|
+
complete with the ruled or squared background the notes were written on,
|
|
57
|
+
and adds what was written on a PDF on the device to a copy of that PDF.
|
|
58
|
+
|
|
59
|
+
Includes a reader for the signature container the device wraps its SQLite
|
|
60
|
+
database in, a binary Amazon Ion parser, and a decoder for the second-order
|
|
61
|
+
delta encoding the firmware stores pen strokes in. Ships the `nbk2pdf`
|
|
62
|
+
command line tool.
|
|
63
|
+
email:
|
|
64
|
+
- sergey.avseyev@gmail.com
|
|
65
|
+
executables:
|
|
66
|
+
- nbk2pdf
|
|
67
|
+
extensions: []
|
|
68
|
+
extra_rdoc_files: []
|
|
69
|
+
files:
|
|
70
|
+
- CHANGELOG.md
|
|
71
|
+
- COPYING
|
|
72
|
+
- README.md
|
|
73
|
+
- exe/nbk2pdf
|
|
74
|
+
- lib/kindle_notebook/cli.rb
|
|
75
|
+
- lib/kindle_notebook/container.rb
|
|
76
|
+
- lib/kindle_notebook/converter.rb
|
|
77
|
+
- lib/kindle_notebook/device.rb
|
|
78
|
+
- lib/kindle_notebook/document.rb
|
|
79
|
+
- lib/kindle_notebook/document_renderer.rb
|
|
80
|
+
- lib/kindle_notebook/ion.rb
|
|
81
|
+
- lib/kindle_notebook/krds.rb
|
|
82
|
+
- lib/kindle_notebook/notebook.rb
|
|
83
|
+
- lib/kindle_notebook/pdf_renderer.rb
|
|
84
|
+
- lib/kindle_notebook/stroke_decoder.rb
|
|
85
|
+
- lib/kindle_notebook/titles.rb
|
|
86
|
+
- lib/kindle_notebook/version.rb
|
|
87
|
+
- lib/nbk2pdf.rb
|
|
88
|
+
licenses:
|
|
89
|
+
- GPL-3.0-or-later
|
|
90
|
+
metadata:
|
|
91
|
+
rubygems_mfa_required: 'true'
|
|
92
|
+
rdoc_options: []
|
|
93
|
+
require_paths:
|
|
94
|
+
- lib
|
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: 3.1.0
|
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '0'
|
|
105
|
+
requirements: []
|
|
106
|
+
rubygems_version: 4.0.22
|
|
107
|
+
specification_version: 4
|
|
108
|
+
summary: Convert Kindle Scribe notebooks into vector PDFs
|
|
109
|
+
test_files: []
|