1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306 | """Writes one document twice — spelled out, then packed into object streams —
and draws a page stating what each of them weighs.
The Python twin of the `write_object_streams` example in Rust. A PDF is mostly
dictionaries: the catalogue, the page tree, every page, every font, every
annotation. None of them is a stream, so none of them is compressed, and each is
written out in full. Object streams gather them into streams that compress
together, and the table saying where everything is becomes a stream as well.
The document being weighed is built here and written both ways. The page that
reports the two weights is itself written packed, so the file this leaves behind
is one of the two things it is talking about.
The words are held in `Words`, once per language, and `HQF_PDF_LANG` picks which set
is drawn. The numbers are not language: they are counted off the two files.
A second path, if one is given, is where the document that was weighed is written
spelled out, so that the two spellings of one document can be handed to a reader
side by side.
Usage: python examples/write_object_streams.py [out.pdf] [spelled_out.pdf]
HQF_PDF_LANG=fr python examples/write_object_streams.py
"""
from __future__ import annotations
import sys
from dataclasses import dataclass
from pathlib import Path
import _language
import _licence
import _out
import hqf_pdf
# Where the committed fonts sit.
FONT_DIR = _out.ROOT / "crates" / "hqf-pdf" / "tests" / "fonts"
# How many pages the document being weighed holds.
#
# Every page is a dictionary, and so is everything hanging off it, which is exactly what
# packing gathers up: a document of one page would show almost nothing.
WEIGHED_PAGES = 24
@dataclass(frozen=True)
class Words:
"""Every word the page draws, in one language.
What is not language stays out of it: the three weights are counted off the files
themselves.
"""
# The line across the top of the page.
title: str
# What packing does, in two sentences.
why: str
# The heading over the three weights.
weights: str
# What the document being weighed is, cut where the page count goes in.
subject_before: str
subject_after: str
# The label on the weight of the document spelled out.
flat: str
# The label on the weight of the same document packed.
packed: str
# The label on what was saved.
saved: str
# What a weight is counted in.
unit: str
# The closing note, cut where the share saved goes into it.
note_before: str
note_after: str
# The line saying what this very file is.
itself: str
def subject(self) -> str:
"""What the document being weighed is."""
return f"{self.subject_before}{WEIGHED_PAGES}{self.subject_after}"
def note(self, share: int) -> str:
"""The closing note, with the share saved in it."""
return f"{self.note_before}{share}{self.note_after}"
# The page in English.
ENGLISH = Words(
title="What packing a document saves",
why=(
"A PDF is mostly dictionaries — the catalogue, the page tree, every "
"page, every font, every annotation — and a dictionary is not a stream, "
"so each one is written out in full and none of them is compressed.\n"
"Object streams gather them into streams that are compressed together, "
"and the table stating where every object lives becomes a stream too. "
"The document a reader sees is the same one either way."
),
weights="The same document, weighed twice",
subject_before="The document weighed here holds ",
subject_after=" pages of text, each with a heading of its own.",
flat="Spelled out",
packed="Packed",
saved="Saved",
unit="bytes",
note_before="Packing this document saved ",
note_after=(
" per cent of it. What it saves goes up with the number of "
"pages, because a page costs a handful of dictionaries and "
"dictionaries are what packing compresses."
),
itself=(
"This very file is packed, so a reader that opened it has read one "
"of the two documents being weighed."
),
)
# The page in French.
FRENCH = Words(
title="Ce que le rangement d'un document fait gagner",
why=(
"Un PDF est surtout fait de dictionnaires — le catalogue, l'arbre des "
"pages, chaque page, chaque police, chaque annotation — et un "
"dictionnaire n'est pas un flux : chacun est donc écrit en toutes "
"lettres, et aucun n'est compressé.\n"
"Les flux d'objets les rassemblent dans des flux compressés ensemble, "
"et la table qui dit où vit chaque objet devient elle aussi un flux. Le "
"document que le lecteur voit est le même dans les deux cas."
),
weights="Le même document, pesé deux fois",
subject_before="Le document pesé ici tient en ",
subject_after=" pages de texte, chacune avec son propre titre.",
flat="En toutes lettres",
packed="Rangé",
saved="Gagné",
unit="octets",
note_before="Ranger ce document en a fait gagner ",
note_after=(
" pour cent. Le gain monte avec le nombre de pages, parce "
"qu'une page coûte une poignée de dictionnaires et que ce sont "
"les dictionnaires que le rangement compresse."
),
itself=(
"Ce fichier-ci est rangé : un lecteur qui l'a ouvert a donc lu l'un "
"des deux documents pesés."
),
)
# Every language the example is written in. A language is added by writing its own set
# of words and naming it here.
WORDS = {_language.ENGLISH: ENGLISH, _language.FRENCH: FRENCH}
# The left edge of everything on the page.
X = 60.0
# The width every block is broken to.
ROOM = 475.0
# The baseline the title sits on.
TITLE_TOP = 780.0
# The top of the block that says what packing does.
WHY_TOP = 748.0
# The top of the line naming what was weighed.
SUBJECT_TOP = 636.0
# The top of the heading over the three weights.
WEIGHTS_TOP = 600.0
# The top of the first of the three weights.
FIRST_WEIGHT_TOP = 574.0
# How far apart two weights sit.
WEIGHT_DROP = 20.0
# Where the number of a weight begins, its label sitting at X.
NUMBER_X = X + 160.0
# The top of the closing note.
NOTE_TOP = 486.0
# The top of the line saying what this file is.
ITSELF_TOP = 424.0
def weighed(font: hqf_pdf.Font) -> hqf_pdf.Document:
"""The document whose two weights the page reports: pages of text, each under a
heading of its own."""
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
handle = document.add_font(font)
for number in range(1, WEIGHED_PAGES + 1):
content = hqf_pdf.Content()
heading = hqf_pdf.TextFlow(handle, 16.0)
title = f"Chapter {number}"
heading.draw(content, heading.break_lines(title, ROOM), X, 760.0, ROOM)
body = hqf_pdf.TextFlow(handle, 11.0, leading=15.0)
prose = (
f"This is page {number} of a document written twice over, so that "
"what packing saves can be weighed rather than claimed."
)
body.draw(content, body.break_lines(prose, ROOM), X, 720.0, ROOM)
page = hqf_pdf.Page.a4()
page.set_content(content)
document.add_page(page)
return document
def counted(value: int) -> str:
"""A count of bytes, its thousands held apart by a space."""
digits = str(value)
grouped = ""
for index, digit in enumerate(digits):
if index > 0 and (len(digits) - index) % 3 == 0:
grouped += " "
grouped += digit
return grouped
def share_saved(flat: int, packed: int) -> int:
"""The share of a document packing saved, in whole per cent.
Whole numbers throughout: a share written as a fraction would be spelled by
whatever language drew it, and the two twins must write the same bytes.
"""
if flat == 0:
return 0
return (flat - packed) * 100 // flat
def main() -> None:
language = _language.from_environment()
words = _language.words_of(WORDS, language)
out = _out.output_path(
Path(_language.file_name("object_streams.pdf", language)).stem
)
font = hqf_pdf.Font.from_path(FONT_DIR / "DejaVuSans.ttf")
spelled_out = weighed(font).to_bytes()
flat = len(spelled_out)
packed_document = weighed(font)
packed_document.set_object_streams(True)
packed = len(packed_document.to_bytes())
if len(sys.argv) > 2:
beside = Path(sys.argv[2])
beside.parent.mkdir(parents=True, exist_ok=True)
beside.write_bytes(spelled_out)
print(f"wrote {beside}: {flat} bytes")
document = hqf_pdf.Document()
document.set_license(_licence.licensed())
document.set_object_streams(True)
handle = document.add_font(font)
content = hqf_pdf.Content()
title = hqf_pdf.TextFlow(handle, 18.0)
title.draw(content, title.break_lines(words.title, ROOM), X, TITLE_TOP, ROOM)
why = hqf_pdf.TextFlow(handle, 10.0, leading=14.0)
why.draw(content, why.break_lines(words.why, ROOM), X, WHY_TOP, ROOM)
subject = hqf_pdf.TextFlow(handle, 10.0, leading=14.0)
subject.draw(
content, subject.break_lines(words.subject(), ROOM), X, SUBJECT_TOP, ROOM
)
heading = hqf_pdf.TextFlow(handle, 11.0)
heading.draw(
content, heading.break_lines(words.weights, ROOM), X, WEIGHTS_TOP, ROOM
)
rows = ((words.flat, flat), (words.packed, packed), (words.saved, flat - packed))
for index, (label, weight) in enumerate(rows):
top = FIRST_WEIGHT_TOP - WEIGHT_DROP * float(index)
line = hqf_pdf.TextFlow(handle, 10.0)
line.draw(content, line.break_lines(label, ROOM), X, top, ROOM)
stated = f"{counted(weight)} {words.unit}"
number = hqf_pdf.TextFlow(handle, 10.0)
number.draw(content, number.break_lines(stated, ROOM), NUMBER_X, top, ROOM)
note = words.note(share_saved(flat, packed))
closing = hqf_pdf.TextFlow(handle, 10.0, leading=14.0)
closing.draw(content, closing.break_lines(note, ROOM), X, NOTE_TOP, ROOM)
itself = hqf_pdf.TextFlow(handle, 10.0, leading=14.0)
itself.draw(content, itself.break_lines(words.itself, ROOM), X, ITSELF_TOP, ROOM)
page = hqf_pdf.Page.a4()
page.set_content(content)
document.add_page(page)
written = document.write(out)
print(f"wrote {out}: {written} bytes")
if __name__ == "__main__":
main()
|