Anteprime, decompressione LZW e contenuto dei disegni
Aggiunge tre capacita' al lettore, tutte senza dipendenze esterne.
Anteprima incorporata (preview.py)
Ogni file contiene una miniatura JPEG dell'ultimo salvataggio: il
render del pezzo nei modelli, l'immagine della tavola nei disegni.
Non e' referenziata dall'indice, quindi va cercata per firma: ogni
candidato viene validato percorrendone i segmenti JPEG, cosi' i byte
che iniziano per FFD8FF per caso non passano. Sui file campione il
risultato e' esattamente una immagine per file.
Decompressione compress(1) (lzw.py)
Creo comprime alcuni blocchi nel vecchio formato .Z. Rispetto a un
LZW generico va riprodotto l'allineamento a gruppi di otto codici:
senza, la decompressione produce qualche centinaio di byte corretti
e poi diverge silenziosamente.
Contenuto dei disegni (drawing.py)
Un .drw non ha la tabella parametri del modello ma contiene la
rappresentazione vettoriale della tavola. Ne estraiamo i testi:
cartiglio, note, tolleranze, quote. Le tavole diventano cosi'
ricercabili per contenuto. Nel disegno i valori compaiono gia'
risolti come testo, compreso il peso, mentre nel .prt sono ancora
in codifica binaria.
I blocchi oltre l'indice sono ora interpretati da ugc.iter_blocks, che
decomprime quando serve: lo stesso blocco puo' essere memorizzato
compresso in un file e in chiaro in un altro, quindi la scelta va fatta
guardando i byte e non il nome. Cercare soltanto gli stream compressi
faceva perdere i testi su un terzo dei disegni.
Sulla codifica dei numeri: sono double IEEE big-endian con i byte bassi
omessi, preceduti da un byte-tag. Lo dimostrano i disegni, dove size_x e
size_y valgono 420,0 x 297,0 (un A3 esatto). La mappatura del byte-tag
resta pero' incerta, quindi PESO e DENSITA continuano a essere riportati
come encoded_not_decoded: un valore plausibile ma sbagliato sarebbe
peggio di un valore assente.
La geometria 3D resta fuori portata, e ora e' verificato: in 200 KB di
blocchi geometrici non compare una sola costante 1.0 in nessuna codifica
in virgola mobile. Quei blocchi sono compressi con un algoritmo che non
espone firme note.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""Estrazione dell'anteprima incorporata nei file nativi Creo.
|
||||
|
||||
Creo salva nel file una miniatura JPEG dell'ultimo salvataggio: il render
|
||||
ombreggiato del pezzo per i modelli, l'immagine della tavola per i disegni.
|
||||
E' la stessa immagine che compare nella finestra "Apri" di Creo.
|
||||
|
||||
La miniatura non e' referenziata dall'indice UGC_TOC, quindi la si individua
|
||||
per firma. Per non scambiare dati binari qualsiasi per un'immagine, ogni
|
||||
candidato viene validato percorrendone i segmenti JPEG.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
|
||||
SOI = b"\xff\xd8\xff"
|
||||
EOI = b"\xff\xd9"
|
||||
|
||||
#: Marcatori "Start of Frame": contengono le dimensioni dell'immagine.
|
||||
_SOF = {0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF}
|
||||
_SOS = 0xDA
|
||||
_STANDALONE = {0xD8, 0xD9, 0x01} | set(range(0xD0, 0xD8))
|
||||
|
||||
MIN_BYTES = 512
|
||||
|
||||
|
||||
@dataclass
|
||||
class Preview:
|
||||
"""Una miniatura trovata nel file."""
|
||||
|
||||
data: bytes
|
||||
offset: int
|
||||
width: int
|
||||
height: int
|
||||
format: str = "jpeg"
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"present": True,
|
||||
"format": self.format,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"bytes": len(self.data),
|
||||
"offset": hex(self.offset),
|
||||
}
|
||||
|
||||
def save(self, path: str) -> None:
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(self.data)
|
||||
|
||||
|
||||
def _parse_jpeg(data: bytes, start: int) -> Preview | None:
|
||||
"""Percorre i segmenti a partire da `start`. Restituisce None se la
|
||||
sequenza non e' un JPEG completo e coerente."""
|
||||
pos = start + 2 # oltre il SOI
|
||||
size = None
|
||||
|
||||
while pos < len(data) - 3:
|
||||
if data[pos] != 0xFF:
|
||||
return None
|
||||
marker = data[pos + 1]
|
||||
|
||||
if marker == 0xFF: # byte di riempimento
|
||||
pos += 1
|
||||
continue
|
||||
if marker in _STANDALONE:
|
||||
pos += 2
|
||||
continue
|
||||
|
||||
length = struct.unpack(">H", data[pos + 2 : pos + 4])[0]
|
||||
if length < 2:
|
||||
return None
|
||||
|
||||
if marker in _SOF and pos + 9 <= len(data):
|
||||
height, width = struct.unpack(">HH", data[pos + 5 : pos + 9])
|
||||
size = (width, height)
|
||||
|
||||
if marker == _SOS:
|
||||
# I dati compressi seguono il segmento: il byte FF che vi compare
|
||||
# e' sempre preceduto da escape, quindi il primo EOI e' la fine.
|
||||
end = data.find(EOI, pos + 2 + length)
|
||||
if end < 0 or size is None:
|
||||
return None
|
||||
blob = data[start : end + 2]
|
||||
if len(blob) < MIN_BYTES:
|
||||
return None
|
||||
return Preview(blob, start, size[0], size[1])
|
||||
|
||||
pos += 2 + length
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_all(data: bytes) -> "list[Preview]":
|
||||
"""Tutte le miniature valide presenti nel file, in ordine di comparsa."""
|
||||
found, pos = [], 0
|
||||
while True:
|
||||
pos = data.find(SOI, pos)
|
||||
if pos < 0:
|
||||
return found
|
||||
preview = _parse_jpeg(data, pos)
|
||||
if preview:
|
||||
found.append(preview)
|
||||
pos += len(preview.data)
|
||||
else:
|
||||
pos += 1
|
||||
|
||||
|
||||
def find(data: bytes) -> Preview | None:
|
||||
"""La miniatura piu' grande, che e' quella a risoluzione migliore."""
|
||||
candidates = find_all(data)
|
||||
if not candidates:
|
||||
return None
|
||||
return max(candidates, key=lambda p: (p.width * p.height, len(p.data)))
|
||||
Reference in New Issue
Block a user