87 lines
3.5 KiB
Python
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)
|