nbk2pdf 1.0.1 → 1.1.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/CHANGELOG.md +14 -0
- data/README.md +46 -0
- data/lib/kindle_notebook/cli.rb +26 -2
- data/lib/kindle_notebook/converter.rb +3 -3
- data/lib/kindle_notebook/pdf_renderer.rb +138 -20
- data/lib/kindle_notebook/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2993302368291aeb45035687bc5bdd501beef69c948910e7f173ea6db799383e
|
|
4
|
+
data.tar.gz: 720767d5e65bfef5e979661f09222d4af55bdcc62da60b5527dd556de07c2ea3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91a2ab63c1932539554505defb38810e049adeae2c6c352ca8583d11badd1e5894cfaa6a12adf73656a772a23f33d6bf3609c6e0b2ae48b9aedb2693a9774cb3
|
|
7
|
+
data.tar.gz: 8b28c36986aaefb0a3478c91fe49dbd221ca4e36cb05af45cbd6475f46f2e472cc5564738eab6637207dbab9c9eb22b8d3f6ef0b6e915be659452547a2b08efd
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.1.0] - 2026-10-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `--two-up` prints notebook pages in pairs, side by side on landscape paper,
|
|
12
|
+
with margins so the pages do not touch.
|
|
13
|
+
- Two-up and booklet pages are numbered in light grey; `--no-page-numbers`
|
|
14
|
+
leaves the numbers out.
|
|
15
|
+
- `--booklet` lays notebook pages out to fold into a booklet, printed
|
|
16
|
+
double-sided on the long edge; `--signature` splits it into signatures of
|
|
17
|
+
that many sheets.
|
|
18
|
+
- `--flip-on-edge short` lays a booklet out for a printer that flips on the
|
|
19
|
+
short edge.
|
|
20
|
+
|
|
7
21
|
## [1.0.1] - 2026-10-20
|
|
8
22
|
|
|
9
23
|
### Fixed
|
data/README.md
CHANGED
|
@@ -202,6 +202,11 @@ Options:
|
|
|
202
202
|
-t, --titles FILE JSON/YAML map of notebook uuid to the name you gave it
|
|
203
203
|
--[no-]template Draw the notebook's ruled background (default: yes)
|
|
204
204
|
-p, --paper SIZE Page size: letter (default), a4, native, ...
|
|
205
|
+
-2, --two-up Put notebook pages in pairs side by side on landscape paper
|
|
206
|
+
-b, --booklet Lay notebook pages out to fold into a booklet
|
|
207
|
+
--signature SHEETS Fold the --booklet in signatures of SHEETS sheets each
|
|
208
|
+
--flip-on-edge EDGE Edge the printer flips a --booklet on: long (default) or short
|
|
209
|
+
--[no-]page-numbers Number the pages when --two-up or --booklet (default: yes)
|
|
205
210
|
-a, --annotations Add ink to a PDF as annotations, not drawn into the page
|
|
206
211
|
-q, --quiet Only report errors
|
|
207
212
|
-h, --help Show this message
|
|
@@ -325,6 +330,47 @@ never tells a viewer whether scaling is allowed. If a print dialog greys out its
|
|
|
325
330
|
scale field, that is the viewer's own setting -- in Evince, the manual scale is
|
|
326
331
|
only editable once *Page Scaling* is set to *None*.
|
|
327
332
|
|
|
333
|
+
#### Two pages per sheet
|
|
334
|
+
|
|
335
|
+
`--two-up` puts notebook pages in pairs, side by side, on the paper turned
|
|
336
|
+
landscape. Each page stays portrait and is scaled to fit its half. An 18 pt
|
|
337
|
+
margin is kept around the sheet and between the two pages, so they never
|
|
338
|
+
touch. An odd last page sits alone on the left of its sheet.
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
nbk2pdf --two-up notebook/ # two pages on landscape Letter
|
|
342
|
+
nbk2pdf --two-up --paper a4 notebook/
|
|
343
|
+
nbk2pdf --two-up --paper native notebook/ # a sheet just wide enough for both
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
On Letter each page is scaled to 83% of the device's size.
|
|
347
|
+
|
|
348
|
+
Each page is numbered in light grey, centred under it. `--no-page-numbers`
|
|
349
|
+
leaves the numbers out. Pages printed one per sheet are not numbered.
|
|
350
|
+
|
|
351
|
+
#### Booklets
|
|
352
|
+
|
|
353
|
+
`--booklet` lays the pages out two-up in the order that folds into a booklet.
|
|
354
|
+
Print the file double-sided at 100% scale, flipping on the **long** edge. The
|
|
355
|
+
back of every sheet is drawn upside down so that it comes out the right way up
|
|
356
|
+
from a long-edge duplexer. For a printer that flips on the short edge, pass
|
|
357
|
+
`--flip-on-edge short` and the backs are drawn the right way up.
|
|
358
|
+
|
|
359
|
+
```
|
|
360
|
+
nbk2pdf --booklet notebook/ # every sheet folded together
|
|
361
|
+
nbk2pdf --booklet --signature 4 notebook/ # signatures of 4 sheets, 16 pages
|
|
362
|
+
nbk2pdf --booklet --flip-on-edge short notebook/
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
By default all the sheets are folded together down the middle into one
|
|
366
|
+
booklet, padded to whole sheets with blank pages at the end. A thick notebook
|
|
367
|
+
does not fold well as one stack, so `--signature` splits it into signatures of
|
|
368
|
+
that many sheets each. Fold each signature on its own, then stack them in order
|
|
369
|
+
to sew or staple them. The last signature is padded with blank pages to the
|
|
370
|
+
same thickness as the others, so the spine stays even.
|
|
371
|
+
|
|
372
|
+
Blank pages are not numbered, so the numbers follow the notebook.
|
|
373
|
+
|
|
328
374
|
#### Stroke taper
|
|
329
375
|
|
|
330
376
|
The pen records a 12-bit pressure per point, and it varies a lot inside a single
|
data/lib/kindle_notebook/cli.rb
CHANGED
|
@@ -52,7 +52,7 @@ module KindleNotebook
|
|
|
52
52
|
@out = out
|
|
53
53
|
@err = err
|
|
54
54
|
@device = device
|
|
55
|
-
@options = { template: true, quiet: false, annotations: false }
|
|
55
|
+
@options = { template: true, quiet: false, annotations: false, two_up: false, page_numbers: true }
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Runs the command and returns the process exit status.
|
|
@@ -105,12 +105,34 @@ module KindleNotebook
|
|
|
105
105
|
opts.on("-p", "--paper SIZE", "Page size: letter (default), a4, native, ...") do |size|
|
|
106
106
|
options[:paper] = size
|
|
107
107
|
end
|
|
108
|
+
define_layout_options(opts)
|
|
108
109
|
opts.on("-a", "--annotations", "Add ink to a PDF as annotations, not drawn into the page") do
|
|
109
110
|
options[:annotations] = true
|
|
110
111
|
end
|
|
111
112
|
opts.on("-q", "--quiet", "Only report errors") { options[:quiet] = true }
|
|
112
113
|
end
|
|
113
114
|
|
|
115
|
+
def define_layout_options(opts)
|
|
116
|
+
opts.on("-2", "--two-up", "Put notebook pages in pairs side by side on landscape paper") do
|
|
117
|
+
options[:two_up] = true
|
|
118
|
+
end
|
|
119
|
+
opts.on("-b", "--booklet", "Lay notebook pages out to fold into a booklet") do
|
|
120
|
+
options[:booklet] ||= {}
|
|
121
|
+
end
|
|
122
|
+
opts.on("--signature SHEETS", Integer, "Fold the --booklet in signatures of SHEETS sheets each") do |sheets|
|
|
123
|
+
raise OptionParser::InvalidArgument, "--signature #{sheets}" unless sheets.positive?
|
|
124
|
+
|
|
125
|
+
(options[:booklet] ||= {})[:signature] = sheets
|
|
126
|
+
end
|
|
127
|
+
opts.on("--flip-on-edge EDGE", %w[long short],
|
|
128
|
+
"Edge the printer flips a --booklet on: long (default) or short") do |edge|
|
|
129
|
+
(options[:booklet] ||= {})[:flip] = edge.to_sym
|
|
130
|
+
end
|
|
131
|
+
opts.on("--[no-]page-numbers", "Number the pages when --two-up or --booklet (default: yes)") do |number|
|
|
132
|
+
options[:page_numbers] = number
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
114
136
|
def convert(paths)
|
|
115
137
|
sources = paths.flat_map { |path| Converter.document?(path) ? [File.expand_path(path)] : Notebook.find(path) }
|
|
116
138
|
sources = with_documents(sources)
|
|
@@ -231,7 +253,9 @@ module KindleNotebook
|
|
|
231
253
|
def converter
|
|
232
254
|
@converter ||= Converter.new(titles: titles, render_template: options[:template],
|
|
233
255
|
paper: PdfRenderer.paper(options[:paper]),
|
|
234
|
-
annotations: options[:annotations]
|
|
256
|
+
annotations: options[:annotations],
|
|
257
|
+
two_up: options[:two_up], page_numbers: options[:page_numbers],
|
|
258
|
+
booklet: options[:booklet])
|
|
235
259
|
end
|
|
236
260
|
|
|
237
261
|
def titles = @titles ||= Titles.load(options[:titles])
|
|
@@ -16,10 +16,10 @@ module KindleNotebook
|
|
|
16
16
|
# +titles+ maps a notebook uuid (or book id) to the name you know it by on
|
|
17
17
|
# the device, since standalone notebooks do not store their own title.
|
|
18
18
|
# +annotations+ adds ink to a document as annotations rather than drawing
|
|
19
|
-
# it into the page.
|
|
20
|
-
def initialize(titles: {},
|
|
19
|
+
# it into the page. The remaining options go to PdfRenderer.
|
|
20
|
+
def initialize(titles: {}, annotations: false, **rendering)
|
|
21
21
|
@titles = titles
|
|
22
|
-
@renderer = PdfRenderer.new(
|
|
22
|
+
@renderer = PdfRenderer.new(**rendering)
|
|
23
23
|
@documents = DocumentRenderer.new(annotations: annotations)
|
|
24
24
|
@written = {}
|
|
25
25
|
end
|
|
@@ -44,20 +44,72 @@ module KindleNotebook
|
|
|
44
44
|
# sub-paths instead of one per sample point.
|
|
45
45
|
WIDTH_LEVELS = 8
|
|
46
46
|
|
|
47
|
+
# Two-up sheets keep this much space around and between their pages, so
|
|
48
|
+
# neither page touches the other or the edge of the sheet.
|
|
49
|
+
SHEET_MARGIN = 18
|
|
50
|
+
|
|
51
|
+
# Page numbers sit centred under each page of a two-up sheet, in a band
|
|
52
|
+
# reserved below it.
|
|
53
|
+
NUMBER_SIZE = 8
|
|
54
|
+
NUMBER_BAND = NUMBER_SIZE + 4
|
|
55
|
+
NUMBER_COLOR = "A0A0A0"
|
|
56
|
+
|
|
47
57
|
# +paper+ is a resolved [width, height], or nil for the device's own size.
|
|
48
|
-
|
|
58
|
+
# +two_up+ puts pages in pairs side by side on the paper turned landscape;
|
|
59
|
+
# +page_numbers+ then numbers each of them. +booklet+, when given, is a
|
|
60
|
+
# hash of the options PdfRenderer.booklet takes; it implies +two_up+.
|
|
61
|
+
def initialize(render_template: true, paper: self.class.paper, two_up: false, page_numbers: true,
|
|
62
|
+
booklet: nil)
|
|
49
63
|
@render_template = render_template
|
|
50
64
|
@paper = paper
|
|
65
|
+
@two_up = two_up || !booklet.nil?
|
|
66
|
+
@page_numbers = page_numbers
|
|
67
|
+
@booklet = booklet
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The sides of a booklet's sheets in print order, each as
|
|
71
|
+
# [[left, right], turned] taken from +items+, nil for a blank side of a
|
|
72
|
+
# leaf. All the items are folded together in one signature of whole
|
|
73
|
+
# sheets. Given +signature+, they are split into signatures of that many
|
|
74
|
+
# sheets instead, each folded on its own and stacked in order; the last is
|
|
75
|
+
# padded with blanks to the same thickness as the others, unless
|
|
76
|
+
# everything fits in one, which then shrinks to whole sheets.
|
|
77
|
+
#
|
|
78
|
+
# +flip+ is the edge the printer turns the sheet over on. Flipping on the
|
|
79
|
+
# :long edge lands the back of each sheet upside down, so it is drawn
|
|
80
|
+
# turned and with its halves swapped; on the :short edge it is drawn as is.
|
|
81
|
+
def self.booklet(items, signature: nil, flip: :long)
|
|
82
|
+
whole = ((items.size + 3) / 4) * 4
|
|
83
|
+
capacity = signature ? [signature * 4, whole].min : whole
|
|
84
|
+
(0...items.size).step(capacity).flat_map do |first|
|
|
85
|
+
fold_order(capacity).map { |local| items[first + local] }
|
|
86
|
+
.each_slice(4).flat_map do |front_left, front_right, back_left, back_right|
|
|
87
|
+
back = flip == :short ? [[back_left, back_right], false] : [[back_right, back_left], true]
|
|
88
|
+
[[[front_left, front_right], false], back]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
51
91
|
end
|
|
52
92
|
|
|
93
|
+
# Local page indices of one signature in print order, four per sheet: the
|
|
94
|
+
# front carries the last page on the left and the first on the right, the
|
|
95
|
+
# back the next two in.
|
|
96
|
+
def self.fold_order(capacity)
|
|
97
|
+
(0...(capacity / 2)).step(2).flat_map do |left|
|
|
98
|
+
right = capacity - 1 - left
|
|
99
|
+
[right, left, left + 1, right - 1]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
private_class_method :fold_order
|
|
103
|
+
|
|
53
104
|
# +destination+ may be a path or any IO that responds to #write.
|
|
54
105
|
def render(pages, destination, title: nil)
|
|
55
106
|
raise Error, "notebook has no pages" if pages.empty?
|
|
56
107
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
108
|
+
sides = layout(pages)
|
|
109
|
+
document = new_document(sheet_size(side_page(sides.first, pages)), title)
|
|
110
|
+
sides.each_with_index do |side, index|
|
|
111
|
+
document.start_new_page(size: sheet_size(side_page(side, pages)), margin: 0) unless index.zero?
|
|
112
|
+
draw_side(document, side, side_page(side, pages))
|
|
61
113
|
end
|
|
62
114
|
emit(document, destination)
|
|
63
115
|
end
|
|
@@ -85,9 +137,9 @@ module KindleNotebook
|
|
|
85
137
|
|
|
86
138
|
private
|
|
87
139
|
|
|
88
|
-
def new_document(
|
|
140
|
+
def new_document(size, title)
|
|
89
141
|
Prawn::Document.new(
|
|
90
|
-
page_size:
|
|
142
|
+
page_size: size,
|
|
91
143
|
margin: 0,
|
|
92
144
|
info: {
|
|
93
145
|
Title: title,
|
|
@@ -106,28 +158,94 @@ module KindleNotebook
|
|
|
106
158
|
destination
|
|
107
159
|
end
|
|
108
160
|
|
|
109
|
-
|
|
161
|
+
# Each side of a sheet is [slots, turned], a slot being [page, number], or
|
|
162
|
+
# nil where a booklet leaves a blank.
|
|
163
|
+
def layout(pages)
|
|
164
|
+
slots = pages.each_with_index.map { |page, index| [page, index + 1] }
|
|
165
|
+
return slots.map { |slot| [[slot], false] } unless @two_up
|
|
166
|
+
return slots.each_slice(2).map { |pair| [pair, false] } unless @booklet
|
|
167
|
+
|
|
168
|
+
self.class.booklet(slots, **@booklet)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# The page that sets a side's size. A side left wholly blank takes the size
|
|
172
|
+
# of the notebook's first page.
|
|
173
|
+
def side_page(side, pages) = side.first.compact.first&.first || pages.first
|
|
174
|
+
|
|
175
|
+
# A two-up sheet on the device's own size is just wide enough for its two
|
|
176
|
+
# pages at full size, plus the margins and the band for numbers.
|
|
177
|
+
def sheet_size(page)
|
|
178
|
+
return @paper || [page.width_in_points, page.height_in_points] unless @two_up
|
|
179
|
+
return [@paper.max, @paper.min] if @paper
|
|
180
|
+
|
|
181
|
+
[(page.width_in_points * 2) + (SHEET_MARGIN * 3),
|
|
182
|
+
page.height_in_points + (SHEET_MARGIN * 2) + number_band]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def draw_side(document, side, page)
|
|
186
|
+
return draw_two_up(document, side, page) if @two_up
|
|
187
|
+
|
|
188
|
+
draw_page(document, page, @paper && [0, 0, *@paper])
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def number_band = @page_numbers ? NUMBER_BAND : 0
|
|
192
|
+
|
|
193
|
+
# A pair of pages, left and right, each fitted into its half of the sheet.
|
|
194
|
+
# An odd last page sits alone on the left.
|
|
195
|
+
def draw_two_up(document, (slots, turned), reference)
|
|
196
|
+
sheet_width, sheet_height = sheet_size(reference)
|
|
197
|
+
width = (sheet_width - (SHEET_MARGIN * 3)) / 2.0
|
|
198
|
+
height = sheet_height - (SHEET_MARGIN * 2) - number_band
|
|
199
|
+
upside_down(document, turned, sheet_width, sheet_height) do
|
|
200
|
+
slots.each_with_index do |(page, number), column|
|
|
201
|
+
next if page.nil?
|
|
202
|
+
|
|
203
|
+
box = [SHEET_MARGIN + (column * (width + SHEET_MARGIN)), SHEET_MARGIN + number_band, width, height]
|
|
204
|
+
draw_page(document, page, box)
|
|
205
|
+
draw_number(document, page, box, number) if @page_numbers
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
110
209
|
|
|
111
|
-
def
|
|
210
|
+
def upside_down(document, turned, width, height, &)
|
|
211
|
+
turned ? document.rotate(180, origin: [width / 2.0, height / 2.0], &) : yield
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def draw_number(document, page, box, number)
|
|
215
|
+
scale, x, y = placement(page, box)
|
|
216
|
+
text = number.to_s
|
|
217
|
+
left = x + (((page.width_in_points * scale) - document.width_of(text, size: NUMBER_SIZE)) / 2)
|
|
218
|
+
document.fill_color(NUMBER_COLOR)
|
|
219
|
+
document.draw_text(text, size: NUMBER_SIZE, at: [left, y - NUMBER_BAND])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# +box+ is [x, y, width, height] to fit the page into, or nil to draw it
|
|
223
|
+
# at the device's own size.
|
|
224
|
+
def draw_page(document, page, box)
|
|
112
225
|
document.cap_style(:round)
|
|
113
226
|
document.join_style(:round)
|
|
114
|
-
fitted(document, page) do
|
|
227
|
+
fitted(document, page, box) do
|
|
115
228
|
draw_template(document, page) if @render_template
|
|
116
229
|
page.strokes.each { |stroke| draw_stroke(document, page, stroke) }
|
|
117
230
|
end
|
|
118
231
|
end
|
|
119
232
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def fitted(document, page, &)
|
|
123
|
-
return yield if @paper.nil?
|
|
233
|
+
def fitted(document, page, box, &)
|
|
234
|
+
return yield if box.nil?
|
|
124
235
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
236
|
+
scale, x, y = placement(page, box)
|
|
237
|
+
document.transformation_matrix(scale, 0, 0, scale, x, y, &)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# The canvas is scaled to fit +box+ and centred in it, keeping its
|
|
241
|
+
# proportions, so nothing is stretched or cropped. Answers the scale and
|
|
242
|
+
# where the canvas's lower left corner lands.
|
|
243
|
+
def placement(page, box)
|
|
244
|
+
x, y, width, height = box
|
|
245
|
+
scale = [width / page.width_in_points, height / page.height_in_points].min
|
|
246
|
+
[scale,
|
|
247
|
+
x + ((width - (page.width_in_points * scale)) / 2),
|
|
248
|
+
y + ((height - (page.height_in_points * scale)) / 2)]
|
|
131
249
|
end
|
|
132
250
|
|
|
133
251
|
def draw_template(document, page)
|