"""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('= 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