/** * A DXF reader that keeps every group code. * * An ASCII DXF is nothing but pairs of lines: a numeric group code and a value. * Libraries map the codes they know onto named fields and drop the rest, which * is where they come unstuck - a spline drawn from fit points, an attribute * hanging off a block, a text inside a block that needs the block's base point. * Keeping the raw pairs means the geometry code can read whatever it needs. */ export interface Entity { type: string; /** Every group code and value in order; a code can repeat (vertices, knots). */ pairs: [number, string][]; /** VERTEX records of a POLYLINE, or ATTRIB records of an INSERT. */ children: Entity[]; } export interface Layer { name: string; color: number; trueColor: number | null; off: boolean; frozen: boolean; lineType: string; } export interface Block { name: string; baseX: number; baseY: number; entities: Entity[]; } export interface Drawing { version: string; units: number; ltScale: number; layers: Map; lineTypes: Map; blocks: Map; entities: Entity[]; } export function num(e: Entity, code: number, fallback = 0): number { for (const [c, v] of e.pairs) if (c === code) { const n = parseFloat(v); return isNaN(n) ? fallback : n; } return fallback; } export function str(e: Entity, code: number, fallback = ''): string { for (const [c, v] of e.pairs) if (c === code) return v; return fallback; } export function has(e: Entity, code: number): boolean { return e.pairs.some(([c]) => c === code); } export function all(e: Entity, code: number): number[] { return e.pairs.filter(([c]) => c === code).map(([, v]) => parseFloat(v)); } /** * Pre-2007 files are in the drawing's ANSI code page, later ones in UTF-8. A * strict UTF-8 decode that fails is the sign of the older kind. */ export function decodeDxf(bytes: Uint8Array): string { try { return new TextDecoder('utf-8', { fatal: true }).decode(bytes); } catch { const head = new TextDecoder('latin1').decode(bytes.subarray(0, 20000)); const page = /\$DWGCODEPAGE\s*\r?\n\s*3\s*\r?\n\s*ANSI_(\d+)/.exec(head)?.[1]; try { return new TextDecoder(page === '932' ? 'shift_jis' : page === '936' ? 'gbk' : page === '949' ? 'euc-kr' : page === '950' ? 'big5' : `windows-${page ?? 1252}`).decode(bytes); } catch { return new TextDecoder('windows-1252').decode(bytes); } } } export function readDxf(text: string): Drawing { const lines = text.split(/\r\n|\r|\n/); const pairs: [number, string][] = []; for (let i = 0; i + 1 < lines.length; i += 2) { const code = parseInt(lines[i], 10); if (isNaN(code)) { // Blank lines at the very end are common; anything else is not a DXF. if (lines[i].trim() === '') continue; throw new Error('Not a DXF file'); } pairs.push([code, lines[i + 1].trim()]); } if (!pairs.some(([c, v]) => c === 0 && v === 'SECTION')) throw new Error('Not a DXF file'); const drawing: Drawing = { version: '', units: 0, ltScale: 1, layers: new Map(), lineTypes: new Map(), blocks: new Map(), entities: [], }; let i = 0; while (i < pairs.length) { const [code, value] = pairs[i]; if (code === 0 && value === 'SECTION') { const name = pairs[i + 1]?.[1]; const end = findEnd(pairs, i + 2, 'ENDSEC'); const body = pairs.slice(i + 2, end); if (name === 'HEADER') readHeader(body, drawing); else if (name === 'TABLES') readTables(body, drawing); else if (name === 'BLOCKS') readBlocks(body, drawing); else if (name === 'ENTITIES') drawing.entities = group(records(body)); i = end + 1; } else { i++; } } return drawing; } function findEnd(pairs: [number, string][], from: number, marker: string): number { for (let i = from; i < pairs.length; i++) if (pairs[i][0] === 0 && pairs[i][1] === marker) return i; return pairs.length; } /** Splits a run of pairs into records, each starting at a code-0 pair. */ function records(body: [number, string][]): Entity[] { const out: Entity[] = []; let cur: Entity | null = null; let embedded = false; for (const p of body) { if (p[0] === 0) { cur = { type: p[1], pairs: [], children: [] }; embedded = false; out.push(cur); } else if (p[0] === 101) { // An "Embedded Object" (an MTEXT's own copy of its layout, written by // newer AutoCAD) reuses codes 10, 11 and 21 with other meanings. Read // past it and an MTEXT takes its insertion point for its direction. embedded = true; } else if (cur && !embedded) { cur.pairs.push(p); } } return out; } /** Attaches VERTEX records to their POLYLINE and ATTRIB records to their INSERT. */ function group(recs: Entity[]): Entity[] { const out: Entity[] = []; let owner: Entity | null = null; for (const r of recs) { if (owner && (r.type === 'VERTEX' || r.type === 'ATTRIB')) { owner.children.push(r); continue; } if (r.type === 'SEQEND') { owner = null; continue; } owner = r.type === 'POLYLINE' || (r.type === 'INSERT' && num(r, 66) === 1) ? r : null; out.push(r); } return out; } function readHeader(body: [number, string][], d: Drawing): void { for (let i = 0; i < body.length; i++) { const [code, value] = body[i]; if (code !== 9) continue; const next = body[i + 1]?.[1] ?? ''; if (value === '$ACADVER') d.version = next; else if (value === '$INSUNITS') d.units = parseInt(next, 10) || 0; else if (value === '$LTSCALE') d.ltScale = parseFloat(next) || 1; } } function readTables(body: [number, string][], d: Drawing): void { for (const r of records(body)) { if (r.type === 'LAYER') { const color = num(r, 62, 7); const flags = num(r, 70); const name = str(r, 2); d.layers.set(name, { name, // A negative colour number is how DXF says a layer is switched off. color: Math.abs(color) || 7, trueColor: has(r, 420) ? num(r, 420) : null, off: color < 0, frozen: (flags & 1) === 1, lineType: str(r, 6, 'CONTINUOUS'), }); } else if (r.type === 'LTYPE') { const pattern = all(r, 49); if (pattern.length) d.lineTypes.set(str(r, 2).toUpperCase(), pattern); } } } function readBlocks(body: [number, string][], d: Drawing): void { let current: Block | null = null; let entityRecs: Entity[] = []; for (const r of records(body)) { if (r.type === 'BLOCK') { current = { name: str(r, 2), baseX: num(r, 10), baseY: num(r, 20), entities: [] }; entityRecs = []; } else if (r.type === 'ENDBLK') { if (current) { current.entities = group(entityRecs); d.blocks.set(current.name, current); } current = null; } else if (current) { entityRecs.push(r); } } }