The cover is a full-bleed mosaic of historical Bengali types filling the word ব্যঞ্জণ, produced by tools/gen_cover_mosaic.py from plates/ and fonts/ alone (build.sh remakes it if missing). It is static, so ebook readers can cache it. The mark on the About this edition page is drawn by tools/gen_seal.py, seeded from a SHA-256 of src/ (never the build's own outputs), so it changes exactly when the transcription does. The colophon is shortened to name the cover typefaces and the mark's mechanism, and drops the trailing ProQuest note. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
458 lines
20 KiB
Python
458 lines
20 KiB
Python
r"""Generate a fleuron as TikZ paths, deterministically from a version string.
|
|
|
|
Vector, not raster: the mark sits at about half an inch, where a PNG would
|
|
either be huge or visibly soft. Emits \sealbody and \sealseed for
|
|
src/colophon.tex (and work/cover/mosaic/cover_byanjan.tex).
|
|
|
|
Seed: a hash of this edition's sources, so the mark tracks the text. The same
|
|
sources always yield the same mark.
|
|
|
|
APPROACH -- growth, not arrangement.
|
|
|
|
Following Wong, Zongker & Salesin, "Computer-Generated Floral Ornament"
|
|
(SIGGRAPH '98), traditional ornament is generated rather than composed: a main
|
|
stem grows, subordinate elements branch from it tangentially, and each element
|
|
is placed against the space left by its parent. Earlier attempts here failed in
|
|
two opposite directions -- a free random walk gave lopsided scribbles, and a
|
|
fixed vocabulary on shared radial bands gave machined rosettes. Growth with
|
|
tangential continuity is the middle path, and it is what makes fleurons look
|
|
drawn.
|
|
|
|
Strokes are rendered as filled outlines with a width profile rather than as
|
|
constant-width lines. That taper -- full at the base of a scroll, vanishing at
|
|
its tip -- is the single thing that most makes the result read as a written
|
|
glyph instead of a diagram.
|
|
|
|
Nothing is mirrored or rotated into place: no symmetry is enforced anywhere.
|
|
Any balance is a consequence of the growth rules and the final fit to the disc.
|
|
"""
|
|
import hashlib
|
|
import math
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
SCALE = 0.52 # the scale the cover and colophon apply; see W()
|
|
WMIN = 0.012 # cm, about 0.34pt -- below this a hairline drops out in print
|
|
|
|
|
|
def W(u):
|
|
"""Line width in cm for a stroke of `u` seal-units.
|
|
|
|
TikZ `scale=` transforms coordinates but NOT line widths, so a width
|
|
written as an absolute cm value looks like a hairline in a large preview
|
|
and like a heavy bar at the final scale. Widths are therefore expressed as
|
|
a fraction of the seal's radius and converted here, with a floor so nothing
|
|
lands below what print can hold. (Only the frame is stroked; the ornament
|
|
itself is filled outlines, which scale cleanly.)
|
|
"""
|
|
return max(WMIN, u * SCALE)
|
|
|
|
|
|
def seed_of(text):
|
|
# whole digest, not a truncation: the seed was never the limiting factor on
|
|
# variety -- the generator's design space is -- but truncating invites the
|
|
# question, and consuming all of it costs nothing.
|
|
return int(hashlib.sha256(text.encode()).hexdigest(), 16)
|
|
|
|
|
|
class R:
|
|
"""Small deterministic PRNG, so the mark does not depend on the Python
|
|
version's random module internals."""
|
|
|
|
def __init__(self, s):
|
|
self.s = s & ((1 << 64) - 1)
|
|
|
|
def _next(self):
|
|
self.s = (self.s * 6364136223846793005 + 1442695040888963407) & ((1 << 64) - 1)
|
|
return (self.s >> 11) / float(1 << 53)
|
|
|
|
def uni(self, a, b):
|
|
return a + (b - a) * self._next()
|
|
|
|
def pick(self, xs):
|
|
return xs[int(self._next() * len(xs)) % len(xs)]
|
|
|
|
def chance(self, p):
|
|
return self._next() < p
|
|
|
|
|
|
# ---------------------------------------------------------------- geometry
|
|
|
|
def _spiral(b, sweep, curl, n):
|
|
"""A logarithmic-spiral arc in local coordinates, normalised so it starts
|
|
at the origin with its tangent along +x and spans roughly unit size.
|
|
|
|
Log spirals are the backbone of scrollwork: the growth rate `b` fixes how
|
|
quickly the scroll opens, which is the proportion the eye reads as either
|
|
tight and vegetal or loose and calligraphic.
|
|
"""
|
|
pts = []
|
|
for i in range(n):
|
|
th = sweep * i / (n - 1)
|
|
r = math.exp(b * th)
|
|
pts.append((r * math.cos(th), curl * r * math.sin(th)))
|
|
ox, oy = pts[0]
|
|
pts = [(x - ox, y - oy) for x, y in pts]
|
|
ang = math.atan2(curl, b) # initial tangent
|
|
ca, sa = math.cos(-ang), math.sin(-ang)
|
|
pts = [(x * ca - y * sa, x * sa + y * ca) for x, y in pts]
|
|
m = max(math.hypot(x, y) for x, y in pts) or 1.0
|
|
return [(x / m, y / m) for x, y in pts]
|
|
|
|
|
|
def _place(pts, ang, scale, ox, oy):
|
|
ca, sa = math.cos(ang), math.sin(ang)
|
|
return [(ox + (x * ca - y * sa) * scale, oy + (x * sa + y * ca) * scale)
|
|
for x, y in pts]
|
|
|
|
|
|
def _curvature_radius(p0, p1, p2):
|
|
"""Circumradius of three consecutive samples."""
|
|
ax, ay = p0; bx, by = p1; cx, cy = p2
|
|
a = math.hypot(bx-cx, by-cy)
|
|
b = math.hypot(ax-cx, ay-cy)
|
|
c = math.hypot(ax-bx, ay-by)
|
|
area2 = abs((bx-ax)*(cy-ay) - (cx-ax)*(by-ay))
|
|
if area2 < 1e-12:
|
|
return 1e9
|
|
return (a*b*c) / (2.0*area2)
|
|
|
|
|
|
def _tapered(pts, w0, w1):
|
|
"""Filled outline of a stroke whose half-width runs from w0 to w1.
|
|
|
|
The width eases rather than falling linearly, so the stroke keeps its
|
|
weight through the body of the curve and gives it up late -- a pen's
|
|
behaviour, and the reason this reads as a written mark.
|
|
|
|
The half-width is additionally capped at a fraction of the local radius of
|
|
curvature. Where a scroll curls tighter than its own stroke is wide, the
|
|
offset outline turns itself inside out and fills as a solid blob -- which
|
|
is exactly what the tight inner coils were doing. Capping keeps the inner
|
|
edge from crossing itself.
|
|
|
|
Returns the path and its outline points, so the caller can fit the mark on
|
|
the ink it actually puts down rather than on the centreline.
|
|
"""
|
|
PA, PB = 0.45, 0.80 # swell exponents; peak at PA/(PA+PB)
|
|
TP = PA / (PA + PB)
|
|
n = len(pts)
|
|
left, right = [], []
|
|
for i, (x, y) in enumerate(pts):
|
|
t = i / (n - 1)
|
|
j0 = max(0, i - 1)
|
|
j1 = min(n - 1, i + 1)
|
|
dx = pts[j1][0] - pts[j0][0]
|
|
dy = pts[j1][1] - pts[j0][1]
|
|
L = math.hypot(dx, dy) or 1.0
|
|
nx, ny = -dy / L, dx / L
|
|
# Swell, not a monotonic taper: the stroke rises from nothing near the
|
|
# root, peaks about a third along, and tapers to a point. A profile
|
|
# that merely decreased left a blunt slab at the start -- the hard
|
|
# straight edge that read as a wedge stuck onto the scroll.
|
|
tt = min(max(t, 1e-6), 1.0 - 1e-6)
|
|
w = w0 * ((tt / TP) ** PA) * (((1.0 - tt) / (1.0 - TP)) ** PB)
|
|
w = max(w, w1 * 0.25)
|
|
if 0 < i < n - 1:
|
|
w = min(w, 0.55 * _curvature_radius(pts[i-1], pts[i], pts[i+1]))
|
|
left.append((x + nx * w, y + ny * w))
|
|
right.append((x - nx * w, y - ny * w))
|
|
ring = left + right[::-1]
|
|
return (" -- ".join(f"({x:.4f},{y:.4f})" for x, y in ring) + " -- cycle", ring)
|
|
|
|
|
|
def _bud(x, y, ang, r, elong):
|
|
"""A teardrop terminal: the swelling a scroll resolves into."""
|
|
ux, uy = math.cos(ang), math.sin(ang)
|
|
px, py = -uy, ux
|
|
tipx, tipy = x + ux * r * elong, y + uy * r * elong
|
|
c1 = (x + px * r + ux * r * 0.4, y + py * r + uy * r * 0.4)
|
|
c2 = (tipx + px * r * 0.25, tipy + py * r * 0.25)
|
|
c3 = (tipx - px * r * 0.25, tipy - py * r * 0.25)
|
|
c4 = (x - px * r + ux * r * 0.4, y - py * r + uy * r * 0.4)
|
|
return (f"({x:.4f},{y:.4f}) .. controls ({c1[0]:.4f},{c1[1]:.4f}) and "
|
|
f"({c2[0]:.4f},{c2[1]:.4f}) .. ({tipx:.4f},{tipy:.4f}) .. controls "
|
|
f"({c3[0]:.4f},{c3[1]:.4f}) and ({c4[0]:.4f},{c4[1]:.4f}) .. cycle")
|
|
|
|
|
|
def _diamond(x, y, ang, w, h):
|
|
"""A lozenge: the gem of the ornament vocabulary."""
|
|
ux, uy = math.cos(ang), math.sin(ang)
|
|
px, py = -uy, ux
|
|
pts = [(x + ux*h, y + uy*h), (x + px*w, y + py*w),
|
|
(x - ux*h, y - uy*h), (x - px*w, y - py*w)]
|
|
return " -- ".join(f"({a:.4f},{b:.4f})" for a, b in pts) + " -- cycle", pts
|
|
|
|
|
|
def _thorn(x, y, ang, length, w, bend):
|
|
"""A spike with a slight bend -- the barb that sharpens a scroll's rhythm
|
|
against all the rounded lobes."""
|
|
ux, uy = math.cos(ang), math.sin(ang)
|
|
px, py = -uy, ux
|
|
tip = (x + ux*length + px*bend, y + uy*length + py*bend)
|
|
b1 = (x + px*w, y + py*w)
|
|
b2 = (x - px*w, y - py*w)
|
|
c1 = (x + px*w*0.7 + ux*length*0.45, y + py*w*0.7 + uy*length*0.45)
|
|
c2 = (x - px*w*0.7 + ux*length*0.45, y - py*w*0.7 + uy*length*0.45)
|
|
return (f"({b1[0]:.4f},{b1[1]:.4f}) .. controls ({c1[0]:.4f},{c1[1]:.4f}) and "
|
|
f"({tip[0]:.4f},{tip[1]:.4f}) .. ({tip[0]:.4f},{tip[1]:.4f}) .. controls "
|
|
f"({c2[0]:.4f},{c2[1]:.4f}) and ({b2[0]:.4f},{b2[1]:.4f}) .. "
|
|
f"({b2[0]:.4f},{b2[1]:.4f}) -- cycle"), [tip, b1, b2]
|
|
|
|
|
|
def build(seed_text):
|
|
"""A printer's ornament: a sweeping scroll carrying lobed leaves.
|
|
|
|
Modelled on typographic ornament specimens (acanthus scrolls, corner
|
|
pieces, flourishes) rather than on plants. The vocabulary is: one broad
|
|
scroll spine, lobes hanging off it on alternating sides, a vein cut out of
|
|
each large lobe, and a tight curl at the terminals. No frame -- these sit
|
|
as free silhouettes on the page.
|
|
|
|
Two earlier approaches missed in opposite directions. Free growth with no
|
|
axis produced biomorphic creatures; a stem with mirrored leaves produced
|
|
botany. What separates ornament from both is mass: broad filled sweeps
|
|
with counters cut into them, not thin strokes of uniform weight.
|
|
|
|
Mirrored composition is one option among several, not the rule -- the
|
|
specimens include both symmetric and asymmetric pieces.
|
|
"""
|
|
rng = R(seed_of(seed_text))
|
|
ink_shapes, cut_shapes, allpts = [], [], []
|
|
NS = 26
|
|
|
|
def add(path, ring, cut=False):
|
|
(cut_shapes if cut else ink_shapes).append(path)
|
|
allpts.extend(ring)
|
|
|
|
def bud_extent(x, y, ang, r, elong):
|
|
"""The bud's actual corners. A crude +/-3r box here overstated the
|
|
piece's radius, so the fit shrank the whole ornament away from the
|
|
circle it was supposed to fill."""
|
|
ux, uy = math.cos(ang), math.sin(ang)
|
|
px, py = -uy, ux
|
|
tip = (x + ux*r*elong, y + uy*r*elong)
|
|
return [tip, (x + px*r, y + py*r), (x - px*r, y - py*r),
|
|
(x - ux*r*0.3, y - uy*r*0.3)]
|
|
|
|
def sweep_stroke(ox, oy, ang, length, w0, w1, curl, sweep, b):
|
|
local = _spiral(b, sweep, curl, NS)
|
|
pts = _place(local, ang, length, ox, oy)
|
|
path, ring = _tapered(pts, w0, w1)
|
|
add(path, ring)
|
|
return pts
|
|
|
|
# ---- the spine: one broad scroll, the piece's backbone
|
|
# Envelope: the undrawn shape the piece is fitted into. Ornament
|
|
# specimens are not all round -- there are wide band pieces, upright ones
|
|
# and squarish ones -- so the format is seeded, and the spine's direction
|
|
# is biased to suit it rather than fitted to it after the fact.
|
|
fmt = rng.pick(['round', 'round', 'round', 'band', 'band', 'tall', 'square'])
|
|
ENV = {'round': (1.00, 1.00), 'band': (1.40, 0.66),
|
|
'tall': (0.66, 1.40), 'square': (1.06, 1.06)}[fmt]
|
|
curl = 1.0 if rng.chance(0.5) else -1.0
|
|
if fmt == 'band':
|
|
ang0 = math.radians(rng.uni(-18, 18))
|
|
elif fmt == 'tall':
|
|
ang0 = math.radians(rng.uni(72, 108))
|
|
else:
|
|
ang0 = math.radians(rng.uni(-40, 40))
|
|
spine_b = rng.uni(0.13, 0.30)
|
|
# Sweep kept under a half-turn: a spine that curls further closes on
|
|
# itself, and once mirrored it becomes a ring with a big hole punched
|
|
# through the middle. Ornament wants open space AROUND the mass, not
|
|
# trapped inside it.
|
|
spine_sweep = rng.uni(1.9, 3.1)
|
|
spine_w = rng.uni(0.14, 0.23)
|
|
spine = sweep_stroke(0.0, 0.0, ang0, 1.0, spine_w, spine_w * 0.10,
|
|
curl, spine_sweep, spine_b)
|
|
|
|
# ---- lobes along the spine, alternating sides, largest near the root
|
|
nlobe = rng.pick([4, 5, 5, 6, 7]) # floor: too few and the spine reads as a bare comma
|
|
side = 1.0 if rng.chance(0.5) else -1.0
|
|
lobe_base = rng.uni(0.34, 0.52)
|
|
for i in range(nlobe):
|
|
f = i / max(1, nlobe - 1)
|
|
idx = max(1, min(NS - 2, int((0.10 + 0.74 * f) * (NS - 1))))
|
|
px, py = spine[idx]
|
|
tang = math.atan2(spine[idx + 1][1] - spine[idx - 1][1],
|
|
spine[idx + 1][0] - spine[idx - 1][0])
|
|
a = tang + side * math.radians(rng.uni(52, 96))
|
|
ln = lobe_base * (1.0 - 0.46 * f)
|
|
lw = spine_w * rng.uni(0.75, 1.05) * (1.0 - 0.34 * f)
|
|
lc = curl if rng.chance(0.68) else -curl
|
|
lpts = sweep_stroke(px, py, a, ln, lw, lw * 0.10, lc,
|
|
rng.uni(1.6, 3.0), rng.uni(0.12, 0.30))
|
|
# vein: the same curve cut back out in paper, stopping short of the
|
|
# tip. Cutting a counter into the mass is what reads as carved
|
|
# acanthus rather than as a painted blob.
|
|
if lw > spine_w * 0.62 and rng.chance(0.78):
|
|
vp = lpts[:int(NS * rng.uni(0.62, 0.80))]
|
|
if len(vp) > 3:
|
|
vpath, vring = _tapered(vp, lw * rng.uni(0.26, 0.40), lw * 0.05)
|
|
add(vpath, vring, cut=True)
|
|
# secondary tendril off the larger lobes: the density the richer
|
|
# specimen pieces get from subordinate curls
|
|
if f < 0.55 and rng.chance(0.55):
|
|
tx, ty = lpts[int(len(lpts) * rng.uni(0.45, 0.70))]
|
|
ta = a + side * math.radians(rng.uni(40, 95))
|
|
sweep_stroke(tx, ty, ta, ln * rng.uni(0.34, 0.55),
|
|
lw * rng.uni(0.34, 0.52), lw * 0.05,
|
|
-lc, rng.uni(2.2, 3.8), rng.uni(0.08, 0.22))
|
|
side = -side if rng.chance(0.72) else side
|
|
|
|
# ---- terminal curl: a tight eye where the spine runs out
|
|
ex, ey = spine[-1]
|
|
tg = math.atan2(ey - spine[-2][1], ex - spine[-2][0])
|
|
if rng.chance(0.8):
|
|
sweep_stroke(ex, ey, tg, rng.uni(0.16, 0.30), spine_w * 0.34,
|
|
spine_w * 0.06, curl, rng.uni(2.4, 3.6), rng.uni(0.05, 0.14))
|
|
if rng.chance(0.5):
|
|
br = spine_w * rng.uni(1.0, 1.8)
|
|
rx, ry = spine[0]
|
|
ba, be = ang0 + math.pi, rng.uni(1.4, 2.2)
|
|
add(_bud(rx, ry, ba, br, be), bud_extent(rx, ry, ba, br, be))
|
|
|
|
# ---- accents from the wider ornament vocabulary, hung off the spine
|
|
naccent = rng.pick([1, 2, 2, 3])
|
|
for _ in range(naccent):
|
|
kind = rng.pick(['gem', 'berries', 'thorn', 'swash', 'petal'])
|
|
idx = max(1, min(NS - 2, int(rng.uni(0.15, 0.92) * (NS - 1))))
|
|
ax, ay = spine[idx]
|
|
tg = math.atan2(spine[idx + 1][1] - spine[idx - 1][1],
|
|
spine[idx + 1][0] - spine[idx - 1][0])
|
|
nrm = tg + (math.pi / 2 if rng.chance(0.5) else -math.pi / 2)
|
|
off = spine_w * rng.uni(0.9, 2.2)
|
|
gx, gy = ax + math.cos(nrm) * off, ay + math.sin(nrm) * off
|
|
if kind == 'gem':
|
|
gw = spine_w * rng.uni(0.55, 1.0)
|
|
gh = gw * rng.uni(1.4, 2.4)
|
|
pth, ext = _diamond(gx, gy, nrm, gw, gh)
|
|
add(pth, ext)
|
|
if rng.chance(0.6): # facet cut out of the gem
|
|
pth2, _ = _diamond(gx, gy, nrm, gw * 0.42, gh * 0.42)
|
|
add(pth2, [], cut=True)
|
|
elif kind == 'berries':
|
|
for j in range(rng.pick([2, 3, 3])):
|
|
bx = gx + math.cos(nrm + j * 2.1) * spine_w * 0.9
|
|
by = gy + math.sin(nrm + j * 2.1) * spine_w * 0.9
|
|
br = spine_w * rng.uni(0.34, 0.55)
|
|
add(f"({bx:.4f},{by:.4f}) circle[radius={br:.4f}]",
|
|
[(bx + br, by + br), (bx - br, by - br)])
|
|
elif kind == 'thorn':
|
|
tl = spine_w * rng.uni(2.0, 4.0)
|
|
pth, ext = _thorn(gx, gy, nrm, tl, spine_w * rng.uni(0.30, 0.50),
|
|
tl * rng.uni(-0.35, 0.35))
|
|
add(pth, ext)
|
|
elif kind == 'petal':
|
|
pr = spine_w * rng.uni(0.8, 1.5)
|
|
pe = rng.uni(1.5, 2.6)
|
|
add(_bud(gx, gy, nrm, pr, pe), bud_extent(gx, gy, nrm, pr, pe))
|
|
else: # swash: a long thin sweep, the counterweight to the heavy lobes
|
|
sweep_stroke(gx, gy, tg + rng.uni(-0.8, 0.8), rng.uni(0.40, 0.72),
|
|
spine_w * rng.uni(0.16, 0.30), spine_w * 0.03,
|
|
curl if rng.chance(0.5) else -curl,
|
|
rng.uni(2.4, 4.2), rng.uni(0.06, 0.18))
|
|
|
|
# Mirroring follows the format: a band ornament is a motif mirrored along
|
|
# its long axis, which is how it comes to fill a wide envelope at all --
|
|
# uniform scaling alone just centred a small piece in a lot of space.
|
|
if fmt == 'band':
|
|
mirrored = 'x'
|
|
elif fmt == 'tall':
|
|
mirrored = 'y'
|
|
else:
|
|
mirrored = 'x' if rng.chance(0.34) else None
|
|
|
|
# ---- fit inside an undrawn circle: the piece is inscribed in a disc that
|
|
# is never stroked. Fitting a bounding box instead lets the diagonal run
|
|
# past that disc, so pieces of different proportion sat at visibly
|
|
# different sizes; fitting the furthest point makes every mark occupy the
|
|
# same optical circle whatever its shape.
|
|
if mirrored == 'x':
|
|
pts_for_fit = allpts + [(-x, y) for x, y in allpts]
|
|
elif mirrored == 'y':
|
|
pts_for_fit = allpts + [(x, -y) for x, y in allpts]
|
|
else:
|
|
pts_for_fit = list(allpts)
|
|
xs = [p[0] for p in pts_for_fit]; ys = [p[1] for p in pts_for_fit]
|
|
cx, cy = (min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0
|
|
ea, eb = ENV
|
|
mx = max(abs(x - cx) for x, y in pts_for_fit) or 1e-6
|
|
my = max(abs(y - cy) for x, y in pts_for_fit) or 1e-6
|
|
if fmt == 'square':
|
|
k = 0.95 * min(ea / mx, eb / my)
|
|
else:
|
|
# uniform scale that just fits the envelope ellipse: never distort the
|
|
# letterforms to fill a shape, only choose how much room they get
|
|
k = 0.95 * min(ea / mx, eb / my)
|
|
|
|
out = [f"% seal seeded from: {seed_text}",
|
|
f"% printer's ornament, {mirrored or 'single'}, {fmt}",
|
|
f"% envelope {ENV[0]:.2f} {ENV[1]:.2f}"]
|
|
out.append(f"\\begin{{scope}}[shift={{({-cx*k:.4f},{-cy*k:.4f})}},scale={k:.4f}]")
|
|
def emit(xs_):
|
|
for pth in ink_shapes:
|
|
out.append(f"\\fill[ink,{xs_}] {pth};")
|
|
for pth in cut_shapes:
|
|
out.append(f"\\fill[paper,{xs_}] {pth};")
|
|
emit("xscale=1")
|
|
if mirrored == 'x':
|
|
emit("xscale=-1")
|
|
elif mirrored == 'y':
|
|
emit("yscale=-1")
|
|
out.append("\\end{scope}")
|
|
return "\n".join(out)
|
|
|
|
|
|
def content_hash():
|
|
"""SHA-256 over the sources that define this edition's text, so the seal
|
|
tracks the CONTENT rather than the commit: re-committing without changing a
|
|
word yields the same seal, and changing a word changes it even before
|
|
anything is committed."""
|
|
# NOT hashed: anything generated -- src/main.tex (build.sh writes it), the
|
|
# built PDF, work/seal.tex itself. Hashing a derived file would make the
|
|
# seed depend on the seal it produces.
|
|
h = hashlib.sha256()
|
|
import glob as g
|
|
for path in sorted(g.glob('src/pages/*.tex') + g.glob('src/*.tex')):
|
|
if path.endswith('main.tex'):
|
|
continue
|
|
with open(path, 'rb') as f:
|
|
h.update(path.encode())
|
|
h.update(f.read())
|
|
return h.hexdigest()[:16]
|
|
|
|
|
|
if __name__ == '__main__':
|
|
args = sys.argv[1:]
|
|
if '--scale' in args:
|
|
i = args.index('--scale')
|
|
globals()['SCALE'] = float(args[i + 1])
|
|
del args[i:i + 2]
|
|
out_path = 'work/seal.tex'
|
|
if '--out' in args:
|
|
i = args.index('--out')
|
|
out_path = args[i + 1]
|
|
del args[i:i + 2]
|
|
if args and args[0] == '--content':
|
|
tag, kind = content_hash(), 'content'
|
|
elif args:
|
|
tag, kind = args[0], 'given'
|
|
else:
|
|
try:
|
|
tag = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'],
|
|
text=True, stderr=subprocess.DEVNULL).strip()
|
|
kind = 'commit'
|
|
except Exception:
|
|
tag, kind = content_hash(), 'content'
|
|
body = build(tag)
|
|
os.makedirs(os.path.dirname(out_path) or '.', exist_ok=True)
|
|
with open(out_path, 'w') as f:
|
|
f.write("% generated by tools/gen_seal.py -- do not edit, not tracked\n")
|
|
f.write(f"% seed kind: {kind}\n")
|
|
f.write("\\newcommand{\\sealseed}{" + tag + "}\n")
|
|
f.write("\\newcommand{\\sealbody}{%\n" + body + "\n}\n")
|
|
print(f"{kind}: {tag} -> {out_path}")
|