-
Notifications
You must be signed in to change notification settings - Fork 2
/
build.py
339 lines (243 loc) · 10.2 KB
/
build.py
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
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Render PDF files needed for printing labels.
#
#
import sys, os, csv, PIL, pdb, re, click
import logging
from io import BytesIO
from PIL import Image
from binascii import b2a_hex, a2b_hex
from collections import Counter
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.units import inch, cm
#from reportlab.graphics import renderPDF
#from reportlab.graphics.shapes import Drawing
from reportlab.pdfbase import pdfdoc
from reportlab.lib import colors
from reportlab import rl_config
# just for fonts
from reportlab.lib.fonts import addMapping
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
from pdfrw.toreportlab import makerl
# These config values allow us to see text in a plain form in resulting PDF
# - see pp/reportlab/rl_settings.py
rl_config.useA85 = 0
rl_config.invariant = 1
rl_config.pageCompression = 0
# These very-specific text values are matched on the Coldcard; cannot be changed.
class placeholders:
addr = 'ADDRESS_XXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 37 long
privkey = 'PRIVKEY_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' # 51 long
# rather than Tokyo, I chose Chiba Prefecture in ShiftJIS encoding...
header = b'%PDF-1.3\n%\x90\xe7\x97t\x8c\xa7 Coldcard Paper Wallet Template\n'
# Control the first few bytes of the file ... still valid PDF file tho.
# - had to monkey-patch in this change
# - see pp/reportlab/pdfbase/pdfdoc.py PDFFile() class
class myPDFFile(pdfdoc.PDFFile):
def __init__(self,unused):
self.strings = []
self.write = self.strings.append
self.offset = 0
self.add(placeholders.header)
pdfdoc.PDFFile = myPDFFile
class TemplateBuilder(object):
def __init__(self, input_template, output_fname=None, canvas=None):
pages = PdfReader(input_template).pages
self.xobjs = [(pagexobj(x),) for x in pages]
assert len(pages) == 1, "only supporting a single page"
if output_fname:
# probably an object, not filename, but whatevers
self.canvas = Canvas(output_fname)
else:
self.canvas = canvas
assert self.canvas, 'cant write?'
self.qr_set = set()
def insert_values(self, page_num, *values):
raise NotImplemented
def make_custom(self, template_name):
c = self.canvas
for page_num, xobjlist in enumerate(self.xobjs):
x = y = 0
for xobj in xobjlist:
x += xobj.BBox[2]
y = max(y, xobj.BBox[3])
c.setPageSize((x,y))
x,y = 0,0
# render background template data
for xobj in xobjlist:
c.saveState()
c.translate(x, y)
c.doForm(makerl(c, xobj))
c.restoreState()
# put our data on "top"
self.insert_values(page_num, template_name)
x += xobj.BBox[2]
c.showPage()
def make_image_page(self, img, label=None, width=4*inch, height=6*inch, footnote=None):
'''
Whole page is one raster image. XXX untested
'''
c = self.canvas
c.setPageSize((width, height))
from reportlab.lib.utils import ImageReader
X_SHIFT = 0
Y_SHIFT = -0.120 * inch
# paste in the image
c.drawImage(ImageReader(img, ident=str(label)), X_SHIFT,Y_SHIFT,
width=width, height=height, preserveAspectRatio=True)
c.showPage()
def simple_text(self, msg, x=1*inch, y=1*inch):
# draw a single line of simple stuff
c = self.canvas
c.saveState()
c.setFillColorRGB(0,0,0)
c.setStrokeColorRGB(0,0,0)
c.setFont("Courier-Bold", 6)
# centered horizontally at target spot
#c.drawCentredString(x, y, msg)
# right-justified
c.drawRightString(x, y, msg)
c.restoreState()
def finalize(self):
c = self.canvas
c.setTitle("Paper Wallet template for Coldcard")
c.setAuthor("Templator")
c.setCreator("Templator")
c.setProducer("Templator")
self.canvas.save()
class WalletBuilder(TemplateBuilder):
def insert_values(self, page_num, template_name):
if template_name == 'placeholder':
self.add_qr_spot('addr', placeholders.addr, 1.5*inch, 3.75*inch)
self.add_qr_spot('pk', placeholders.privkey, 6.75*inch, 3.75*inch)
self.add_qr_spot('pk', placeholders.privkey, 6.75*inch, 1*inch, inch)
for i in range(3):
self.add_qr_spot('addr', None, (0.75*inch) + (1.5*i*inch), 1.25*inch, inch)
self.address_at(1.00*inch, 1.0*inch)
elif template_name == 'coldcard-paper':
x = 1.5*inch
self.addr_qr(x, 7.5*inch, 2*inch)
self.privkey_qr(x, 1.1*inch, 2*inch)
else:
print(f"\n\nDefine code for: {template_name}\n\n")
def XXX_insert_values(self, page_num, *unused):
c = self.canvas
if 0:
c.saveState()
# change color: black
c.setFillColorRGB(0,0,0)
c.setStrokeColorRGB(0,0,0)
# 12pt font:
c.setFont("Courier", 12)
# these are trival to find in output PDF once A85 encoding is disabled
c.drawString(1.25*inch, 3.5*inch, placeholders.addr)
c.drawString(6.25*inch, 3.5*inch, placeholders.privkey)
c.restoreState()
def addr_qr(self, x,y, size=1*inch, no_text=False):
self.add_qr_spot('addr', placeholders.addr if not no_text else None, x,y, page_size=size)
def privkey_qr(self, x,y, size=1*inch, no_text=False):
self.add_qr_spot('pk', placeholders.privkey if not no_text else None, x,y, page_size=size)
def address_at(self, x,y, **kws):
self.add_text(placeholders.addr, x, y, **kws)
def privkey_at(self, x,y, **kws):
self.add_text(placeholders.privkey, x, y, **kws)
def add_text(self, msg, x,y, font_size=12, font_name='Courier'):
c = self.canvas
c.saveState()
# change color: black
c.setFillColorRGB(0,0,0)
c.setStrokeColorRGB(0,0,0)
# size and font name.
c.setFont(font_name, font_size)
c.drawString(x, y, msg)
c.restoreState()
def add_qr_spot(self, name, subtext, x,y, page_size=2.25*inch, SZ=33*8):
# make a temp image to get started, data not critical except that
# must be unique because it gets hashed into eh xobj name
c = self.canvas
c.saveState()
# change color: black
c.setFillColorRGB(0,0,0)
c.setStrokeColorRGB(0,0,0)
img = Image.new('L', (SZ,SZ))
img.putdata(name.encode('utf-8'))
from reportlab.lib.utils import ImageReader
width = height = page_size
# paste in the image
c.drawImage(ImageReader(img, ident='qr1'), x, y,
width=width, height=height, preserveAspectRatio=True)
# Hack Zone:
# - find image just created, and change it to hex encoded, non-compressed form
# - also put magic pattern into data, which the Coldcard can find
# - see: pp/reportlab/pdfgen/pdfimages.py
# - and: reportlab/pdfgen/canvas.py drawImage()
# - add: reportlab/pdfbase/pdfdoc.py PDFImageXObject()
line = c._code[-2]
assert line.endswith(' Do')
handle = line[1:-3]
ximg = c._doc.idToObject.get(handle)
assert ximg
assert ximg.width == ximg.height == SZ # pixel sizes
ximg._filters = ('ASCIIHexDecode',) # kill the Flate (zlib)
ximg.bitsPerComponent = 1
# Stream itself, is just hex of raw pixels.
# - add whitespace as needed, so will split newline each raster line
# - first line reserved for magic data pattern, rest is dont-care
# - each byte is 8 pixels of monochrome data
# - left-to-right, top-to-bottom
fl = ('QR:%s' % name).encode('ascii').ljust(SZ//8, b'\xff')
assert len(fl) == (SZ//8)
# make a placeholder image for sizing/preview purposes. Not a real QR.
lines = []
img = Image.open(f'qrsample-{name}.pnm')
assert img.size == (SZ, SZ), 'need another sample'
sample = img.tobytes()
for o in range(0, len(sample), SZ//8):
lines.append(sample[o:o+(SZ//8)])
lines[0] = fl
ximg.streamContent = '\n'.join(ln.hex().upper() for ln in lines)
ximg.streamContent += '\n'
if subtext:
# pick font size; doesn't try to suit size of QR, more like readable size
font_size = 8 if len(subtext) > 40 else 11
c.setFont("Courier", font_size)
# these strings are trival to find in output PDF once A85 encoding is disabled
c.drawCentredString(x+(page_size/2), y - 5 - font_size, subtext)
c.restoreState()
def file_checker(fname):
raw = open(fname, 'rb').read()
assert raw.startswith(placeholders.header), 'header wrong/missing'
assert placeholders.addr.encode('ascii') in raw, "payment addr (text) missing"
assert placeholders.privkey.encode('ascii') in raw, 'privkey (text) missing'
lines = raw.split(b'\n')
max_len = max(len(ln) for ln in lines)
print(f"Max line length in file: {max_len}")
assert max_len < 2048, "some lines are too long"
counts = Counter()
for n, ln in enumerate(lines):
if ln == b'stream':
try:
fl = a2b_hex(lines[n+1])
assert fl.startswith(b'QR:')
except:
continue
fl = fl.rstrip(b'\xff').decode('ascii')[3:]
counts[fl] += 1
assert len(counts) == 2, "missing QR instances"
assert all(i==1 for i in counts.values()), "too many images?"
print("Includes QR's: " + ', '.join(counts))
print("File checks out ok!")
if __name__ == '__main__':
for fn in [ 'coldcard-paper', 'placeholder']:
outfile = f'outputs/{fn}.pdf'
foo = WalletBuilder(f'templates/{fn}.pdf', outfile)
foo.make_custom(fn)
foo.finalize()
file_checker(outfile)
os.system(f'open {outfile}')
# EOF