-
-
Notifications
You must be signed in to change notification settings - Fork 638
fix(dxf): recode TEXT from $DWGCODEPAGE after ST_Read #1979
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
giswqs
merged 6 commits into
opengeos:main
from
mfkj8866:fix/issue-1973-dxf-text-encoding
Aug 18, 2026
+725
−4
Merged
Changes from 3 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
85955e8
fix(dxf): recode TEXT from $DWGCODEPAGE after ST_Read
mfkj8866 6bf7f6c
Merge branch 'main' into fix/issue-1973-dxf-text-encoding
giswqs e6c80ce
Address Claude review feedback
giswqs 7142faf
Address CodeRabbit review feedback
giswqs e3ed7f3
Address Claude review feedback
giswqs 37a8c9a
Document why ANSI_949 maps to euc-kr
giswqs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| /** | ||
| * Repair DXF string fields after DuckDB-WASM `ST_Read`. | ||
| * | ||
| * AutoCAD TEXT in R2004 and earlier is stored in `$DWGCODEPAGE`; R2007 | ||
| * (`AC1021`) and later is UTF-8. Desktop GDAL recodes to UTF-8; WASM GDAL has | ||
| * no iconv, so each file byte becomes a Latin-1 character. Recode those | ||
| * strings — recover the original bytes, then decode with the drawing | ||
| * codepage — at the GeoJSON boundary. Rewriting the file as UTF-8 does not | ||
| * help: WASM GDAL still copies bytes as Latin-1. Binary DXF is left unchanged. | ||
| * | ||
| * Kept free of the DuckDB-WASM import so `node --test` can cover it without | ||
| * pulling the engine into the coverage denominator. | ||
| */ | ||
|
|
||
| import type { Feature, FeatureCollection, GeoJsonProperties } from "geojson"; | ||
|
|
||
| const HEADER_PROBE_BYTES = 64 * 1024; | ||
| const BINARY_DXF_MAGIC = "AutoCAD Binary DXF"; | ||
| /** `$ACADVER` numeric suffix at which DXF switched from codepage to UTF-8. */ | ||
| const UTF8_DXF_VERSION = 1021; | ||
|
|
||
| /** | ||
| * AutoCAD `$DWGCODEPAGE` → WHATWG `TextDecoder` label. | ||
| * | ||
| * `ISO8859-1` / `ISO-8859-1` / `ASCII` / `US-ASCII` are deliberately absent: | ||
| * WASM GDAL already turns each file byte into the matching Latin-1 code point, | ||
| * which *is* the correct Unicode for those codepages, so recoding has nothing | ||
| * to repair. Mapping them through `TextDecoder` would instead corrupt bytes | ||
| * 0x80–0x9F, because the WHATWG Encoding Standard aliases every `latin1` / | ||
| * `iso-8859-1` label to windows-1252, where that range holds printable | ||
| * characters rather than ISO-8859-1's C1 controls. `ANSI_1252` is a different | ||
| * matter and stays mapped: there the file really is windows-1252, so those | ||
| * bytes do need recoding. | ||
| */ | ||
| const CODEPAGE_LABELS: Record<string, string> = { | ||
| "UTF-8": "utf-8", | ||
| UTF8: "utf-8", | ||
| ANSI_936: "gb18030", | ||
| GBK: "gb18030", | ||
| GB2312: "gb18030", | ||
| GB18030: "gb18030", | ||
| ANSI_950: "big5", | ||
| BIG5: "big5", | ||
| ANSI_932: "shift_jis", | ||
| SHIFT_JIS: "shift_jis", | ||
| ANSI_949: "euc-kr", | ||
| ANSI_1252: "windows-1252", | ||
| ANSI_1250: "windows-1250", | ||
| ANSI_1251: "windows-1251", | ||
| ANSI_1253: "windows-1253", | ||
| ANSI_1254: "windows-1254", | ||
| ANSI_1255: "windows-1255", | ||
| ANSI_1256: "windows-1256", | ||
|
giswqs marked this conversation as resolved.
|
||
| ANSI_1257: "windows-1257", | ||
| ANSI_1258: "windows-1258", | ||
| }; | ||
|
|
||
| /** | ||
| * Decode a Latin-1 prefix so ASCII DXF headers can be scanned. | ||
| * | ||
| * The `latin1` label is windows-1252 in disguise (see {@link CODEPAGE_LABELS}), | ||
| * which is harmless here: the header variables and values this scans are ASCII. | ||
| * | ||
| * @param bytes The DXF file bytes. | ||
| * @returns The first {@link HEADER_PROBE_BYTES} decoded as Latin-1. | ||
| */ | ||
| function headerLatin1(bytes: Uint8Array): string { | ||
| return new TextDecoder("latin1").decode( | ||
| bytes.subarray(0, Math.min(bytes.length, HEADER_PROBE_BYTES)), | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * True when the buffer is a binary DXF (not the ASCII/ANSI text form). | ||
| * | ||
| * @param bytes The file bytes. | ||
| * @returns True when the AutoCAD binary-DXF magic is present. | ||
| */ | ||
| function isBinaryDxf(bytes: Uint8Array): boolean { | ||
| if (bytes.length < BINARY_DXF_MAGIC.length) return false; | ||
| for (let i = 0; i < BINARY_DXF_MAGIC.length; i += 1) { | ||
| if (bytes[i] !== BINARY_DXF_MAGIC.charCodeAt(i)) return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Read one HEADER variable from an ASCII DXF prefix. | ||
| * | ||
| * @param header Latin-1 text of the file prefix. | ||
| * @param name The variable without `$`, such as `ACADVER`. | ||
| * @param groupCode The DXF group code of the value line (`1` or `3`). | ||
| * @returns The trimmed value, or null when the variable is absent. | ||
| */ | ||
| function readDxfTaggedValue(header: string, name: string, groupCode: number): string | null { | ||
| const pattern = new RegExp( | ||
| `\\$${name}[ \\t]*\\r?\\n[ \\t]*0*${groupCode}[ \\t]*\\r?\\n([^\\r\\n]+)`, | ||
| "i", | ||
| ); | ||
| const value = pattern.exec(header)?.[1]?.trim(); | ||
| return value || null; | ||
|
giswqs marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| /** | ||
| * True when `$ACADVER` is R2007 or later (UTF-8 DXF). | ||
| * | ||
| * @param acadver A value such as `AC1021`. | ||
| * @returns True when the numeric suffix is >= {@link UTF8_DXF_VERSION}. | ||
| */ | ||
| function isUtf8DxfVersion(acadver: string): boolean { | ||
| const match = /^AC(\d+)$/i.exec(acadver.trim()); | ||
| return match !== null && Number(match[1]) >= UTF8_DXF_VERSION; | ||
| } | ||
|
|
||
| /** | ||
| * Map an AutoCAD codepage name to a `TextDecoder` label, if supported. | ||
|
giswqs marked this conversation as resolved.
|
||
| * | ||
| * @param codepage An uppercased `$DWGCODEPAGE` value. | ||
| * @returns A WHATWG encoding label, or null when unknown/unsupported. | ||
| */ | ||
| function decoderLabelForCodepage(codepage: string): string | null { | ||
| const mapped = CODEPAGE_LABELS[codepage]; | ||
| if (!mapped) return null; | ||
| try { | ||
| new TextDecoder(mapped); | ||
| return mapped; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Normalize a `$DWGCODEPAGE` value to a CODEPAGE_LABELS key. | ||
| * | ||
| * @param raw A token such as `ansi_936` or `utf8`. | ||
| * @returns The uppercased key, with `UTF8` folded to `UTF-8`. | ||
| */ | ||
| function normalizeCodepageToken(raw: string): string { | ||
| const upper = raw.trim().toUpperCase(); | ||
| return upper === "UTF8" ? "UTF-8" : upper; | ||
| } | ||
|
|
||
| /** | ||
| * Read the drawing codepage from an ASCII DXF header. | ||
| * | ||
| * R2007+ (`$ACADVER` >= AC1021) is UTF-8 even when `$DWGCODEPAGE` still names | ||
| * a legacy ANSI_* page. Earlier versions use `$DWGCODEPAGE`. Binary DXF and | ||
| * files with neither variable are left unlabelled. | ||
| * | ||
| * @param bytes The file bytes (must be read before DuckDB detaches the buffer). | ||
| * @returns An AutoCAD codepage name, or null when recoding should not run. | ||
| */ | ||
| export function readDxfCodepage(bytes: Uint8Array): string | null { | ||
| if (bytes.length === 0 || isBinaryDxf(bytes)) return null; | ||
| const header = headerLatin1(bytes); | ||
| const acadver = readDxfTaggedValue(header, "ACADVER", 1); | ||
| if (acadver && isUtf8DxfVersion(acadver)) return "UTF-8"; | ||
| const codepage = readDxfTaggedValue(header, "DWGCODEPAGE", 3); | ||
| return codepage ? normalizeCodepageToken(codepage) : null; | ||
| } | ||
|
|
||
|
giswqs marked this conversation as resolved.
|
||
| /** | ||
| * Rebuild the DXF bytes WASM GDAL copied into a JS string as Latin-1. | ||
| * | ||
| * @param value A DuckDB string field. | ||
| * @returns The original bytes, or null when `value` is already real Unicode. | ||
| */ | ||
| function duckDbStringBytes(value: string): Uint8Array | null { | ||
| const bytes = new Uint8Array(value.length); | ||
| for (let i = 0; i < value.length; i += 1) { | ||
| const code = value.charCodeAt(i); | ||
| if (code > 255) return null; | ||
| bytes[i] = code; | ||
| } | ||
| return bytes; | ||
| } | ||
|
|
||
| /** | ||
| * Recode one DXF string with an already-constructed decoder. | ||
| * | ||
| * @param value The raw DuckDB/OGR string (or already-correct Unicode). | ||
| * @param decoder A `TextDecoder` for the drawing codepage. | ||
| * @returns Unicode text, or `value` when recoding does not apply. | ||
| */ | ||
| function recodeWithDecoder(value: string, decoder: TextDecoder): string { | ||
| if (!value) return value; | ||
| const bytes = duckDbStringBytes(value); | ||
| if (!bytes) return value; | ||
| try { | ||
| return decoder.decode(bytes); | ||
| } catch { | ||
| return value; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Recode one DXF attribute that WASM GDAL exposed as Latin-1 mojibake. | ||
| * | ||
| * @param value The raw DuckDB/OGR string (or already-correct Unicode). | ||
| * @param codepage From {@link readDxfCodepage}, or null. | ||
| * @returns Unicode text, or `value` when recoding does not apply. | ||
| */ | ||
| export function recodeCadString(value: string, codepage: string | null): string { | ||
| if (!value || !codepage) return value; | ||
| const label = decoderLabelForCodepage(codepage); | ||
| if (!label) return value; | ||
| return recodeWithDecoder(value, new TextDecoder(label, { fatal: true })); | ||
| } | ||
|
|
||
|
giswqs marked this conversation as resolved.
|
||
| /** | ||
| * Recode string properties on one feature. | ||
| * | ||
| * @param properties The feature properties object, or null. | ||
| * @param decoder A `TextDecoder` for the drawing codepage. | ||
| * @returns Properties with DXF strings recoded; non-strings left unchanged. | ||
| */ | ||
| function recodeCadProperties( | ||
| properties: GeoJsonProperties, | ||
| decoder: TextDecoder, | ||
| ): GeoJsonProperties { | ||
| if (!properties) return properties; | ||
| const next: Record<string, unknown> = {}; | ||
| for (const [key, value] of Object.entries(properties)) { | ||
| next[key] = typeof value === "string" ? recodeWithDecoder(value, decoder) : value; | ||
| } | ||
| return next; | ||
| } | ||
|
|
||
| /** | ||
| * Recode string properties on every feature in a DXF-derived collection. | ||
| * | ||
| * @param collection The FeatureCollection `ST_Read` materialized. | ||
| * @param codepage From {@link readDxfCodepage}, or null. | ||
| * @returns A new collection when recoding ran; `collection` when it did not. | ||
| */ | ||
| export function recodeCadFeatureCollection( | ||
| collection: FeatureCollection, | ||
| codepage: string | null, | ||
| ): FeatureCollection { | ||
| const label = codepage ? decoderLabelForCodepage(codepage) : null; | ||
| if (!label) return collection; | ||
| const decoder = new TextDecoder(label, { fatal: true }); | ||
| return { | ||
| ...collection, | ||
| features: collection.features.map((feature: Feature) => ({ | ||
| ...feature, | ||
| properties: recodeCadProperties(feature.properties, decoder), | ||
| })), | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.