textmachine/frontend/scripts/measure.py

87 lines
3.5 KiB
Python

# Measuring a frame: the shares of the surfaces, the borders of the panels, the heights of the
# strips, the baselines of the text. The tool exists for the sake of the norm "work by measurement
# and not by eye" (FRONTEND_PLAN.md §5): every session that touches scale and gaps is bound to bring
# numbers rather than an impression.
#
# .tooling/py/bin/python scripts/measure.py references/fleet.png .shots/showcase.png
#
# Both frames are brought to CSS pixels by their own scale (the --scale argument, 2 by default —
# both the reference and our shot were taken at deviceScaleFactor 2).
#
# Pillow is pinned in scripts/requirements.txt — the procedure of installing it is there too. The
# system python on the stand comes without it, hence the venv in .tooling/py (it does not travel
# into git).
import sys
from collections import Counter
from PIL import Image
def surfaces(image, top=8):
counts = Counter(image.getdata())
total = sum(counts.values())
return [
('#%02x%02x%02x' % color, round(100 * n / total, 2)) for color, n in counts.most_common(top)
]
def runs(values, minimum=2):
"""Runs of one and the same value in a row: (value, start, length)."""
out = []
start = 0
for index in range(1, len(values) + 1):
if index == len(values) or values[index] != values[start]:
if index - start >= minimum:
out.append((values[start], start, index - start))
start = index
return out
def column_profile(image, y):
"""The colours along a horizontal cut — this is how panel borders and gaps become visible."""
return [image.getpixel((x, y)) for x in range(image.width)]
def row_profile(image, x):
return [image.getpixel((x, y)) for y in range(image.height)]
def report(path, scale):
image = Image.open(path).convert('RGB')
print(f'\n=== {path}{image.width}x{image.height} phys, {image.width // scale}x{image.height // scale} CSS ===')
print('surfaces (share of the frame):')
for color, share in surfaces(image):
print(f' {color} {share:5.2f}%')
middle = image.height // 2
# The probe is taken at the centre of the top strip and not in a corner: on the reference the
# corners of the macOS window are rounded, and in the corner lies the black ground of the
# desktop, not the ground of the shell.
shell = image.getpixel((image.width // 2, 2 * scale))
print('\nground of the shell (top strip): #%02x%02x%02x' % shell)
# A horizontal cut through the middle: the runs of the shell's ground = the gaps between panels.
strip = column_profile(image, middle)
gaps = [(start, length) for color, start, length in runs(strip) if color == shell]
print('gaps of the ground horizontally (CSS: start, width):')
for start, length in gaps:
print(f' x={start / scale:7.1f} w={length / scale:5.1f}')
# A vertical cut down the left edge of the panel: the top strip and the status strip.
x = gaps[0][0] + gaps[0][1] + 4 if gaps else 20
column = row_profile(image, x)
bands = [(start, length) for color, start, length in runs(column) if color == shell]
print(f'bands of the ground vertically at x={x / scale:.1f} (CSS: start, height):')
for start, length in bands:
print(f' y={start / scale:7.1f} h={length / scale:5.1f}')
if __name__ == '__main__':
args = [a for a in sys.argv[1:] if not a.startswith('--')]
scale = 2
for a in sys.argv[1:]:
if a.startswith('--scale='):
scale = int(a.split('=')[1])
for path in args:
report(path, scale)