@@ -60,14 +60,18 @@ const CODEPAGE_LABELS: Record<string, string> = {
6060 *
6161 * The `latin1` label is windows-1252 in disguise (see {@link CODEPAGE_LABELS}),
6262 * which is harmless here: the header variables and values this scans are ASCII.
63+ * A leading UTF-8 BOM (`EF BB BF`, which that decode turns into ``) is
64+ * dropped so it cannot glue itself to the file's first group code — a DXF
65+ * re-saved as UTF-8 by a text editor is exactly the case this module exists for.
6366 *
6467 * @param bytes The DXF file bytes.
6568 * @returns The first {@link HEADER_PROBE_BYTES} decoded as Latin-1.
6669 */
6770function headerLatin1 ( bytes : Uint8Array ) : string {
68- return new TextDecoder ( "latin1" ) . decode (
71+ const text = new TextDecoder ( "latin1" ) . decode (
6972 bytes . subarray ( 0 , Math . min ( bytes . length , HEADER_PROBE_BYTES ) ) ,
7073 ) ;
74+ return text . replace ( / ^ (?: \uFEFF | ï » ¿ ) / , "" ) ;
7175}
7276
7377/**
@@ -84,21 +88,87 @@ function isBinaryDxf(bytes: Uint8Array): boolean {
8488 return true ;
8589}
8690
91+ /** One HEADER variable: its value records, keyed by DXF group code. */
92+ type DxfHeaderVariable = Map < number , string > ;
93+
8794/**
88- * Read one HEADER variable from an ASCII DXF prefix.
95+ * Parse the HEADER variables out of an ASCII DXF prefix.
96+ *
97+ * DXF is a flat stream of (group code, value) line pairs. A HEADER variable is
98+ * introduced by a group-9 record naming it, followed by the value records that
99+ * belong to it. Walking those pairs — rather than pattern-matching the text —
100+ * is what keeps an entity from impersonating a variable: an `MTEXT` whose own
101+ * content reads `$DWGCODEPAGE` sits under group 1, not group 9, and the scan
102+ * stops at the HEADER section's `ENDSEC` before reaching ENTITIES anyway.
103+ *
104+ * A pair whose group code is not a number is skipped rather than treated as a
105+ * value; pair alignment is fixed by the format, so one unreadable code does not
106+ * desync the ones after it.
89107 *
90108 * @param header Latin-1 text of the file prefix.
109+ * @returns Each variable name (uppercased, without `$`) mapped to its records.
110+ */
111+ function readDxfHeaderVariables ( header : string ) : Map < string , DxfHeaderVariable > {
112+ const lines = header . split ( / \r ? \n / ) ;
113+ const variables = new Map < string , DxfHeaderVariable > ( ) ;
114+ let awaitingSectionName = false ;
115+ let inHeader = false ;
116+ let current : DxfHeaderVariable | null = null ;
117+
118+ for ( let i = 0 ; i + 1 < lines . length ; i += 2 ) {
119+ const code = Number . parseInt ( lines [ i ] ! . trim ( ) , 10 ) ;
120+ const value = lines [ i + 1 ] ! . trim ( ) ;
121+ if ( ! Number . isInteger ( code ) ) continue ;
122+
123+ if ( code === 0 && value . toUpperCase ( ) === "SECTION" ) {
124+ awaitingSectionName = true ;
125+ inHeader = false ;
126+ current = null ;
127+ continue ;
128+ }
129+ if ( awaitingSectionName ) {
130+ // The `2` record right after `0/SECTION` names the section.
131+ if ( code === 2 ) {
132+ inHeader = value . toUpperCase ( ) === "HEADER" ;
133+ awaitingSectionName = false ;
134+ }
135+ continue ;
136+ }
137+ if ( ! inHeader ) continue ;
138+ // HEADER is the first section, so its end is the end of anything readable.
139+ if ( code === 0 && value . toUpperCase ( ) === "ENDSEC" ) break ;
140+
141+ if ( code === 9 ) {
142+ const name = value . startsWith ( "$" ) ? value . slice ( 1 ) . toUpperCase ( ) : "" ;
143+ if ( ! name ) {
144+ current = null ;
145+ continue ;
146+ }
147+ current = variables . get ( name ) ?? new Map < number , string > ( ) ;
148+ variables . set ( name , current ) ;
149+ continue ;
150+ }
151+ // First record of a given code wins, so a repeated variable cannot shadow
152+ // the value AutoCAD wrote first.
153+ if ( current && ! current . has ( code ) ) current . set ( code , value ) ;
154+ }
155+ return variables ;
156+ }
157+
158+ /**
159+ * Read one HEADER variable's value record.
160+ *
161+ * @param variables From {@link readDxfHeaderVariables}.
91162 * @param name The variable without `$`, such as `ACADVER`.
92- * @param groupCode The DXF group code of the value line (`1` or `3`).
93- * @returns The trimmed value, or null when the variable is absent.
163+ * @param groupCode The DXF group code of the value record (`1` or `3`).
164+ * @returns The trimmed value, or null when the variable or record is absent.
94165 */
95- function readDxfTaggedValue ( header : string , name : string , groupCode : number ) : string | null {
96- const pattern = new RegExp (
97- `\\$${ name } [ \\t]*\\r?\\n[ \\t]*0*${ groupCode } [ \\t]*\\r?\\n([^\\r\\n]+)` ,
98- "i" ,
99- ) ;
100- const value = pattern . exec ( header ) ?. [ 1 ] ?. trim ( ) ;
101- return value || null ;
166+ function readDxfHeaderValue (
167+ variables : Map < string , DxfHeaderVariable > ,
168+ name : string ,
169+ groupCode : number ,
170+ ) : string | null {
171+ return variables . get ( name ) ?. get ( groupCode ) || null ;
102172}
103173
104174/**
@@ -152,10 +222,10 @@ function normalizeCodepageToken(raw: string): string {
152222 */
153223export function readDxfCodepage ( bytes : Uint8Array ) : string | null {
154224 if ( bytes . length === 0 || isBinaryDxf ( bytes ) ) return null ;
155- const header = headerLatin1 ( bytes ) ;
156- const acadver = readDxfTaggedValue ( header , "ACADVER" , 1 ) ;
225+ const variables = readDxfHeaderVariables ( headerLatin1 ( bytes ) ) ;
226+ const acadver = readDxfHeaderValue ( variables , "ACADVER" , 1 ) ;
157227 if ( acadver && isUtf8DxfVersion ( acadver ) ) return "UTF-8" ;
158- const codepage = readDxfTaggedValue ( header , "DWGCODEPAGE" , 3 ) ;
228+ const codepage = readDxfHeaderValue ( variables , "DWGCODEPAGE" , 3 ) ;
159229 return codepage ? normalizeCodepageToken ( codepage ) : null ;
160230}
161231
0 commit comments