Extends TableOperation with text style create/modify, dimension style create/modify (DimStyle fields as a validated JSON object), rename, delete and set-current. Reuses OCS's style in-use scan and reference-following rename (four style_ops methods widened to pub(super)); a renamed text style also updates dimension styles that name it. Real-IPC audit with DWG/DXF persistence and undo, IPC and Python unit tests, docs. Four cadcodec findings are pinned by canaries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 KiB
cadcodec DXF reader/writer gaps found by the OCS Python host audit
Draft issue reports for HakanSeven12/cadcodec (the acadrust crate). Nothing
here has been filed. Every finding was made against cadcodec revision
5b682ed (OCS pins acadrust to it) by saving a document with
DxfWriter or DwgWriter, reloading it, and comparing entity fields.
Each report names an executable canary in OCS (src/app/plugin_host.rs). The
canaries assert today's wrong result, so they fail loudly once cadcodec is
fixed; flip the marked expectation when that happens. To reproduce, from the
OCS repository:
STAGE="$PWD/work/bundled-plugins/opencad.python"
bash plugins/opencad-python/tools/stage-bundled.sh "$STAGE" --debug
cargo build --bin OpenCADStudio
OCS_TEST_PYTHON_PLUGIN="$STAGE/libopencad_python.dylib" \
OCS_PLUGIN_RUNNER_EXE="$PWD/target/debug/OpenCADStudio" \
cargo test --lib app::plugin_host::tests::<test name> -- --test-threads=1
| # | Area | Severity | Effect |
|---|---|---|---|
| 1 | ATTDEF reader |
high | Most attribute-definition properties silently reset to defaults |
| 2 | Boolean group codes (290-299) | high | Booleans read through as_i16() are never applied |
| 3 | UNDERLAY rotation |
high | Rotation is not converted back to radians; repeated saves compound it |
| 4 | LEADER |
medium | Annotation link and three vectors are lost |
| 5 | DRAWINGVIEW / SECTIONLINE |
medium | Typed entity not restored from the ENTITIES section |
| 6 | TABLE merged ranges |
low | merged_ranges is empty after DXF reopen |
1. ATTDEF reader handles eight group codes; the writer emits all of them
Where: src/io/dxf/reader/section_reader.rs, read_attdef (around line
15647). The writer, write_attdef in writer/section_writer.rs (around line
4837), emits 7, 11, 41, 51, 70, 71, 72, 73, 74, 210 and 280 in addition to
the groups the reader consumes.
What happens: the reader has arms for groups 1, 2, 3, 10/20/30, 40, 50, 280 and 101 only; everything else falls through to _ =>. After a DXF round
trip these fields reset to defaults:
- text style name (7)
- alignment point (11/21/31) and both alignments (72, 74)
- width factor (41) and oblique angle (51)
- attribute flags (70): invisible, constant, verify, preset
- text generation flags (71) and field length (73)
- normal (210/220/230) and the position-lock state
Tag, prompt, default value, insertion point, height and rotation survive. Losing the attribute flags is the worst part: an invisible or constant attribute becomes visible and editable after a DXF save.
Evidence: audit_python_attribute_definition_lifecycle_over_real_ipc
compares every writable field through both formats. DWG round-trips the full
state; the DXF digest is pinned to the defaults (see the BLOCKER comment on
expect_edited_dxf).
Suggested fix: add reader arms for the missing groups, using the same
conversions the writer applies (rotation and oblique angle are written in
degrees). TEXT and ATTRIB already read most of these groups and can share
the implementation.
2. Boolean group codes are read with as_i16() and never applied
Where: src/io/dxf/reader/stream_reader.rs defines
DxfCodePair::as_i16() as Some only for CodePairValue::Int. Groups 290-299
are typed Bool, so as_i16() returns None for them and
unwrap_or(0) != 0 yields false. The pattern appears in many arms of
section_reader.rs.
Confirmed impact (round-trip tested):
HELIXgroup 290 (handedness): a clockwise helix reopens counter-clockwise. The reader arm is290 => { if let Some(v) = pair.as_i16() { ... } }(around line 12796).LIGHTgroups 291, 292, 293 (plot_glyph,use_attenuation_limits,cast_shadows; around lines 18154-18182), and 290 for the photometric web file flag:cast_shadowsreopensfalse.
Same pattern, not individually tested (line numbers in section_reader.rs):
13833, 13955-13969 (point-cloud style flags); 18194 (has_web_file);
18431-18438 and 18493-18494 (sweep, loft and revolve option flags); and several
if let Some(v) = pair.as_i16() arms on 290/292 near lines 9815, 10096, 10478
and 12861. Some other arms already use as_bool() correctly (for example lines
18376 and 18550), so the fix is mechanical.
Evidence: audit_python_helix_lifecycle_over_real_ipc (canary
expect_edited_dxf) and audit_python_light_lifecycle_over_real_ipc.
Suggested fix: read these groups with a helper that accepts either
representation, for example
pair.as_bool().or_else(|| pair.as_i16().map(|v| v != 0)), and audit every arm
whose code is in 290-299. Alternatively make as_i16() accept Bool values.
3. UNDERLAY rotation is written in degrees and read back unconverted
Where: write_underlay writes group 50 with underlay.rotation.to_degrees()
(around line 11038 of writer/section_writer.rs). read_underlay stores the
raw value: 50 => { if let Some(v) = pair.as_double() { underlay.rotation = v; } }
(reader/section_reader.rs, around line 19470), with no to_radians().
What happens: a rotation of 0.5 rad is written as 28.6479 degrees and
read back as 28.6479 rad. Because the writer converts again on every save, the
error compounds: a second save-and-reopen yields 1641.4. This silently corrupts
existing drawings each time they pass through cadcodec.
Evidence: audit_python_underlay_lifecycle_over_real_ipc; the canary
expect_edited_dxf / expect_reedited_dxf records 28.6 then 1641.4.
Suggested fix: underlay.rotation = v.to_radians() in the reader. Check
that DGN/DWF/PDF underlays share the code path.
4. LEADER: annotation link and vectors are lost in DXF
a. The writer never emits group 340. write_leader writes 3, 71-76, 10, 210,
211, 212 and 213 but no associated-annotation handle, while the reader has a
340 => arm that fills annotation_handle (around line 16619). A DXF save
therefore unlinks every leader from its text, tolerance or block annotation.
b. The coordinate mapping does not reassemble 211, 212 and 213.
GroupCodeValueType::coordinate_axis in src/io/dxf/group_code_value.rs maps
X to 10..=18 | 110..=112 | 210 | 1010..=1013 (and the Y/Z equivalents), so
add_coordinate ignores the 211/221/231, 212/222/232 and 213/223/233 series.
read_leader uses a PointReader for horizontal_direction, block_offset
and annotation_offset, so all three reopen as their defaults even though the
writer emits them. (normal, group 210, works.)
c. override_color is not persisted in DXF or DWG: a leader coloured by
override_color reopens ByLayer. Observed, cause not traced.
Evidence: the canaries in staged_python_leader_lifecycle_over_real_ipc
(search for BLOCKER). DWG keeps the annotation link, offset and arrow flag.
Suggested fix: write group 340 for a non-null annotation_handle; extend
coordinate_axis/coordinate_group to cover the 211-213 series (or read those
groups explicitly); decide where the override colour belongs (group 62/420 on
the leader).
5. DRAWINGVIEW and SECTIONLINE are dispatched only inside blocks
Where: section_reader.rs has two entity dispatch tables: one for block
contents (around lines 3241-3390) and one for the ENTITIES section
(read_entities, from line 3482). "DRAWINGVIEW" (line 3386) and
"SECTIONLINE" (line 3382) appear only in the first; the ENTITIES table has no
arm for them. write_view_border_dxf writes DRAWINGVIEW correctly.
What happens: a ViewBorder saved to DXF in the drawing's entity list
reopens as a different, opaque entity kind instead of ViewBorder. DWG restores
the typed record. The same applies to SectionSymbol.
Evidence: audit_python_view_border_lifecycle_over_real_ipc (canary
expect_edited_dxf: "wrong kind") and audit_python_section_symbol_lifecycle_over_real_ipc
(canary wrong kind Unknown).
Suggested fix: add the two arms to the ENTITIES dispatch, calling
read_view_border_dxf and read_section_symbol_dxf.
6. TABLE merged_ranges is empty after a DXF reopen
What happens: the writer emits the merged-range list (AcDbFormattedTableData,
group 90 then 91-94 per range; near line 10216 of writer/section_writer.rs),
and reader/section_reader/table_content.rs (around lines 627-690) appears to
parse it, yet Table::merged_ranges is empty after a DXF reopen. The merge
itself survives through the origin cell's merge_width and merge_height
(groups 175 and 176), and DWG restores merged_ranges but not the per-cell
dimensions.
Impact: low, because the merge is preserved in the cell fields; consumers
that read merged_ranges directly see nothing. Cause not traced; it may be that
the entity path does not call the table-content parser.
Evidence: staged_python_table_lifecycle_over_real_ipc (the DXF branch
asserts an empty merged_ranges and marks it as a blocker).
Suggested fix: rebuild merged_ranges from the cell merge dimensions after
reading, or make sure the AcDbFormattedTableData block is parsed for entities.
Observed but not filed
These are format constraints or OCS-side issues, not reader bugs:
- DWG R2010+ does not store a leader's
text_height,text_widthorhookline_enabled, or a 3-D polyline'selevation; the writer stores them only for the older versions that define them. - Entity-level
MultiLeader.text_heightis not stored by either writer (context.text_heightis), so OCS exposes it read-only. - Legacy
Polylinereopens asPolyline3Dfrom both formats and a 2-D polyline reopens asLwPolylinefrom DXF. These look like deliberate normalization; OCS made the legacy kind update-only and records the DXF conversion. - Spline weights in DXF were briefly suspected but are correct: the writer
emits group 41 only when
flags.rationalis set, so the host must set that flag when weights are present (fixed in OCS).
Text and dimension style findings (2026-09-21, not filed)
Found by audit_python_text_and_dim_styles_over_real_ipc; each is pinned by a canary.
STYLEgeneration flags: the DXF writer hard-codes group 71 to 0, so a text style'sflags.backwardandflags.upside_downare lost on every DXF save (DWG keeps them).STYLEoblique angle units: group 50 is written and read as the raw radian value, while DXF defines it in degrees; it round-trips inside cadcodec but AutoCAD would read 15 degrees as 0.26. DWG stores radians correctly.DIMSTYLEtext style name: the DXF reader keeps only the text-style handle (group 340) and never resolvesdimtxstyfrom it, so the name reopens asStandard(the handle is right; DWG resolves both).TextStyle::true_type_fontis never written or read by either codec, so it lives only in memory.