Files
HMDesign/examples/org-chart/audit-quality-pixels.py

92 lines
5.6 KiB
Python

"""Read browser PNG backgrounds beside text; never edit production images.
Sample positions cover DOM text line bounds, with a clear padding-column probe
for the current vertical opaque gradients. Pixel sampling and the sRGB CSS model
are distinct evidence; unsupported/clipped painting is NOT VERIFIED.
"""
import json
import math
import re
import sys
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / 'test-results' / 'org-chart-quality'
report = json.loads((OUT / (sys.argv[1] if len(sys.argv) > 1 else 'browser.json')).read_text(encoding='utf-8'))
def rgb(value):
match = re.fullmatch(r'rgba?\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)(?:,\s*(1))?\)', value)
return tuple(map(float, match.groups()[:3])) if match else None
def luminance(color):
def linear(channel):
encoded = channel / 255
return encoded / 12.92 if encoded <= .04045 else ((encoded + .055) / 1.055) ** 2.4
return sum(weight * linear(channel) for weight, channel in zip((.2126, .7152, .0722), color))
def contrast(first, second):
a, b = sorted((luminance(first), luminance(second)))
return (b + .05) / (a + .05)
evidence = []
for measurement in report['measurements']:
image = Image.open(OUT / measurement['screenshot']).convert('RGB')
for item in measurement['colors']:
foreground = rgb(item['color'])
behind = item['behind']
entry = dict(label=measurement['label'], width=measurement['width'], selector=item['className'],
organizationId=item.get('organizationId'), variant=item.get('variant'), target=item.get('target'),
foreground=item['color'], fontSize=item['fontSize'], fontWeight=item['fontWeight'])
threshold = 3 if item['fontSize'] >= 24 or (float(item['fontWeight']) >= 700 and item['fontSize'] >= 14 * 96 / 72) else 4.5
entry['threshold'] = threshold
if not foreground or not behind:
entry.update(status='NOT VERIFIED', reason='Unknown foreground/background composition')
elif behind['gradient'] == 'none':
background = rgb(behind['background'])
if background:
value = contrast(foreground, background)
entry.update(status='PASS' if value >= threshold else 'REVIEW', method='computed opaque solid', ratio=value, background=behind['background'])
else:
entry.update(status='NOT VERIFIED', reason='Transparent/unsupported solid composition')
else:
stops = re.findall(r'rgb\((\d+),\s*(\d+),\s*(\d+)\)\s+(0|100)%', behind['gradient'])
box, lines, clip = behind['box'], item['ranges'], item.get('clip')
angle = re.search(r'linear-gradient\(([^r][^,]*),', behind['gradient'])
if len(stops) != 2 or not lines or (angle and angle.group(1).strip() != '180deg'):
entry.update(status='NOT VERIFIED', reason='Unsupported gradient/text range')
else:
colors = [tuple(map(int, stop[:3])) for stop in stops]
# Probe an empty padding column, inside corners and before text.
text_left = min(line['x'] for line in lines)
x = math.floor(min(text_left - 2, box['x'] + max(4, (text_left - box['x']) / 2)))
samples, deltas = [], []
for line in lines:
for y in range(math.ceil(line['y']), math.floor(line['y'] + line['height']) + 1):
if not (0 <= x < image.width and 0 <= y < image.height):
continue
if clip and not (clip['x'] + 1 <= x < clip['x'] + clip['width'] - 1 and clip['y'] + 1 <= y < clip['y'] + clip['height'] - 1):
continue
offset = (y + .5 - box['y']) / box['height']
if not 0 <= offset <= 1:
continue
pixel = image.getpixel((x, y))
modeled = tuple(a + (b - a) * offset for a, b in zip(*colors))
delta = max(abs(a - b) for a, b in zip(pixel, modeled))
deltas.append(delta)
samples.append(dict(x=x, y=y, background=pixel, contrast=contrast(foreground, pixel), modelDelta=delta))
if not samples or max(deltas) > 2:
entry.update(status='NOT VERIFIED', reason='Probe clipped, overpainted or disagrees with supported sRGB painting model', sampled=len(samples), maxModelDelta=max(deltas) if deltas else None)
else:
minimum = min(samples, key=lambda sample: sample['contrast'])
entry.update(status='PASS' if minimum['contrast'] >= threshold else 'REVIEW', method='PNG background beside actual DOM text lines',
ratio=minimum['contrast'], sampled=len(samples), maxModelDelta=max(deltas), weakest=minimum,
conservativeStopMinimum=min(contrast(foreground, color) for color in colors))
evidence.append(entry)
result = dict(method='DPR1 Chrome PNG; opaque foreground from computed CSS; padding-column gradient probes over DOM Range line bounds',
thresholds='Unrounded 4.5 normal / 3 large; no logo recoloring; no full WCAG certification',
results=evidence, statusCounts={status: sum(item['status'] == status for item in evidence) for status in ('PASS', 'REVIEW', 'NOT VERIFIED')})
(OUT / (sys.argv[2] if len(sys.argv) > 2 else 'contrast-pixels.json')).write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
print(json.dumps(result['statusCounts']))