Files
dtf-system/app/grade_check.py
Cauê Faleiros 37715ef223
All checks were successful
Build and deploy / Validate source (push) Successful in 1m23s
Build and deploy / Integration suite on a real stack (push) Successful in 3m13s
Build and deploy / Secret scan and release gate (push) Successful in 10s
Build and deploy / Publish images (push) Successful in 1m25s
feat: the server checks the grade against the files before approving
The price depends on the grade, which the browser worked out and the API
took on trust. Before a cart is approved at checkout the API now recomputes
it from the uploaded files by the Site's own rules: the pixel size in a
PNG, JPG or WebP header across the printed width (rotation included), and
the area-weighted DPI of the images placed in a PDF of up to 150 MB, 300
for vectors. Sheets take the worst grade, artworks the average. A claim more
than 2 points above the file's grade, or a discount on a file the server
cannot grade, waits for an operator, with the reason on the Kanban.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:24:37 -03:00

190 lines
7.7 KiB
Python

"""The grade (the resolution discount) recomputed from the uploaded files.
The Site grades the art in the customer's browser and sends the grade with the
cart, and the price depends on it. A customer who edits the page could claim a
better grade, so before a cart is approved at checkout the server reads the
files it already has and works the grade out again, by the Site's own rules:
- a finished sheet: its pixel width over the sheet width is its DPI, and the
item's grade comes from the worst sheet (web/site-quality.js);
- artworks: each one's DPI along the width it is printed at, rotation
included; the item's grade is the average of the artworks' grades;
- a PDF: the area-weighted DPI of the images drawn on its page, 300 when it
is only vectors (web/site-pdf.js, dpiDasImagens and resumoDpi).
Images are measured from their first bytes, never decoded. A PDF is opened
only up to PDF_MAX_BYTES, the size the Site itself analyses. A grade that
cannot be recomputed is None; the caller decides what that means.
"""
import math
import os
import struct
import tempfile
DPI_IDEAL = 300
# The Site analyses PDFs up to 150 MB (GRANDE_BYTES); above that it does not
# grade them, so neither does this.
PDF_MAX_BYTES = 150 * 1048576
HEAD_BYTES = 1048576
# Rounding in the browser and here can differ by a point on the same file.
TOLERANCE = 2
def js_round(value):
"""Math.round, which rounds halves up; Python's round() does not."""
return math.floor(value + 0.5)
def grade_of(dpi):
return max(6, min(100, js_round(dpi / DPI_IDEAL * 100)))
def image_size(head):
"""(width, height) in pixels from a PNG, JPEG or WebP's first bytes."""
if head[:8] == b'\x89PNG\r\n\x1a\n' and head[12:16] == b'IHDR':
return struct.unpack('>II', head[16:24])
if head[:2] == b'\xff\xd8':
i = 2
while i + 9 < len(head):
if head[i] != 0xFF:
i += 1
continue
marker = head[i + 1]
if marker in (0xD8, 0x01) or 0xD0 <= marker <= 0xD7 or marker == 0xFF:
i += 1 if marker == 0xFF else 2
continue
length = struct.unpack('>H', head[i + 2:i + 4])[0]
if 0xC0 <= marker <= 0xCF and marker not in (0xC4, 0xC8, 0xCC):
h, w = struct.unpack('>HH', head[i + 5:i + 9])
return w, h
i += 2 + length
return None
if head[:4] == b'RIFF' and head[8:12] == b'WEBP':
chunk = head[12:16]
if chunk == b'VP8X':
return (int.from_bytes(head[24:27], 'little') + 1, int.from_bytes(head[27:30], 'little') + 1)
if chunk == b'VP8L' and head[20] == 0x2F:
bits = int.from_bytes(head[21:25], 'little')
return (bits & 0x3FFF) + 1, ((bits >> 14) & 0x3FFF) + 1
if chunk == b'VP8 ' and head[23:26] == b'\x9d\x01\x2a':
w, h = struct.unpack('<HH', head[26:30])
return w & 0x3FFF, h & 0x3FFF
return None
def _multiply(a, b):
return [a[0] * b[0] + a[2] * b[1], a[1] * b[0] + a[3] * b[1],
a[0] * b[2] + a[2] * b[3], a[1] * b[2] + a[3] * b[3],
a[0] * b[4] + a[2] * b[5] + a[4], a[1] * b[4] + a[3] * b[5] + a[5]]
def pdf_dpi(path):
"""The page's area-weighted image DPI, 300 for vectors only, None if the
file is not a one-page PDF this can read."""
import pikepdf
try:
with pikepdf.open(path) as pdf:
if len(pdf.pages) != 1:
return None
page = pdf.pages[0]
unit = float(page.obj.get('/UserUnit', 1))
found = []
def walk(resources, instructions, ctm, depth):
stack = []
xobjects = resources.get('/XObject', {}) if resources is not None else {}
for operands, operator in instructions:
op = str(operator)
if op == 'q':
stack.append(ctm)
elif op == 'Q':
ctm = stack.pop() if stack else [1, 0, 0, 1, 0, 0]
elif op == 'cm':
ctm = _multiply(ctm, [float(v) for v in operands])
elif op == 'Do' and operands:
xobject = xobjects.get(str(operands[0]))
if xobject is None:
continue
subtype = xobject.get('/Subtype')
if subtype == '/Image':
width_pt = math.hypot(ctm[0], ctm[1])
if width_pt >= 1:
found.append((int(xobject.get('/Width', 0)) / (width_pt * unit / 72),
abs(ctm[0] * ctm[3] - ctm[1] * ctm[2])))
elif subtype == '/Form' and depth < 8:
matrix = [float(v) for v in xobject.get('/Matrix', [1, 0, 0, 1, 0, 0])]
walk(xobject.get('/Resources', resources),
pikepdf.parse_content_stream(xobject), _multiply(ctm, matrix), depth + 1)
walk(page.obj.get('/Resources'), pikepdf.parse_content_stream(page), [1, 0, 0, 1, 0, 0], 0)
except Exception:
return None
found = [(dpi, area) for dpi, area in found if dpi > 0]
if not found:
return DPI_IDEAL
total = sum(area for _, area in found) or 1
return js_round(sum(dpi * area for dpi, area in found) / total)
class Files:
"""The uploaded files, read from storage as little as needed."""
def __init__(self, storage):
self.storage = storage
def head(self, row):
return self.storage.client.get_object(Bucket=self.storage.bucket, Key=row['object_key'],
Range=f'bytes=0-{HEAD_BYTES - 1}')['Body'].read()
def pdf_dpi(self, row):
if row['size'] > PDF_MAX_BYTES:
return None
with tempfile.NamedTemporaryFile(suffix='.pdf') as handle:
self.storage.client.download_fileobj(self.storage.bucket, row['object_key'], handle)
handle.flush()
return pdf_dpi(handle.name)
def source_dpi(files, row, source):
"""One source's DPI across the width it is printed at, or None."""
name = row['name'].lower()
width_in = float(source['width_cm']) / 2.54
if name.endswith('.pdf'):
return files.pdf_dpi(row) if source['kind'] == 'sheet' else None
if not name.endswith(('.png', '.jpg', '.jpeg', '.webp')):
return None
size = image_size(files.head(row))
if not size or not all(size):
return None
across = size[1] if source['rotation_degrees'] in (90, 270) else size[0]
return across / width_in
def item_grade(files, item, rows):
"""The grade the Site gives this item, recomputed; None if it cannot be."""
sources = item['production']['sources']
dpis = []
for source in sources:
row = rows.get(str(source['upload_id']))
dpi = source_dpi(files, row, source) if row else None
if dpi is None:
return None
dpis.append(dpi)
if all(source['kind'] == 'sheet' for source in sources):
# A sheet's DPI is shown and compared as a whole number.
return grade_of(min(js_round(d) for d in dpis))
return js_round(sum(grade_of(d) for d in dpis) / len(dpis))
def mismatch(files, items, rows):
"""Why a cart's grades cannot be approved at checkout, or None."""
for item in items:
if item['grade'] == 0:
continue # full price: nothing to verify
verified = item_grade(files, item, rows)
if verified is None:
return 'Nota não conferida no servidor'
if item['grade'] > verified + TOLERANCE:
return f"Nota {item['grade']} maior que a do arquivo ({verified})"
return None