Skip to main content

altium_format/records/pcb/
text.rs

1//! PCB Text record.
2
3use std::io::Read;
4
5use altium_format_derive::AltiumRecord;
6
7use super::primitive::{
8    PcbPrimitiveCommon, PcbRectangularBase, PcbTextJustification, PcbTextKind, PcbTextStrokeFont,
9};
10use crate::error::{AltiumError, Result};
11use crate::traits::{FromBinary, ToBinary};
12use crate::types::{Coord, CoordPoint, CoordRect};
13
14/// Font name stored as 32-byte fixed-length UTF-16 LE string.
15///
16/// Format: 32 bytes (16 UTF-16 LE code units)
17/// - Typically null-terminated for strings < 16 characters
18/// - May use all 16 code units (no null) for exactly 16-character strings
19/// - Remaining bytes after null are zero-padded
20#[derive(Debug, Clone, Default)]
21struct FontName(String);
22
23impl FromBinary for FontName {
24    fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
25        let mut buf = [0u8; 32];
26        reader.read_exact(&mut buf)?;
27
28        // Read UTF-16 LE code units until null terminator or end of buffer
29        let mut code_units = Vec::new();
30        for chunk in buf.chunks_exact(2) {
31            let code = u16::from_le_bytes([chunk[0], chunk[1]]);
32            if code == 0 {
33                break;
34            }
35            code_units.push(code);
36        }
37
38        // Convert UTF-16 to Rust string
39        let value = String::from_utf16(&code_units)
40            .map_err(|e| AltiumError::Encoding(format!("Invalid UTF-16 font name: {}", e)))?;
41        Ok(FontName(value))
42    }
43}
44
45impl ToBinary for FontName {
46    fn write_to<W: std::io::Write>(&self, writer: &mut W) -> Result<()> {
47        use crate::io::writer::write_font_name;
48
49        write_font_name(writer, &self.0)
50    }
51
52    fn binary_size(&self) -> usize {
53        32
54    }
55}
56
57#[derive(Debug, Clone, Default, AltiumRecord)]
58#[altium(format = "binary")]
59struct PcbTextBaseBinary {
60    #[altium(flatten)]
61    common: PcbPrimitiveCommon,
62    #[altium(coord_point)]
63    corner1: CoordPoint,
64    #[altium(coord)]
65    height: Coord,
66    stroke_font: PcbTextStrokeFont,
67    rotation: f64,
68    mirrored: bool,
69    #[altium(coord)]
70    stroke_width: Coord,
71}
72
73#[derive(Debug, Clone, Default, AltiumRecord)]
74#[altium(format = "binary")]
75struct PcbTextExtendedBinary {
76    _unknown1: u16,
77    _unknown2: u8,
78    text_kind: PcbTextKind,
79    font_bold: bool,
80    font_italic: bool,
81    font_name: FontName,
82    #[altium(coord)]
83    barcode_lr_margin: Coord,
84    #[altium(coord)]
85    barcode_tb_margin: Coord,
86    _unknown3: i32,
87    _unknown4: i32,
88    _unknown5: u8,
89    _unknown6: u8,
90    _unknown7: i32,
91    _unknown8: u16,
92    _unknown9: i32,
93    _unknown10: i32,
94    font_inverted: bool,
95    #[altium(coord)]
96    font_inverted_border: Coord,
97    wide_strings_index: i32,
98    _unknown11: i32,
99    font_inverted_rect: bool,
100    #[altium(coord)]
101    font_inverted_rect_width: Coord,
102    #[altium(coord)]
103    font_inverted_rect_height: Coord,
104    font_inverted_rect_justification: PcbTextJustification,
105    #[altium(coord)]
106    font_inverted_rect_text_offset: Coord,
107}
108
109/// PCB Text primitive.
110#[derive(Debug, Clone, Default)]
111pub struct PcbText {
112    /// Base rectangular fields.
113    pub base: PcbRectangularBase,
114    /// Whether the text is mirrored.
115    pub mirrored: bool,
116    /// Text kind (Stroke, TrueType, BarCode).
117    pub text_kind: PcbTextKind,
118    /// Stroke font type.
119    pub stroke_font: PcbTextStrokeFont,
120    /// Stroke width.
121    pub stroke_width: Coord,
122    /// Font is bold.
123    pub font_bold: bool,
124    /// Font is italic.
125    pub font_italic: bool,
126    /// TrueType font name.
127    pub font_name: String,
128    /// Barcode left/right margin.
129    pub barcode_lr_margin: Coord,
130    /// Barcode top/bottom margin.
131    pub barcode_tb_margin: Coord,
132    /// Font is inverted.
133    pub font_inverted: bool,
134    /// Inverted border width.
135    pub font_inverted_border: Coord,
136    /// Inverted rectangle mode.
137    pub font_inverted_rect: bool,
138    /// Inverted rectangle width.
139    pub font_inverted_rect_width: Coord,
140    /// Inverted rectangle height.
141    pub font_inverted_rect_height: Coord,
142    /// Inverted rectangle justification.
143    pub font_inverted_rect_justification: PcbTextJustification,
144    /// Inverted rectangle text offset.
145    pub font_inverted_rect_text_offset: Coord,
146    /// The actual text content.
147    pub text: String,
148    /// Index into wide strings array (for Unicode text).
149    pub wide_strings_index: i32,
150}
151
152impl FromBinary for PcbText {
153    fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
154        let base = <PcbTextBaseBinary as FromBinary>::read_from(reader)?;
155
156        let mut remaining = Vec::new();
157        reader.read_to_end(&mut remaining)?;
158
159        let mut extended = PcbTextExtendedBinary::default();
160        if remaining.len() >= extended.binary_size() {
161            let mut cursor = std::io::Cursor::new(&remaining);
162            extended = <PcbTextExtendedBinary as FromBinary>::read_from(&mut cursor)?;
163        }
164
165        let corner2 = CoordPoint::new(base.corner1.x, base.corner1.y + base.height);
166
167        Ok(PcbText {
168            base: PcbRectangularBase {
169                common: base.common,
170                corner1: base.corner1,
171                corner2,
172                rotation: base.rotation,
173            },
174            mirrored: base.mirrored,
175            text_kind: extended.text_kind,
176            stroke_font: base.stroke_font,
177            stroke_width: base.stroke_width,
178            font_bold: extended.font_bold,
179            font_italic: extended.font_italic,
180            font_name: extended.font_name.0,
181            barcode_lr_margin: extended.barcode_lr_margin,
182            barcode_tb_margin: extended.barcode_tb_margin,
183            font_inverted: extended.font_inverted,
184            font_inverted_border: extended.font_inverted_border,
185            font_inverted_rect: extended.font_inverted_rect,
186            font_inverted_rect_width: extended.font_inverted_rect_width,
187            font_inverted_rect_height: extended.font_inverted_rect_height,
188            font_inverted_rect_justification: extended.font_inverted_rect_justification,
189            font_inverted_rect_text_offset: extended.font_inverted_rect_text_offset,
190            text: String::new(), // Set later from string block
191            wide_strings_index: extended.wide_strings_index,
192        })
193    }
194}
195
196use std::io::Write;
197
198impl ToBinary for PcbText {
199    fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
200        use byteorder::{LittleEndian, WriteBytesExt};
201
202        // Common primitive fields
203        self.base.common.write_to(writer)?;
204
205        // Corner1
206        writer.write_i32::<LittleEndian>(self.base.corner1.x.to_raw())?;
207        writer.write_i32::<LittleEndian>(self.base.corner1.y.to_raw())?;
208
209        // Height (derived from corner2 - corner1)
210        let height = self.base.corner2.y.to_raw() - self.base.corner1.y.to_raw();
211        writer.write_i32::<LittleEndian>(height)?;
212
213        // Stroke font
214        writer.write_i16::<LittleEndian>(self.stroke_font.to_i16())?;
215
216        // Rotation
217        writer.write_f64::<LittleEndian>(self.base.rotation)?;
218
219        // Mirrored
220        writer.write_u8(if self.mirrored { 1 } else { 0 })?;
221
222        // Stroke width
223        writer.write_i32::<LittleEndian>(self.stroke_width.to_raw())?;
224
225        // Extended binary data
226        writer.write_u16::<LittleEndian>(0)?; // _unknown1
227        writer.write_u8(0)?; // _unknown2
228
229        // Text kind
230        writer.write_u8(self.text_kind.to_byte())?;
231
232        // Font style
233        writer.write_u8(if self.font_bold { 1 } else { 0 })?;
234        writer.write_u8(if self.font_italic { 1 } else { 0 })?;
235
236        // Font name (32 bytes)
237        FontName(self.font_name.clone()).write_to(writer)?;
238
239        // Barcode margins
240        writer.write_i32::<LittleEndian>(self.barcode_lr_margin.to_raw())?;
241        writer.write_i32::<LittleEndian>(self.barcode_tb_margin.to_raw())?;
242
243        // Unknown fields
244        writer.write_i32::<LittleEndian>(0)?; // _unknown3
245        writer.write_i32::<LittleEndian>(0)?; // _unknown4
246        writer.write_u8(0)?; // _unknown5
247        writer.write_u8(0)?; // _unknown6
248        writer.write_i32::<LittleEndian>(0)?; // _unknown7
249        writer.write_u16::<LittleEndian>(0)?; // _unknown8
250        writer.write_i32::<LittleEndian>(0)?; // _unknown9
251        writer.write_i32::<LittleEndian>(0)?; // _unknown10
252
253        // Font inverted
254        writer.write_u8(if self.font_inverted { 1 } else { 0 })?;
255        writer.write_i32::<LittleEndian>(self.font_inverted_border.to_raw())?;
256
257        // Wide strings index
258        writer.write_i32::<LittleEndian>(self.wide_strings_index)?;
259
260        // Unknown11
261        writer.write_i32::<LittleEndian>(0)?;
262
263        // Font inverted rect
264        writer.write_u8(if self.font_inverted_rect { 1 } else { 0 })?;
265        writer.write_i32::<LittleEndian>(self.font_inverted_rect_width.to_raw())?;
266        writer.write_i32::<LittleEndian>(self.font_inverted_rect_height.to_raw())?;
267        writer.write_u8(self.font_inverted_rect_justification.to_byte())?;
268        writer.write_i32::<LittleEndian>(self.font_inverted_rect_text_offset.to_raw())?;
269
270        Ok(())
271    }
272
273    fn binary_size(&self) -> usize {
274        // This is an approximation - actual size depends on content
275        200
276    }
277}
278
279impl PcbText {
280    /// Create a new text element.
281    ///
282    /// # Arguments
283    /// * `x` - X position in millimeters
284    /// * `y` - Y position in millimeters
285    /// * `text` - Text content
286    /// * `height` - Text height in millimeters
287    /// * `stroke_width` - Stroke width in millimeters
288    /// * `rotation` - Rotation angle in degrees
289    /// * `mirrored` - Whether the text is mirrored
290    /// * `layer` - The layer to place the text on
291    #[allow(clippy::too_many_arguments)]
292    pub fn new(
293        x: f64,
294        y: f64,
295        text: &str,
296        height: f64,
297        stroke_width: f64,
298        rotation: f64,
299        mirrored: bool,
300        layer: crate::types::Layer,
301    ) -> Self {
302        use super::primitive::{PcbFlags, PcbPrimitiveCommon};
303
304        let corner1 = CoordPoint::from_mms(x, y);
305        let corner2 = CoordPoint::from_mms(x, y + height);
306
307        PcbText {
308            base: PcbRectangularBase {
309                common: PcbPrimitiveCommon {
310                    layer,
311                    flags: PcbFlags::UNLOCKED | PcbFlags::UNKNOWN8,
312                    unique_id: None,
313                },
314                corner1,
315                corner2,
316                rotation,
317            },
318            mirrored,
319            text_kind: PcbTextKind::Stroke,
320            stroke_font: PcbTextStrokeFont::Default,
321            stroke_width: Coord::from_mms(stroke_width),
322            font_bold: false,
323            font_italic: false,
324            font_name: String::new(),
325            barcode_lr_margin: Coord::from_raw(0),
326            barcode_tb_margin: Coord::from_raw(0),
327            font_inverted: false,
328            font_inverted_border: Coord::from_raw(0),
329            font_inverted_rect: false,
330            font_inverted_rect_width: Coord::from_raw(0),
331            font_inverted_rect_height: Coord::from_raw(0),
332            font_inverted_rect_justification: PcbTextJustification::BottomLeft,
333            font_inverted_rect_text_offset: Coord::from_raw(0),
334            text: text.to_string(),
335            wide_strings_index: -1,
336        }
337    }
338
339    /// Get the text height.
340    pub fn height(&self) -> Coord {
341        self.base.height()
342    }
343
344    /// Calculate the bounding rectangle.
345    pub fn calculate_bounds(&self) -> CoordRect {
346        self.base.calculate_bounds()
347    }
348}