Escape DGN text in MTEXT and read 3D text by its byte count

Backslashes and braces in a DGN text stay characters in the MTEXT it
becomes instead of turning into formatting codes.

A 3D V8 text is cut at its stored byte count before it is decoded, so a
multi-byte character no longer shifts where the text ends.

Doc comments that had slipped onto the wrong functions are back on
their own, comments describe the behaviour instead of pointing at
another program, and a few lines added in the previous commits are laid
out the way the rest of the code is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-16 23:25:27 +03:00
1 parent a5f965ead4
commit 56861aa0bc
4 files changed
+54 -27

No files matched your search

+2 -1
View File
@@ -458,7 +458,8 @@ impl OpenCADStudio {
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
Naming::Prefix(dgn_import::symbol_name(&format!("{stem} - {}$0$", request.model)))
let prefix = format!("{stem} - {}$0$", request.model);
Naming::Prefix(dgn_import::symbol_name(&prefix))
} else {
Naming::Keep
};
+35 -15
View File
@@ -12,7 +12,7 @@
// B-spline curves, complex chains and shapes (polylines with arcs), text,
// text nodes and shared cells (blocks) are converted.
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use codec::entities::{
Arc, AttachmentPoint, Circle, DrawingDirection, Ellipse, Insert, Line, LineSpacingStyle,
@@ -114,7 +114,7 @@ pub fn unit_display(short: &str) -> String {
pub fn symbol_name(name: &str) -> String {
name.chars()
.map(|c| {
if c.is_control() || "<>/\\\":;?*|,=`".contains(c) {
if c.is_control() || "<>/\\\":;?*|=`".contains(c) {
'_'
} else {
c
@@ -221,9 +221,8 @@ fn flat(points: &[P3]) -> bool {
/// (with the bulges), else a 3D polyline (no bulges).
fn polyline(points: &[P3], bulges: &[f64]) -> EntityType {
if !flat(points) {
return EntityType::Polyline3D(Polyline3D::from_points(
points.iter().map(|p| v3(*p)).collect(),
));
let points: Vec<Vector3> = points.iter().map(|p| v3(*p)).collect();
return EntityType::Polyline3D(Polyline3D::from_points(points));
}
let mut pl = LwPolyline::new();
pl.plinegen = true;
@@ -253,7 +252,8 @@ fn solid_hatch(points: &[P3], bulges: &[f64]) -> Option<EntityType> {
}
let mut edge = PolylineEdge::new(Vec::new(), true);
for (k, p) in points[..n].iter().enumerate() {
edge.add_vertex(Vector2::new(p[0], p[1]), bulges.get(k).copied().unwrap_or(0.0));
let bulge = bulges.get(k).copied().unwrap_or(0.0);
edge.add_vertex(Vector2::new(p[0], p[1]), bulge);
}
let mut hatch = Hatch::solid();
let mut boundary = BoundaryPath::external();
@@ -281,7 +281,9 @@ fn chain_points(parts: &[DgnGeom], use_bulges: bool) -> (Vec<P3>, Vec<f64>) {
let mut points: Vec<P3> = Vec::new();
let mut bulges: Vec<f64> = Vec::new();
let mut add = |p: P3, bulge_before: Option<f64>| {
let same = points.last().is_some_and(|q| (0..3).all(|c| (q[c] - p[c]).abs() < 1e-9));
let same = points
.last()
.is_some_and(|q| (0..3).all(|c| (q[c] - p[c]).abs() < 1e-9));
if let (Some(b), Some(last)) = (bulge_before, bulges.last_mut()) {
*last = b;
}
@@ -379,6 +381,14 @@ fn attachment(just: u8) -> AttachmentPoint {
}
}
/// Text as MTEXT contents: backslashes and braces stay characters instead
/// of becoming formatting codes.
fn mtext_escape(text: &str) -> String {
text.replace('\\', "\\\\")
.replace('{', "\\{")
.replace('}', "\\}")
}
/// A number with six significant digits, trailing zeros dropped.
fn sig6(x: f64) -> String {
let digits = if x == 0.0 {
@@ -510,7 +520,7 @@ fn mtext_of_text(geom: &DgnGeom, color: u16, style: &str) -> Option<EntityType>
} else {
String::new()
};
m.value = format!("{{{width}\\C{color};{text}}}");
m.value = format!("{{{width}\\C{color};{}}}", mtext_escape(text));
if *just == 2 {
let up = -0.33 * height;
m.insertion_point = Vector3::new(
@@ -549,7 +559,11 @@ fn mtext_of_node(
color: Option<u16>,
) -> Option<EntityType> {
let (_, first, height, rotation, _) = lines.iter().find_map(text_line)?;
let texts: Vec<&str> = lines.iter().filter_map(text_line).map(|l| l.0).collect();
let texts: Vec<String> = lines
.iter()
.filter_map(text_line)
.map(|l| mtext_escape(l.0))
.collect();
let mut m = MText::new();
let joined = texts.join("\\P");
m.value = match color {
@@ -783,8 +797,11 @@ impl<'a> Converter<'a> {
DgnGeom::Line(a, b) => vec![EntityType::Line(Line::from_points(v3(*a), v3(*b)))],
// A filled shape is its outline and a solid hatch inside it.
DgnGeom::Points(pts) if pts.len() >= 2 => {
let fill = element.filled.then(|| solid_hatch(pts, &[])).flatten();
std::iter::once(polyline(pts, &[])).chain(fill).collect()
let mut out = vec![polyline(pts, &[])];
if element.filled {
out.extend(solid_hatch(pts, &[]));
}
out
}
DgnGeom::Points(_) => Vec::new(),
DgnGeom::Arc {
@@ -816,8 +833,11 @@ impl<'a> Converter<'a> {
if points.len() < 2 {
return Vec::new();
}
let fill = element.filled.then(|| solid_hatch(&points, &bulges)).flatten();
std::iter::once(polyline(&points, &bulges)).chain(fill).collect()
let mut out = vec![polyline(&points, &bulges)];
if element.filled {
out.extend(solid_hatch(&points, &bulges));
}
out
}
// V7 text is TEXT, V8 text an MTEXT.
DgnGeom::Text { font: Some(_), .. } => {
@@ -980,8 +1000,8 @@ pub fn convert(import: &DgnImport, sub_units: bool, explode_text_nodes: bool) ->
c.out.entities = entities;
// A cell left without a block (the point display cell, an empty or
// missing definition) places nothing: its instances are dropped.
let defined: std::collections::HashSet<String> =
c.out.blocks.iter().map(|b| b.0.to_ascii_uppercase()).collect();
let blocks = &c.out.blocks;
let defined: HashSet<String> = blocks.iter().map(|b| b.0.to_ascii_uppercase()).collect();
let placed = |e: &EntityType| match e {
EntityType::Insert(i) => defined.contains(&i.block_name.to_ascii_uppercase()),
_ => true,
+2 -1
View File
@@ -293,7 +293,8 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool, depth: usize) -> Raw {
// covers; they are already placed, so the cell keeps them as
// they are. Nesting past `MAX_CELL_DEPTH` is left out.
2 => {
let mut left = (38 + uint16(e, 36).unwrap_or(0) as usize * 2).saturating_sub(e.len());
let total = 38 + uint16(e, 36).unwrap_or(0) as usize * 2;
let mut left = total.saturating_sub(e.len());
let start = index;
while left > 0 && index < run.len() && run[index][0] & 0x80 != 0 {
left = left.saturating_sub(run[index].len());
+15 -10
View File
@@ -284,7 +284,6 @@ fn f64_at(e: &[u8], at: usize) -> Option<f64> {
Some(f64::from_le_bytes(e.get(at..at + 8)?.try_into().ok()?))
}
/// A string stored after the "ff fe 01 00" marker, when the element has one.
/// The text style a text element uses: the id in its text style linkage
/// (the four bytes after `04 20 e6 04 00 00`).
fn text_style_id(e: &[u8]) -> Option<u32> {
@@ -313,6 +312,7 @@ fn text_style_table(nm: &[&[u8]]) -> Vec<(u32, String)> {
out
}
/// A string stored after the "ff fe 01 00" marker, when the element has one.
fn marked_string(e: &[u8], from: usize) -> Option<String> {
let i = e
.get(from..)?
@@ -446,8 +446,7 @@ impl Styles {
/// A B-spline curve from its order and poles, with uniform knots. A closed
/// curve (its last pole repeating the first) is periodic: its distinct
/// poles wrap around with knots (i + 1) / (count + 1), and the curve is
/// clamped at both ends of its domain by knot insertion, as the reference
/// converts it.
/// clamped at both ends of its domain by knot insertion.
fn bspline_geom(order: usize, poles: Vec<[f64; 3]>, closed: bool) -> DgnGeom {
let degree = order - 1;
let distinct = poles.len() - usize::from(closed && poles.first() == poles.last());
@@ -914,8 +913,14 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
// The byte count at 110 (with the 4-byte marker)
// ends the characters.
{
let count = u16::from_le_bytes(e.get(110..112)?.try_into().ok()?) as usize;
marked_string(e, 200)?.chars().take(count.saturating_sub(4)).collect()
let count =
u16::from_le_bytes(e.get(110..112)?.try_into().ok()?) as usize;
let mark = e
.get(200..)?
.windows(4)
.position(|w| w == [0xff, 0xfe, 0x01, 0x00])?
+ 200;
marked_string(&e[..(mark + count.max(4)).min(e.len())], mark)?
},
))
})()
@@ -1405,9 +1410,6 @@ fn unit_name(metres: f64) -> String {
.unwrap_or_default()
}
/// The named model element by element (DGNIMPORT): in master units with
/// the global origin at 0,0, shared cell definitions in master units, and
/// every level by id with its own name and colour.
/// A design model (not a sheet): the model type byte of its header (4105)
/// is 2 for a sheet.
fn is_design(header: &[u8]) -> bool {
@@ -1503,6 +1505,9 @@ fn references(control: &[u8]) -> Vec<DgnReference> {
.collect()
}
/// The named model element by element (DGNIMPORT): in master units with
/// the global origin at 0,0, shared cell definitions in master units, and
/// every level by id with its own name and colour.
pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
let cfb = Cfb::open(bytes)?;
let all = models(&cfb);
@@ -1546,8 +1551,8 @@ pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
while j < nm.len() && is_component(nm[j]) {
j += 1;
}
// The cell that draws points is the writer's own; the reference
// leaves it out of the drawing.
// The cell that draws points belongs to the writing program,
// not to the drawing: it is left out.
if !cell.eq_ignore_ascii_case("POINT_DISPLAY") {
let def = read_run(&nm[i + 1..j], &styles, &IDENTITY).elements;
cells.push((cell, scaled_elements(&def, s, [0.0, 0.0])));