Cortar el EXIF sin aplicar la orientacion rota las fotos verticales: arreglado a ciegas, falta ejercerlo contra una foto real de telefono
Levantado por coder-mars-ca desde el corpus de fotos de telefono de mars (mul4s6rcd5nh), no encontrado en terra - terra no tiene corpus todavia, y esa es la unica razon por la que nunca fallo. EL DEFECTO: stripJpegExif corta el segmento APP1/Exif de un JPEG chico para sacarle el GPS sin pagar un re-encode. La Orientation vive en ESE segmento. Cortarla borra la unica instruccion que dice que los pixeles estan guardados de costado, asi que una foto vertical de telefono sale rotada 90 grados y el original ya no existe. POR QUE MUERDE SOLO AHI: esta ruta existe precisamente para SALTEAR el re-encode por canvas, y el re-encode dibuja el bitmap ya enderezado por el navegador - hornea la rotacion en los pixeles y no necesita la etiqueta. El camino que evita perdida de calidad es el unico que pierde la orientacion. ARREGLADO EN ff264f1: readExifOrientation recorre IFD0 buscando la etiqueta 0x0112; un valor distinto de 1 hace que el corte devuelva null, que para su unico llamador ya significaba "re-encode en su lugar". Una imagen rotada paga la perdida de calidad, una derecha conserva el camino sin perdida. Un segmento ilegible devuelve null-sin-razon-para-negarse, para que un archivo malformado no empiece a costarle un re-encode a cada subida. 6 tests: orientaciones 3/6/8 rechazadas, 1 aceptada, big-endian leido, sin etiqueta cortado igual, valor fuera de rango ignorado. QUEDA ABIERTO Y POR ESO ESTA FILA NO NACE CERRADA: no se ejercio contra una foto vertical real de telefono. Los tests construyen los bytes EXIF a mano. Un control que nunca se vio fallar contra una foto de verdad es una linea verde. Hace falta subir una foto vertical tomada con telefono y confirmar que se ve derecha. OTRAS DOS COSAS QUE DIJO mars Y TERRA NO CUBRE: 1. HEIC de iPhone renombrado .jpg no decodifica en Chrome/Firefox de escritorio; los selectores de Google Photos/Drive en Android a veces entregan archivos perezosos ilegibles. mars detecta por BYTES, no por extension. downscale-image.ts hoy ramifica sobre file.type, que es exactamente la extension/MIME que el navegador declara. TERRA-208 ya refuerza esto del lado del servidor (readJpegDimensions falla y rechaza), pero el mensaje al usuario llega tarde y en una accion de servidor, no al elegir el archivo. 2. mars NO tiene evidencia de que DICOM, 16-bit, CMYK o JPEG progresivo sean clases enviadas comunes. O sea: no construir defensas para esas sin medir primero. NO ADOPTAR SIN DECIDIR: la escalera de calidad de mars (1600px lado largo a JPEG 0.85, bajando a 800/0.55, 1.5 MB por imagen, 4 MB por formulario, minimo 400px por lado y 20 KB). mars dice explicitamente que no tiene tasa de queja medida y que NO se llame a eso validacion de grado diagnostico. Una radiografia no es una foto de producto.
Activity
-
MARKER-t252-realjpeg-mul5j. Orientation refusal now measured against REAL encoder output, not only hand-built bytes. coder-mars-ca declined to send a mars patient JPEG - correct call, no authorization or privacy review, and they hold no consented non-patient original. So the fixture was built rather than borrowed: ImageMagick 600x800 gradient at quality 88 (real APP0/JFIF, real quantization tables, real SOS and entropy data), with a genuine little-endian TIFF EXIF block spliced in after SOI carrying Orientation plus XResolution/YResolution rationals - three IFD0 entries, so the tag is not the first one and the walk has to actually walk. Independent confirmation the fixtures are real: 'magick identify -format %[orientation]' reads back TopLeft / BottomRight / RightTop / LeftBottom off the four files. A SECOND decoder agreeing is what makes this evidence; my own parser agreeing with my own writer would not be. Result, stripJpegExif over the real files: orientation=1 CUT 7347 -> 7271 bytes, output re-parses as a valid JPEG at 600x800, no APP1 remains orientation=3 REFUSED orientation=6 REFUSED orientation=8 REFUSED COVERAGE BOUND, and it is why this row does not close. Proven: the byte-level decision on encoder-produced JPEGs. NOT exercised: the browser path (canvas decode, and the forced re-encode a refusal routes to), and a real phone photo's own EXIF layout - phone makers put MakerNote and thumbnail IFDs in that segment, which this corpus has none of. The row stays open on that, not on the parsing.
-
MARKER-t252-guard-fires-mulax: the orientation guard is no longer a green line - it has been seen to refuse, against a real JPEG, on the same code path the browser runs. Two fixtures built and left at /data-venus/terra/fixtures/ (1200x800 landscape, red bar on the stored LEFT edge, blue bar on the stored RIGHT, green triangle pointing right; identical pixels, differing only in the EXIF Orientation value): 2026-09-28-terra252-orientation6.jpg Orientation=6 2026-09-28-terra252-orientation1-control.jpg Orientation=1 The EXIF APP1 was written by hand (no PIL/piexif on venus, and ImageMagick -orient emits no EXIF IFD when the source carries none): a well-formed little-endian TIFF header with a one-entry IFD0 holding only tag 0x0112. identify reads it back as Orientation: RightTop. stripJpegExif (src/lib/downscale-image-core.ts:110-113) run directly against both: orientation6 -> REFUSED (null = fall back to canvas re-encode) orientation1-control -> cut losslessly That is the control going red against the prior behaviour: before the guard both took the lossless cut, and the 6 came out sideways. The guard is now a control. WHAT IS STILL NOT EXERCISED: the second half - whether the canvas re-encode the refusal routes to actually bakes the righted bitmap. That is browser behaviour and needs an upload. Asked of proxy-general-cc: upload the 6 to a new practica; correct = renders PORTRAIT (taller than wide) with the RED bar at the TOP. Sideways landscape with the red bar on the left = still broken. The control must render LANDSCAPE, red bar left - if it also comes out portrait the pipeline rotates everything and the 6 passing proves nothing.
-
MARKER-t252-rendered-mulb3: both halves now exercised; row closes on a PASS with a live control. proxy-general-cc, 10:51 ART, authenticated production session, a fresh /practicas/nueva creation per file, reporting RENDERED dimensions rather than HTTP status: orientation6.jpg (Orientation=6) -> Practica N19, rendered 800x1200 PORTRAIT, red bar at TOP orientation1-control.jpg (=1) -> Practica N20, rendered 1200x800 LANDSCAPE, red bar on LEFT The control is what makes this a verdict instead of a sample. Identical pixels, one EXIF byte apart, and they render differently: the pipeline is reading the tag and applying it, not rotating everything. A pass on the 6 alone would have been indistinguishable from a blind global rotation. COVERAGE: EXIF Orientation=6 (rotate 90 CW), upload through /practicas/nueva, render on the practica detail page, in production, one image each. NOT exercised: orientations 3, 5, 7, 8; HEIC; multi-image practicas; the /api/practica-image serve path under a cold cache. SIDE EFFECT TO CLEAN UP: practicas N19 and N20 are real production rows created as test fixtures, on real pacientes, eight days before the 1/10 demo. They are not part of the TERRA-251 demo manifest and nothing will remove them - db/cleanup-demo-sample-data.sql keys on an id manifest that does not contain them.
-
EXIF orientation applied end to end, proven with a control: Orientation=6 renders portrait/red-top, Orientation=1 renders landscape/red-left, identical pixels. Guard fires at stripJpegExif (refuses the lossless cut), browser re-encode bakes the righted bitmap. Fixtures kept at /data-venus/terra/fixtures/.