"""Independent, bounded reader for the documented PcbDoc 6.0 binary record family.""" import hashlib import io import math import re import struct from collections import Counter import olefile from app.models.schematic import Sheet, Point, Bounds, Diagnostic, BoardLayer, Net from app.models.pcb import PCBData as Document, Footprint as Component, PCBObject as SchematicObject, PCBPrimitive as Primitive from .record_decoder import ParseError, properties from .pcb_binary import Reader, at, text_records, binary_records, wide_strings from .pcb_geometry import ( raw_unit, unit, xy, ellipse_path, arc_path, pad_shape, property_outline, ) from .geometry import bounds, union, poly_path, transform_point MAX_TOTAL = 64 * 1024 * 1024 SPECIAL = { 32: ("Bottom Layer", "#5484ed", "copper"), 33: ("Top Overlay", "#f6e4b1", "silkscreen"), 34: ("Bottom Overlay", "#c29ef1", "silkscreen"), 35: ("Top Paste", "#92b8a0", "paste"), 36: ("Bottom Paste", "#81a0b5", "paste"), 37: ("Top Solder", "#966ac1", "mask"), 38: ("Bottom Solder", "#b78b99", "mask"), 55: ("Drill Guide", "#d5d8dc", "mechanical"), 56: ("Keep-Out Layer", "#d374dc", "mechanical"), 73: ("Drill Drawing", "#cccbd0", "mechanical"), 74: ("Multi-Layer", "#d9b660", "copper"), 81: ("Drill holes", "#08151b", "drill"), 0: ("Board Outline", "#67bc9d", "outline"), } def layer_number(name): name = name.upper().replace(" ", "").replace("_", "") fixed = { "TOP": 1, "TOPLAYER": 1, "BOTTOM": 32, "BOTTOMLAYER": 32, "TOPOVERLAY": 33, "BOTTOMOVERLAY": 34, "TOPPASTE": 35, "BOTTOMPASTE": 36, "TOPSOLDER": 37, "BOTTOMSOLDER": 38, "KEEPOUT": 56, "KEEPOUTLAYER": 56, "MULTILAYER": 74, "DRILLGUIDE": 55, "DRILLDRAWING": 73, } if name in fixed: return fixed[name] m = re.fullmatch(r"(MID(?:LAYER)?|PLANE|MECHANICAL)(\d+)", name) if m: n = int(m[2]) if m[1].startswith("MID"): return n + 1 if m[1] == "PLANE": return n + 38 return n + 56 if n <= 16 else 0x01020000 + n return 0 def resolve_layer(legacy, extended=0): # The standard copper/overlay IDs remain authoritative in V6 records. if 0x01020011 <= extended <= 0x0102FFFF: return extended return legacy def parse_pcbdoc(data: bytes, filename: str, document_id: str, **options) -> Document: if not data.startswith(b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"): raise ParseError("PcbDoc must be an OLE compound document") try: ole = olefile.OleFileIO( io.BytesIO(data), raise_defects=olefile.DEFECT_INCORRECT ) except (OSError, ValueError, struct.error) as exc: raise ParseError(f"Invalid PCB container: {exc}") from exc with ole: entries = ole.listdir(streams=True, storages=False) if len(entries) > 1024: raise ParseError("Excessive PCB stream count") streams = [{"name": "/".join(e), "size": ole.get_size(e)} for e in entries] if ( any(s["size"] > 32 * 1024 * 1024 for s in streams) or sum(s["size"] for s in streams) > MAX_TOTAL ): raise ParseError("PCB stream size limit exceeded") def read(name, required=False): if not ole.exists(name): if required: raise ParseError(f"Missing PCB stream {name}") return b"" return ole.openstream(name).read() header = read("FileHeaderSix", True) hreader = Reader(header, "FileHeaderSix") header_length = hreader.read("I") envelope = hreader.pascal() if header_length != len(envelope): raise ParseError("PCB version string length mismatch") if envelope != "PCB 6.0 Binary File": raise ParseError(f"Unsupported PCB envelope: {envelope[:100]}") board_records = text_records(read("Board6/Data", True), "Board6") if len(board_records) != 1: raise ParseError("Expected exactly one Board6 property record") board = board_records[0] if board.get("KIND") != "Protel_Advanced_PCB" or board.get("VERSION") not in ( "5.0", "5.01", "6.0", ): raise ParseError(f"Unsupported PCB board version {board.get('VERSION')}") diagnostics = [] grouped = {} counts = {} variants = {} objects = [] primitives = [] layers = {} component_boxes = {} def report(code, message, record=None, severity="warning"): key = (code, message) if key in grouped: grouped[key].count += 1 return d = Diagnostic( severity=severity, code=code, message=message, record_id=record ) diagnostics.append(d) grouped[key] = d def ensure_layer(n): key = str(n) if key in layers: return layers[key] if n == 1: name, col, category = "Top Layer", "#d5624e", "copper" elif 2 <= n <= 31: name, col, category = ( f"Mid Layer {n - 1}", ("#a5bd59", "#b67ac7", "#d19e4d", "#63bdb5")[(n - 2) % 4], "copper", ) elif n in SPECIAL: name, col, category = SPECIAL[n] elif 39 <= n <= 54: name, col, category = f"Internal Plane {n - 38}", "#698a68", "plane" elif 57 <= n <= 72 or n >= 0x01020000: name, col, category = ( f"Mechanical {n - 56 if n < 100 else n - 0x01020000}", "#8196a8", "mechanical", ) else: name, col, category = f"Layer {n}", "#8196a8", "unknown" name = board.get(f"LAYER{n}NAME", name) if n >= 0x01020000: for k, v in board.items(): if ( k.startswith("LAYERV7_") and k.endswith("LAYERID") and v == str(n) ): name = board.get(k[:-7] + "NAME", name) order = ( 0 if n == 0 else 1000 if n == 81 else 900 if n == 33 else 890 if n == 34 else 800 if n == 74 else 700 if n == 1 else 600 if n == 32 else 500 - n if 2 <= n <= 31 else 850 ) layer = BoardLayer( id=key, name=name, color=col, category=category, visible=n in (0, 1, 33, 74, 81), order=order, ) layers[key] = layer return layer def props_records(name): result = text_records(read(name + "/Data"), name) check_count(name, len(result)) return result def check_count(name, count): counts[name] = count hdr = read(name + "/Header") if len(hdr) == 4 and at(hdr, 0, "I") != count: report( "record_count_mismatch", f"{name}: header declares {at(hdr, 0, 'I')}, decoded {count}", ) def typed(name, tag, blocks=1): count = 0 for idx, parts in binary_records(read(name + "/Data"), name, tag, blocks): count += 1 signature = ",".join(str(len(p)) for p in parts) variants.setdefault(name, Counter())[signature] += 1 if sum(counts.values()) + count > 100000: raise ParseError("Excessive PCB object count") yield idx, parts check_count(name, count) uid_map = {} uid_stream = text_records(read("UniqueIDPrimitiveInformation/Data"), "UniqueIDPrimitiveInformation") for item in uid_stream: key = (item.get("PRIMITIVEOBJECTID", "").lower(), int(item.get("PRIMITIVEINDEX", -1))) if key in uid_map: raise ParseError("Duplicate primitive UID mapping") uid_map[key] = item.get("UNIQUEID", "") nets = [ Net(id=f"net:{i}", name=p.get("NAME", f"Net {i}"), properties=p) for i, p in enumerate(props_records("Nets6")) ] components = [] for i, p in enumerate(props_records("Components6")): if not {"X","Y","LAYER"}.issubset(p): raise ParseError(f"Components6:{i}: missing native placement fields") pos = Point(x=unit(p.get("X", "0mil")), y=-unit(p.get("Y", "0mil"))) components.append( Component( id=f"Components6:{i}", designator=p.get("SOURCEDESIGNATOR", ""), name=p.get("SOURCELIBREFERENCE", p.get("PATTERN", "")), value=p.get("COMMENT", ""), comment=p.get("COMMENT", ""), description=p.get("SOURCEDESCRIPTION", ""), library_reference=p.get("PATTERN", ""), source_library=p.get("SOURCEFOOTPRINTLIBRARY", ""), position=pos, rotation=float(p.get("ROTATION", 0)), mirrored=layer_number(p.get("LAYER", "TOP")) == 32, bounds=bounds([pos], 1), parameters=p, ) ) if any(not math.isfinite(c.rotation) for c in components): raise ParseError("Non-finite footprint rotation") def common(name, index, p, ext=0): if len(p) < 13: raise ParseError(f"{name}:{index}: short primitive header") net, component = at(p, 3, "H"), at(p, 7, "H") if net != 65535 and net >= len(nets): raise ParseError(f"{name}:{index}: invalid net index {net}") if component != 65535 and component >= len(components): raise ParseError(f"{name}:{index}: invalid component index {component}") lid = resolve_layer(p[0], ext) layer = ensure_layer(lid) c = components[component] if component != 65535 else None props = { "LAYER_ID": str(lid), "NET_INDEX": str(net), "NET_NAME": nets[net].name if net != 65535 else "", "COMPONENT_INDEX": str(component), "LOCKED": str(not bool(p[1] & 4)), "FLAGS_1": str(p[1]), "FLAGS_2": str(p[2]), } obj = SchematicObject( id=f"{name}:{index}", kind=name.removesuffix("6").lower(), native_uid=uid_map.get(({"Tracks6":"track","Arcs6":"arc","Pads6":"pad","Vias6":"via","Texts6":"text","Fills6":"fill","Regions6":"region","ShapeBasedRegions6":"region"}.get(name,""),index), ""), record_type={ "Tracks6": 4, "Arcs6": 1, "Pads6": 2, "Vias6": 3, "Texts6": 5, "Fills6": 6, "Regions6": 11, "ShapeBasedRegions6": 11, }.get(name, 0), source_stream=name, source_index=index, owner_id=c.id if c else None, component_id=c.id if c else None, layer_id=layer.id, net_id=nets[net].id if net != 65535 else None, position=Point(x=0, y=0), bounds=bounds([]), properties=props, ) objects.append(obj) layer.object_count += 1 if c: c.object_ids.append(obj.id) return obj, c def shape( obj, path, box, fill="none", width=0.0, layer_id=None, role="", stroke=None, fill_rule="nonzero", position=None, text="", rotation=0, mirrored=False, font_size=10, font_family="Arial", bold=False, italic=False, ): layer = ensure_layer( int(layer_id if layer_id is not None else obj.layer_id or 0) ) col = layer.color primitive = Primitive( id=f"{obj.id}:p{len(obj.primitive_ids)}", kind="text" if text else "path", object_id=obj.id, component_id=obj.component_id, layer_id=layer.id, net_id=obj.net_id, role=role, path=path, bounds=box, stroke=col if stroke is None else stroke, fill=col if fill == "layer" else fill, width=width, fill_rule=fill_rule, position=position or obj.position, text=text, rotation=rotation, mirrored=mirrored, font_size=font_size, font_family=font_family, bold=bold, italic=italic, ) if len(primitives) >= 250000: raise ParseError("PCB drawable limit exceeded") primitives.append(primitive) obj.primitive_ids.append(primitive.id) obj.bounds = union([obj.bounds, box]) if len(obj.primitive_ids) > 1 else box if obj.component_id: ci = int(obj.component_id.split(":")[1]) components[ci].primitive_ids.append(primitive.id) if layer.category not in ("mechanical", "paste", "mask"): component_boxes.setdefault(obj.component_id, []).append(box) return primitive # Outline is extracted from actual Board6 vertices, including rounded corners. outline, board_bounds = property_outline(board) if not outline: raise ParseError("PCB has no supported Board6 outline") outline_obj = SchematicObject( id="Board6:0", kind="board_outline", record_type=0, source_stream="Board6", source_index=0, layer_id="0", position=Point(x=board_bounds.x, y=board_bounds.y), bounds=board_bounds, properties=board, ) objects.append(outline_obj) shape( outline_obj, outline, board_bounds, fill="#13312e", width=0.18, role="board" ) counts["Board6"] = 1 # Preserve polygon definitions as outlines only; the filled copper is read from Regions6. polygons = props_records("Polygons6") for i, p in enumerate(polygons): lid = layer_number(p.get("LAYER", "TOP")) layer = ensure_layer(lid) path, box = property_outline(p) net = int(p.get("NET", 65535)) if net != 65535 and not 0 <= net < len(nets): raise ParseError("Invalid polygon net reference") o = SchematicObject( id=f"Polygons6:{i}", kind="polygon_definition", native_uid=p.get("UNIQUEID", ""), record_type=0, source_stream="Polygons6", source_index=i, layer_id=layer.id, net_id=nets[net].id if 0 <= net < len(nets) else None, visible=False, position=Point(x=box.x, y=box.y), bounds=box, properties=p, ) objects.append(o) for i, (p,) in typed("Tracks6", 4): obj, c = common("Tracks6", i, p, at(p, 41, "I", 0)) a, b = xy(*at(p, 13, "ii")), xy(*at(p, 21, "ii")) w = raw_unit(at(p, 29, "i")) if w < 0: raise ParseError("Negative track width") obj.position = a obj.connection_points = [a, b] obj.kind = "track" obj.properties["WIDTH_MIL"] = str(w * 10) polygon = at(p, 5, "H") obj.properties["POLYGON_INDEX"] = str(polygon) shape( obj, poly_path([a, b]), bounds([a, b], w / 2), width=w, role="polygon_outline" if p[1] & 2 else "track", ) for i, (p,) in typed("Arcs6", 1): obj, c = common("Arcs6", i, p, at(p, 52, "I", 0)) pos = xy(*at(p, 13, "ii")) r = raw_unit(at(p, 21, "i")) start, end = at(p, 25, "dd") w = raw_unit(at(p, 41, "i")) if w < 0: raise ParseError("Negative arc width") obj.kind = "arc" obj.position = pos obj.properties.update( { "RADIUS_MIL": str(r * 10), "START_ANGLE": str(start), "END_ANGLE": str(end), "WIDTH_MIL": str(w * 10), } ) shape( obj, arc_path(pos, r, start, end), Bounds( x=pos.x - r - w / 2, y=pos.y - r - w / 2, width=2 * r + w, height=2 * r + w, ), width=w, role="track", ) for i, (p,) in typed("Fills6", 6): obj, c = common("Fills6", i, p, at(p, 42, "I", 0)) a, b = xy(*at(p, 13, "ii")), xy(*at(p, 21, "ii")) rotation = at(p, 29, "d") center = Point(x=(a.x + b.x) / 2, y=(a.y + b.y) / 2) obj.position = center obj.kind = "fill" path, box = pad_shape(center, abs(b.x - a.x), abs(b.y - a.y), 2, rotation) obj.properties["ROTATION"] = str(rotation) shape(obj, path, box, fill="layer", role="fill") # Render saved tessellated regions with exact interior holes. Keep definition streams separate. def regions(name, extended): for i, (p,) in typed(name, 11): obj, c = common(name, i, p) obj.kind = "region" reader = Reader(p, name) reader.take(14) hole_count = reader.read("H") reader.take(2) props = properties(bytes(reader.block())) obj.properties.update(props) n = reader.read("I") + (1 if extended else 0) if n > 50000 or hole_count > 20000: raise ParseError("Excessive region geometry") vertices = [] commands = [] all_points = [] for _ in range(n): if extended: curved = reader.read("B") pos = xy(*reader.read("ii")) center = xy(*reader.read("ii")) r = raw_unit(reader.read("i")) sa, ea = reader.read("dd") vertices.append((pos, curved, center, r, sa, ea)) else: vertices.append((xy(*reader.read("dd")), 0, None, 0, 0, 0)) for j, (pos, curved, center, r, sa, ea) in enumerate(vertices): commands.append(("M" if j == 0 else "L") + f"{pos.x:.15g},{pos.y:.15g}") all_points.append(pos) if curved and r > 0: arc = arc_path(center, r, sa, ea) if " " in arc: commands.extend( ["L" + arc.split(" ", 1)[0][1:], arc.split(" ", 1)[1]] ) all_points.extend( [ Point(x=center.x - r, y=center.y - r), Point(x=center.x + r, y=center.y + r), ] ) commands.append("Z") for _ in range(hole_count): count = reader.read("I") if count > 50000: raise ParseError("Excessive region hole points") pts = [xy(*reader.read("dd")) for _ in range(count)] commands.append(poly_path(pts, True)) all_points.extend(pts) obj.properties["OUTLINE_PATH"] = " ".join(commands) obj.bounds = bounds(all_points) obj.position = all_points[0] if all_points else Point(x=0, y=0) obj.properties["HOLE_COUNT"] = str(hole_count) obj.properties["VERTEX_COUNT"] = str(n) obj.properties["POLYGON_INDEX"] = str(at(p, 5, "H")) kind = int(props.get("KIND", 0)) polygon = at(p, 5, "H") is_cutout = props.get("ISBOARDCUTOUT", "FALSE").upper() == "TRUE" if extended: # When saved fills exist, shape definitions must not cover clearances twice. if polygon != 65535 and not is_cutout: obj.visible = False continue # Standalone shapes are taken from this stream, with circular arcs intact. role = "region" else: if polygon == 65535 and not is_cutout: obj.visible = False continue role = "pour" if kind != 0 and not is_cutout: obj.visible = False report( "region_definition", f"Region kind {kind} is retained as a definition; saved copper holes govern the fill", obj.id, "info", ) continue if is_cutout: shape( obj, " ".join(commands), obj.bounds, fill="#08151b", layer_id="0", role="board_cutout", fill_rule="evenodd", ) elif len(vertices) >= 3: shape( obj, " ".join(commands), obj.bounds, fill="layer", role=role, fill_rule="evenodd", ) if reader.remaining: report( "region_extension", f"{name} contains uninterpreted region extension bytes", obj.id, ) regions("Regions6", False) regions("ShapeBasedRegions6", True) poured = {o.properties.get("POLYGON_INDEX") for o in objects if o.source_stream == "Regions6" and o.visible} for index, definition in enumerate(polygons): if str(index) not in poured: report("polygon_without_saved_pour", "Polygons6 definition has no decoded saved copper pour; no fill is invented", f"Polygons6:{index}") # Active copper layers are obtained from legacy stack links, not from LAYERSETSCOUNT (view presets). copper_ids = [1] while True: nxt = int(board.get(f"LAYER{copper_ids[-1]}NEXT", "0")) if not nxt: break if nxt in copper_ids or nxt not in range(1,55) or 33 <= nxt <= 38: raise ParseError("Invalid copper stack links") copper_ids.append(nxt) if 32 not in copper_ids: copper_ids = [1,32] report("copper_stack", "No complete layer chain; only explicit top/bottom copper stack supported") for index, lid in enumerate(copper_ids): ensure_layer(lid).order = 700 - index * 10 for i, parts in typed("Pads6", 2, 6): name = Reader(parts[0], "Pad name").pascal() p, stack = parts[4], parts[5] if len(p) < 110: raise ParseError("Unsupported short pad geometry") obj, c = common("Pads6", i, p) obj.kind = "pad" pos = xy(*at(p, 13, "ii")) obj.position = pos sizes = [ (raw_unit(x), raw_unit(y)) for x, y in [at(p, 21, "ii"), at(p, 29, "ii"), at(p, 37, "ii")] ] hole = raw_unit(at(p, 45, "i")) if hole < 0: raise ParseError("Negative pad drill diameter") shapes = list(at(p, 49, "BBB")) rotation = at(p, 52, "d") mode = at(p, 62, "B") obj.properties.update( { "PAD_NAME": name, "HOLE_MIL": str(hole * 10), "ROTATION": str(rotation), "PLATED": str(bool(p[60])), "BOTTOM_SIZE_MIL": f"{sizes[2][0]*10:.15g} x {sizes[2][1]*10:.15g}", "MID_SIZE_MIL": f"{sizes[1][0]*10:.15g} x {sizes[1][1]*10:.15g}", "TOP_SIZE_MIL": f"{sizes[0][0] * 10:.15g} x {sizes[0][1] * 10:.15g}", "SHAPE": str(shapes[0]), "PAD_MODE": str(mode), } ) target_layers = ( copper_ids if int(obj.layer_id) == 74 else [int(obj.layer_id)] ) obj.layer_ids = [str(lid) for lid in target_layers] for lid in target_layers: if mode not in (0,1,2) or (mode == 2 and len(stack) < 596): report("pad_stack", "Unsupported pad stack mode or truncated full-stack variant", obj.id) continue which = 0 if lid == 1 else 2 if lid == 32 else 1 w, h = sizes[which] s = shapes[which] radius = 0 if mode == 2 and len(stack) >= 596 and 2 <= lid <= 30: w, h = ( raw_unit(at(stack, 4 * (lid - 2), "i")), raw_unit(at(stack, 116 + 4 * (lid - 2), "i")), ) s = stack[232 + lid - 2] stack_index = 0 if lid == 1 else 31 if lid == 32 else lid - 1 if mode == 2 and lid > 32: report("pad_plane_stack", "Full-stack pad geometry on internal plane layers is not decoded", obj.id) continue if len(stack) >= 596 and lid <= 32: alt = stack[532 + stack_index] radius = stack[564 + stack_index] if alt in (1, 2, 3, 9): s = alt elif alt != 0: report( "pad_alt_shape", f"Unsupported alternative pad shape {alt}", obj.id, ) continue try: path, box = pad_shape(pos, w, h, s, rotation, radius) except ParseError as exc: report("pad_shape", str(exc), obj.id) continue shape(obj, path, box, fill="layer", layer_id=str(lid), role="pad") if hole > 0: hole_shape = stack[262] if len(stack) >= 596 else 0 slot = raw_unit(at(stack, 263, "i")) if len(stack) >= 596 else 0 hole_angle = at(stack, 267, "d") if len(stack) >= 596 else 0 dx = raw_unit(at(stack, 275, "i")) if len(stack) >= 596 else 0 dy = -raw_unit(at(stack, 403, "i")) if len(stack) >= 596 else 0 hpos = transform_point(Point(x=dx, y=dy), pos, rotation) if hole_shape == 2 and slot > hole: path, box = pad_shape(hpos, slot, hole, 1, rotation + hole_angle) elif hole_shape in (0, 1): path, box = pad_shape( hpos, hole, hole, 2 if hole_shape == 1 else 1, rotation ) else: report( "drill_shape", f"Unsupported pad hole shape {hole_shape}", obj.id, ) continue drill = shape( obj, path, box, fill="#08151b", layer_id="81", role="drill", stroke="none", ) drill.layer_ids = obj.layer_ids for i, (p,) in typed("Vias6", 3): obj, c = common("Vias6", i, p) obj.kind = "via" pos = xy(*at(p, 13, "ii")) obj.position = pos diameter, hole = raw_unit(at(p, 21, "i")), raw_unit(at(p, 25, "i")) if diameter <= 0 or hole < 0: raise ParseError("Invalid via diameter") obj.properties.update( { "DIAMETER_MIL": str(diameter * 10), "HOLE_MIL": str(hole * 10), "START_LAYER": str(at(p, 29, "B")), "END_LAYER": str(at(p, 30, "B")), } ) start_layer, end_layer = int(obj.properties["START_LAYER"]), int(obj.properties["END_LAYER"]) if start_layer not in copper_ids or end_layer not in copper_ids: raise ParseError("Via span refers to an inactive/unsupported copper layer") a, b = sorted((copper_ids.index(start_layer), copper_ids.index(end_layer))) span = copper_ids[a:b+1] obj.layer_ids = [str(lid) for lid in span] obj.properties["LAYER_SPAN"] = ",".join(obj.layer_ids) diameters = at(p,75,"32i") if len(p) >= 203 else None for lid in span: if diameters and lid > 32 and len(set(diameters)) > 1: report("via_plane_stack", "Nonuniform via diameter on internal plane layers is not decoded", obj.id) continue size = raw_unit(diameters[lid-1]) if diameters and lid <= 32 else diameter if size <= 0: raise ParseError("Invalid per-layer via diameter") shape(obj, ellipse_path(pos.x,pos.y,size/2),bounds([pos],size/2),fill="layer",layer_id=str(lid),role="via") if hole: drill = shape(obj,ellipse_path(pos.x,pos.y,hole/2),bounds([pos],hole/2),fill="#08151b",layer_id="81",role="drill",stroke="none") drill.layer_ids = obj.layer_ids strings = wide_strings(read("WideStrings6/Data")) for i, parts in typed("Texts6", 5, 2): p = parts[0] obj, c = common("Texts6", i, p, at(p, 226, "I", 0)) obj.kind = "text" pos = xy(*at(p, 13, "ii")) obj.position = pos height = raw_unit(at(p, 21, "i")) if height < 0: raise ParseError("Negative text height") rotation = at(p, 27, "d") mirror = bool(p[35]) font = "Arial" bold = italic = False is_comment = len(p) >= 123 and bool(p[40]) is_designator = len(p) >= 123 and bool(p[41]) if len(p) >= 123: font = ( bytes(p[46:110]).decode("utf-16le", errors="replace").split("\0")[0] or "Arial" ) bold = bool(p[44]) italic = bool(p[45]) legacy = Reader(parts[1], "PCB text").pascal() if parts[1] else "" value = ( strings.get(at(p, 115, "I", 4294967295), legacy) if len(p) >= 123 else legacy ) if c and is_designator: c.designator = value or c.designator if c.parameters.get("NAMEON", "TRUE") == "FALSE": obj.visible = False if c and is_comment: c.value = value c.comment = value if c.parameters.get("COMMENTON", "FALSE") == "FALSE": obj.visible = False obj.properties.update( { "TEXT_HEIGHT": str(height), "TEXT_MIRRORED": str(mirror), "TEXT": value, "ROTATION": str(rotation), "FONT_NAME": font, "DESIGNATOR": str(is_designator), "COMMENT": str(is_comment), } ) if not obj.visible or not value: continue if len(p) > 123 and (p[43] == 2 or p[110] or p[123]): report( "text_style", "PCB barcode/inverted text is approximated by readable text", obj.id, ) # Native font substitution is explicitly diagnosed below. Geometry remains vector text. size = max(height * 1.35, 0.01) w = max(size * 0.5, len(value) * size * 0.75) corners = [ transform_point(Point(x=x, y=y), pos, rotation, mirror) for x, y in [(0, 0), (w, 0), (w, -size), (0, -size)] ] shape( obj, "", bounds(corners), position=pos, text=value, rotation=-rotation, mirrored=mirror, font_size=size, font_family=font, bold=bold, italic=italic, role="text", ) for c in components: if c.id in component_boxes: c.bounds = union(component_boxes[c.id]) if not c.primitive_ids: report( "empty_footprint", f"No supported 2D geometry for {c.designator or c.id}", ) if len(primitives) > 250000: raise ParseError("PCB drawable limit exceeded") # Group layers once; stable within-layer priority keeps pours behind tracks/pads and drills above all. priority = { "board": 0, "pour": 1, "region": 2, "fill": 3, "track": 4, "polygon_outline": 4, "pad": 5, "via": 6, "text": 7, "board_cutout": 8, "drill": 9, } primitives.sort( key=lambda p: ( 1001 if p.role == "board_cutout" else layers[p.layer_id].order, priority.get(p.role, 4), ) ) supported = { "FileHeader", "FileHeaderSix", "UniqueIDPrimitiveInformation", "Board6", "Components6", "Nets6", "Tracks6", "Arcs6", "Pads6", "Vias6", "Fills6", "Texts6", "WideStrings6", "Regions6", "ShapeBasedRegions6", "Polygons6", } omitted = [ s["name"] for s in streams if s["size"] and s["name"].endswith("/Data") and s["name"].split("/")[0] not in supported ] if omitted: for stream in omitted: header = read(stream.removesuffix('/Data')+'/Header') count = at(header,0,'I') if len(header)==4 else 1 report("uninterpreted_stream", f"Not interpreted for this 2D viewer: {stream}", stream) grouped[("uninterpreted_stream",f"Not interpreted for this 2D viewer: {stream}")].count = count report( "embedded_resources", "Embedded STEP/3D models and fonts are not loaded or executed. This is a 2D PCB viewer.", "", "info", ) report( "pcb_text_metrics", "PCB stroke/TrueType text uses browser font substitution; glyph metrics and special text styles can differ.", ) report( "pcb_colors", "Layer colors use a viewer palette; stored layer identity, geometry, net and footprint ownership are retained.", "", "info", ) report( "pcb_scope", "Saved copper fills are shown without repouring or executing design rules. Negative internal planes, mask-rule expansions and 3D bodies are not reconstructed.", ) return Document( schema_version="1.1", document_type="pcb", id=document_id, filename=filename, metadata={ "format": "ole", "header": envelope, "minor_version": board.get("VERSION"), "sha256": hashlib.sha256(data).hexdigest(), "input_bytes": len(data), "streams": streams, "record_counts": counts, "record_count": sum(counts.values()), "object_count": len(objects), "record_variants": {k: dict(v) for k, v in variants.items()}, "component_count": len(components), "primitive_count": len(primitives), "units": "10mil", "coordinates": "PCB integer /100000; X right, Y down", "compatibility": "partial", "header_properties": { "ENVELOPE": envelope, "BOARD_VERSION": board.get("VERSION", ""), }, "net_count": len(nets), "copper_stack": [str(lid) for lid in copper_ids], "palette_source": "viewer", "board_size_mm": { "width": board_bounds.width * 0.254, "height": board_bounds.height * 0.254, }, "uninterpreted_streams": omitted, }, sheet=Sheet( name=filename, background="#08151b", bounds=board_bounds, properties=board, ), bounds=board_bounds, components=components, symbols=[], objects=objects, primitives=primitives, diagnostics=diagnostics, layers=sorted(layers.values(), key=lambda l: l.order, reverse=True), nets=nets, )