Lettore di metadati per file nativi Creo Parametric
Prototipo CLI in Python che estrae parametri e proprieta' dai file .prt senza avviare Creo e senza SDK commerciali. Solo libreria standard. I file nativi Creo sono container con header ASCII e un indice delle sezioni (UGC_TOC); la maggior parte delle sezioni non e' compressa e la tabella dei parametri e' leggibile direttamente. Ogni parametro compare in due rappresentazioni indipendenti (tabella estesa in LargeText e copia neutra in NeuPrtSld). Il lettore le confronta entrambe, e ne ricava: - confidence: 0.99 quando le due copie concordano, valori inferiori se il dato compare una volta sola o se divergono (status "conflict"); - owner: distingue i parametri del modello da quelli delle feature, che vivono solo nella copia neutra. I parametri numerici (PESO, DENSITA) usano una codifica a lunghezza variabile non ancora risolta: vengono riportati come "encoded_not_decoded" conservando i byte grezzi, senza inventare valori. Validato su file PART e PART/SHEETMETAL scritti da Creo 9.0.3.0. Assiemi, family table e versioni precedenti non sono ancora stati verificati. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
"""Estrazione della tabella parametri dal corpo di un file nativo Creo.
|
||||
|
||||
I parametri sono memorizzati in chiaro, con una struttura regolare che si
|
||||
ripete per ogni voce:
|
||||
|
||||
f6 e3 <NOME> 00 <slot> 88 00 e3 <tipo> <valore> ...
|
||||
|
||||
dove `tipo` e' 0x33 ('3') per le stringhe e 0x32 ('2') per i numerici.
|
||||
Ogni parametro compare due volte di seguito (valore corrente e valore di
|
||||
riferimento) e l'intera tabella e' duplicata in due sezioni del file
|
||||
(tipicamente `LargeText` e `NeuPrtSld`). Usiamo questa ridondanza per
|
||||
assegnare un livello di confidenza a ogni valore.
|
||||
|
||||
I valori stringa sono leggibili direttamente. I numerici usano una codifica
|
||||
binaria a lunghezza variabile (7-8 byte osservati) che non e' un double IEEE
|
||||
lineare e che qui non viene decodificata: ne conserviamo i byte grezzi.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
REC_START = b"\xf6\xe3"
|
||||
VALUE_LEAD = b"\x88\x00\xe3"
|
||||
NUM_LEAD = b"\xe3\x32"
|
||||
|
||||
KIND_STRING = 0x33
|
||||
KIND_NUMERIC = 0x32
|
||||
|
||||
#: Marcatore di espressione: il valore non e' costante ma prodotto da una
|
||||
#: relazione del modello (es. PESO guidato da "value(d_val)").
|
||||
EXPR_LEAD = b"\xe0\x02"
|
||||
|
||||
MAX_STRING = 256
|
||||
MAX_NUMERIC_SPAN = 48
|
||||
|
||||
# Byte ammessi in un valore stringa: ASCII stampabile piu' l'intervallo alto
|
||||
# per le lettere accentate, esclusi i byte usati come token dal formato.
|
||||
_TOKENS = {0xE1, 0xE3, 0xF6, 0xF7}
|
||||
_STRING_BYTES = frozenset(
|
||||
set(range(0x20, 0x7F)) | (set(range(0xA0, 0x100)) - _TOKENS)
|
||||
)
|
||||
|
||||
_NAME = rb"([A-Za-z_][A-Za-z0-9_.\-]{0,63})\x00"
|
||||
|
||||
#: Forma estesa, nella tabella parametri principale (sezione LargeText).
|
||||
_RECORD_RE = re.compile(
|
||||
REC_START + _NAME
|
||||
+ rb"(.)" # byte di slot/identificativo
|
||||
+ re.escape(VALUE_LEAD)
|
||||
+ rb"([\x32\x33])", # tipo del valore
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
#: Forma compatta, nella rappresentazione neutra (sezione NeuPrtSld). Ripete
|
||||
#: gli stessi parametri e ci serve come verifica incrociata. Il prefisso che
|
||||
#: separa i record varia a seconda dei campi accessori presenti (ref_id,
|
||||
#: unit, designated, ...), quindi ci ancoriamo solo al nome seguito dal tipo.
|
||||
_NEUTRAL_RE = re.compile(
|
||||
rb"\xe3" + _NAME + rb"\xe2([\x32\x33])",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Occurrence:
|
||||
"""Una singola comparsa di un parametro nel file."""
|
||||
|
||||
name: str
|
||||
kind: str # "string" | "real"
|
||||
value: str | None # None se non decodificato
|
||||
raw: bytes | None # byte grezzi, per i numerici
|
||||
offset: int
|
||||
section: str = ""
|
||||
expression: str | None = None # relazione che pilota il valore
|
||||
form: str = "extended" # "extended" (tabella modello) | "neutral"
|
||||
|
||||
@property
|
||||
def decoded(self) -> bool:
|
||||
return self.value is not None
|
||||
|
||||
|
||||
def _decode_text(raw: bytes) -> str:
|
||||
"""I valori sono scritti in UTF-8; i file piu' vecchi possono usare la
|
||||
codepage Windows."""
|
||||
try:
|
||||
return raw.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return raw.decode("cp1252", errors="replace")
|
||||
|
||||
|
||||
def _read_string(data: bytes, pos: int) -> tuple[str, int] | None:
|
||||
"""Legge un valore stringa terminato da NUL. Ritorna (valore, fine)."""
|
||||
# Un token f7 xx puo' precedere il valore (flag di stato del parametro).
|
||||
if data[pos : pos + 1] == b"\xf7":
|
||||
pos += 2
|
||||
|
||||
end = pos
|
||||
limit = min(len(data), pos + MAX_STRING)
|
||||
while end < limit and data[end] != 0x00:
|
||||
if data[end] not in _STRING_BYTES:
|
||||
return None
|
||||
end += 1
|
||||
if end >= limit:
|
||||
return None # nessun terminatore: non e' un record valido
|
||||
|
||||
return _decode_text(data[pos:end]), end + 1
|
||||
|
||||
|
||||
def _read_numeric(data: bytes, pos: int) -> tuple[bytes, int] | None:
|
||||
"""Isola i byte grezzi di un valore numerico.
|
||||
|
||||
Sfrutta il fatto che il valore e' immediatamente ripetuto dopo un secondo
|
||||
marcatore `e3 32`: proviamo ogni posizione candidata e teniamo la prima in
|
||||
cui la ripetizione combacia davvero.
|
||||
"""
|
||||
start = pos
|
||||
# Un token f7 xx puo' precedere il valore.
|
||||
if data[start : start + 1] == b"\xf7":
|
||||
start += 2
|
||||
|
||||
search_end = min(len(data), start + MAX_NUMERIC_SPAN)
|
||||
cursor = start
|
||||
while True:
|
||||
repeat = data.find(NUM_LEAD, cursor, search_end)
|
||||
if repeat < 0:
|
||||
return None
|
||||
|
||||
candidate = data[start:repeat]
|
||||
# Un token di separazione puo' chiudere il primo valore.
|
||||
if candidate[-2:-1] == b"\xf7":
|
||||
candidate = candidate[:-2]
|
||||
|
||||
after = repeat + len(NUM_LEAD)
|
||||
if candidate and data[after : after + len(candidate)] == candidate:
|
||||
return candidate, after + len(candidate)
|
||||
|
||||
cursor = repeat + 1
|
||||
|
||||
|
||||
def _read_expression(data: bytes, pos: int) -> tuple[str | None, int]:
|
||||
"""Se il valore e' pilotato da una relazione, ne legge il testo."""
|
||||
if data[pos : pos + len(EXPR_LEAD)] != EXPR_LEAD:
|
||||
return None, pos
|
||||
parsed = _read_string(data, pos + len(EXPR_LEAD))
|
||||
if parsed is None:
|
||||
return None, pos
|
||||
return parsed
|
||||
|
||||
|
||||
def iter_occurrences(data: bytes) -> "list[Occurrence]":
|
||||
"""Scorre il file e restituisce ogni comparsa di parametro trovata.
|
||||
|
||||
Vengono percorse entrambe le rappresentazioni presenti nel file: la
|
||||
tabella parametri estesa e la copia neutra. La ridondanza serve poi ad
|
||||
assegnare la confidenza in `extract`.
|
||||
"""
|
||||
found: list[Occurrence] = []
|
||||
|
||||
for regex, kind_group, form in (
|
||||
(_RECORD_RE, 3, "extended"),
|
||||
(_NEUTRAL_RE, 2, "neutral"),
|
||||
):
|
||||
for match in regex.finditer(data):
|
||||
name = match.group(1).decode("ascii")
|
||||
kind_byte = match.group(kind_group)[0]
|
||||
pos = match.end()
|
||||
|
||||
if kind_byte == KIND_STRING:
|
||||
parsed = _read_string(data, pos)
|
||||
if parsed is None:
|
||||
continue
|
||||
found.append(
|
||||
Occurrence(
|
||||
name, "string", parsed[0], None, match.start(), form=form
|
||||
)
|
||||
)
|
||||
else:
|
||||
expression, pos = _read_expression(data, pos)
|
||||
parsed = _read_numeric(data, pos)
|
||||
found.append(
|
||||
Occurrence(
|
||||
name,
|
||||
"real",
|
||||
None,
|
||||
parsed[0] if parsed else None,
|
||||
match.start(),
|
||||
expression=expression,
|
||||
form=form,
|
||||
)
|
||||
)
|
||||
|
||||
found.sort(key=lambda o: o.offset)
|
||||
return found
|
||||
Reference in New Issue
Block a user