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>
196 lines
6.4 KiB
Python
196 lines
6.4 KiB
Python
"""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
|