From f71c0327d17ba8c9ade1d67dff14bae726cb74b3 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Thu, 24 Aug 2023 01:56:20 -0400 Subject: [PATCH 01/15] Add macro implementations for arrays --- Cargo.lock | 5 +- altium-macros/src/lib.rs | 188 ++++++++++++++++++++++++++++++++++----- altium/Cargo.toml | 1 + altium/src/__private.rs | 28 ++---- altium/src/logging.rs | 12 ++- 5 files changed, 190 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9489a41..3dbf19d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,7 @@ dependencies = [ "flate2", "image", "lazy_static", + "log", "num_enum", "quick-xml", "regex", @@ -262,9 +263,9 @@ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" diff --git a/altium-macros/src/lib.rs b/altium-macros/src/lib.rs index d6aa8d4..0dc80cc 100644 --- a/altium-macros/src/lib.rs +++ b/altium-macros/src/lib.rs @@ -4,8 +4,8 @@ use std::collections::BTreeMap; use convert_case::{Case, Casing}; use proc_macro::TokenStream; -use proc_macro2::{Ident, Literal, TokenStream as TokenStream2, TokenTree}; -use quote::quote; +use proc_macro2::{Ident, Literal, Span, TokenStream as TokenStream2, TokenTree}; +use quote::{quote, ToTokens}; use syn::{parse2, Attribute, Data, DeriveInput, Meta, Type}; /// Derive `FromRecord` for a type. See that trait for better information. @@ -39,61 +39,127 @@ fn inner(tokens: TokenStream2) -> syn::Result { panic!("only usable on structs"); }; - let mut attr_map = parse_attrs(parsed.attrs).expect("attribute with `id = n` required"); - let TokenTree::Literal(id) = attr_map.remove("id").expect("record ID required") else { + // Parse outer attributes + let mut struct_attr_map = parse_attrs(parsed.attrs).expect("attribute with `id = n` required"); + let TokenTree::Literal(id) = struct_attr_map.remove("id").expect("record ID required") else { panic!("record id should be a literal"); }; - let use_box = match attr_map.remove("use_box") { + let use_box = match struct_attr_map.remove("use_box") { Some(TokenTree::Ident(val)) if val == "true" => true, Some(TokenTree::Ident(val)) if val == "false" => true, Some(v) => panic!("Expected ident but got {v:?}"), None => false, }; - error_if_map_not_empty(&attr_map); + error_if_map_not_empty(&struct_attr_map); + + let mut match_stmts: Vec = Vec::new(); + let mut outer_flags: Vec = Vec::new(); - let mut match_stmts = Vec::new(); for field in data.fields { - // Convert to pascal case let Type::Path(path) = field.ty else { panic!("invalid type") }; let field_name = field.ident.unwrap(); - let mut field_map = parse_attrs(field.attrs).unwrap_or_default(); + let mut field_attr_map = parse_attrs(field.attrs).unwrap_or_default(); + + if let Some(arr_val) = field_attr_map.remove("array") { + let arr_val_str = arr_val.to_string(); + if arr_val_str == "true" { + let count_ident = field_attr_map + .remove("count") + .expect("missing 'count' attribute"); + + process_array(&name, &field_name, count_ident, &mut match_stmts); + error_if_map_not_empty(&field_attr_map); + continue; + } else if arr_val_str != "false" { + panic!("array must be `true` or `false` but got {arr_val_str}"); + } + } // We match a single literal, like `OwnerPartId` - let match_pat = match field_map.remove("rename") { + // Perform renaming if attribute requests it + let match_pat = match field_attr_map.remove("rename") { Some(TokenTree::Literal(v)) => v, Some(v) => panic!("expected literal, got {v:?}"), None => create_key_name(&field_name), }; - error_if_map_not_empty(&field_map); + + // If we haven't consumed all attributes, yell + error_if_map_not_empty(&field_attr_map); let match_lit = match_pat.to_string(); - let ret_stmt = if path.path.segments.first().unwrap().ident == "Option" { + let field_name_str = field_name.to_string(); + let update_stmt = if path.path.segments.first().unwrap().ident == "Option" { // Optional return quote! { ret.#field_name = Some(parsed); } } else { quote! { ret.#field_name = parsed; } }; + // Altium does this weird thing where it will create a `%UTF8%` key and + // a key without that. + let path_str = path.to_token_stream().to_string(); + let add_utf8_match = path_str.contains("String") || path_str.contains("str"); + + let (utf8_pat, utf8_def_flag, utf8_check_flag) = if add_utf8_match { + let match_pat_utf8 = create_key_name_utf8(&match_pat); + let match_lit_utf8 = match_pat.to_string(); + let flag_ident = Ident::new( + &format!("{field_name_str}_found_utf8_field"), + Span::call_site(), + ); + + let pat = quote! { + #match_pat_utf8 => { + let parsed = val.parse_as_utf8() + // Add context of what we were trying to parse for errors + .context(concat!( + "while matching `", #match_lit_utf8, "` (`", #field_name_str , + "`) for `", stringify!(#name), "` (via proc macro)" + ))?; + + #flag_ident = true; + #update_stmt + }, + }; + let def_flag = quote! { let mut #flag_ident: bool = false; }; + let check_flag = quote! { + if #flag_ident { + ::log::debug!("skipping {} after finding utf8", #field_name_str); + continue; + } + }; + + (pat, def_flag, check_flag) + } else { + ( + TokenStream2::new(), + TokenStream2::new(), + TokenStream2::new(), + ) + }; + let quoted = quote! { + #utf8_pat + #match_pat => { + #utf8_check_flag + let parsed = val.parse_as_utf8() - // Add context of what we were trying to parse + // Add context of what we were trying to parse for errors .context(concat!( - "while matching `", - #match_lit, - "` for `", - stringify!(#name), - "` (via proc macro)" + "while matching `", #match_lit, "` (`", #field_name_str ,"`) for `", + stringify!(#name), "` (via proc macro)" ))?; - #ret_stmt + #update_stmt }, }; + outer_flags.push(utf8_def_flag); match_stmts.push(quoted); } @@ -111,10 +177,14 @@ fn inner(tokens: TokenStream2) -> syn::Result { records: I, ) -> Result { let mut ret = Self::default(); + + // Boolean flags used to track what we have found throughout the loop + #(#outer_flags)* + for (key, val) in records { match key { #(#match_stmts)* - _ => crate::__private::macro_unsupported_key(stringify!(#name), key, val) + _ => crate::logging::macro_unsupported_key(stringify!(#name), key, val) } } @@ -188,6 +258,68 @@ fn error_if_map_not_empty(map: &BTreeMap) { assert!(map.is_empty(), "unexpected pairs {map:?}"); } +/// Setup handling of `X1 = 1234, Y1 = 909` +fn process_array( + name: &Ident, + field_name: &Ident, + count_ident_tt: TokenTree, + match_stmts: &mut Vec, +) { + let TokenTree::Literal(match_pat) = count_ident_tt else { + panic!("expected a literal for `count`"); + }; + + let field_name_str = field_name.to_string(); + let match_pat_str = match_pat.to_string(); + let count_match = quote! { + // Set the length of our array once given + #match_pat => { + let count = val.parse_as_utf8().context(concat!( + "while matching `", #match_pat_str, "` (`", #field_name_str ,"`) for `", + stringify!(#name), "` (via proc macro array)" + ))?; + + ret.#field_name = vec![crate::common::Location::default(); count]; + }, + + // Set an X value if given + xstr if crate::common::is_number_pattern(xstr, b'X') => { + let idx: usize = xstr.strip_prefix(b"X").unwrap() + .parse_as_utf8() + .or_context(|| format!( + "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) + ))?; + + let x = val.parse_as_utf8().or_context(|| format!( + "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) + ))?; + + ret.#field_name[idx - 1].x = x; + }, + + // Set a Y value if given + ystr if crate::common::is_number_pattern(ystr, b'Y') => { + let idx: usize = ystr.strip_prefix(b"Y").unwrap() + .parse_as_utf8() + .or_context(|| format!( + "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) + ))?; + + let y = val.parse_as_utf8().or_context(|| format!( + "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) + ))?; + + ret.#field_name[idx - 1].y = y; + }, + }; + + match_stmts.push(count_match); +} + /// From a field in our struct, create the name we should match by fn create_key_name(id: &Ident) -> Literal { // Replace these strings, just inconsistencies @@ -203,11 +335,16 @@ fn create_key_name(id: &Ident) -> Literal { ("Frac", "_Frac"), ]; + // Skip replacements if we have a full match + const REPLACE_SKIP: &[&str] = &["CornerXRadius", "CornerYRadius"]; + // First, convert to Pascal case let mut id_str = id.to_string().to_case(Case::Pascal); - for (from, to) in REPLACE { - id_str = id_str.replace(from, to); + if !REPLACE_SKIP.contains(&id_str.as_str()) { + for (from, to) in REPLACE { + id_str = id_str.replace(from, to); + } } Literal::byte_string(id_str.as_bytes()) @@ -223,3 +360,10 @@ fn create_key_name(id: &Ident) -> Literal { // None if field_name == "is_not_accessible" => Literal::byte_string(b"IsNotAccesible"), // None => Literal::byte_string(field_name.to_string().to_case(Case::Pascal).as_bytes()), } + +/// Convert `name` -> `%UTF8%name` for Altium's weird UTF8 pattern +fn create_key_name_utf8(lit: &Literal) -> Literal { + let s = lit.to_string(); + let inner = s.strip_prefix("b\"").unwrap().strip_suffix('"').unwrap(); + Literal::byte_string(format!("%UTF8%{inner}").as_bytes()) +} diff --git a/altium/Cargo.toml b/altium/Cargo.toml index ddaa248..0468e82 100644 --- a/altium/Cargo.toml +++ b/altium/Cargo.toml @@ -14,6 +14,7 @@ flate2 = "1.0.26" # image = "0.24.6" image = { version = "0.24.6", default-features = false, features = ["png", "bmp", "jpeg"] } lazy_static = "1.4.0" +log = "0.4.20" num_enum = "0.6.1" quick-xml = "0.30.0" regex = "1.9.1" diff --git a/altium/src/__private.rs b/altium/src/__private.rs index 6e5ce45..01783f8 100644 --- a/altium/src/__private.rs +++ b/altium/src/__private.rs @@ -1,19 +1,9 @@ -//! Things only used for testing - -use std::sync::atomic::Ordering; - -use crate::{common::buf2lstring, logging::UNSUPPORTED_KEYS}; - -pub fn num_unsupported_keys() -> u32 { - UNSUPPORTED_KEYS.load(Ordering::Relaxed) -} - -/// Called from our proc macro to log a key -pub fn macro_unsupported_key(name: &str, key: &[u8], val: &[u8]) { - UNSUPPORTED_KEYS.fetch_add(1, Ordering::Relaxed); - eprintln!( - "unsupported key for `{name}`: {}={} (via `FromRecord` derive)", - buf2lstring(key), - buf2lstring(val) - ); -} +//! Things only used for testing + +use std::sync::atomic::Ordering; + +use crate::{common::buf2lstring, logging::UNSUPPORTED_KEYS}; + +pub fn num_unsupported_keys() -> u32 { + UNSUPPORTED_KEYS.load(Ordering::Relaxed) +} diff --git a/altium/src/logging.rs b/altium/src/logging.rs index 30fc35d..a7a0f96 100644 --- a/altium/src/logging.rs +++ b/altium/src/logging.rs @@ -8,5 +8,15 @@ pub static UNSUPPORTED_KEYS: AtomicU32 = AtomicU32::new(0); /// Log the unsupported key pub fn log_unsupported_key(key: &[u8], val: &[u8]) { UNSUPPORTED_KEYS.fetch_add(1, Ordering::Relaxed); - eprintln!("unsupported key {}={}", buf2lstring(key), buf2lstring(val)); + log::warn!("unsupported key {}={}", buf2lstring(key), buf2lstring(val)); +} + +/// Called from our proc macro to log a key +pub fn macro_unsupported_key(name: &str, key: &[u8], val: &[u8]) { + UNSUPPORTED_KEYS.fetch_add(1, Ordering::Relaxed); + log::warn!( + "unsupported key for `{name}`: {}={} (via `FromRecord` derive)", + buf2lstring(key), + buf2lstring(val) + ); } From 70699de0876e9aea5a4c8253b479827d35e0645e Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Thu, 24 Aug 2023 01:59:07 -0400 Subject: [PATCH 02/15] Fix static warnings --- altium/src/sch/schlib.rs | 18 +++++++++--------- altium/src/sch/storage.rs | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/altium/src/sch/schlib.rs b/altium/src/sch/schlib.rs index 06ff610..4a9fc23 100644 --- a/altium/src/sch/schlib.rs +++ b/altium/src/sch/schlib.rs @@ -219,32 +219,32 @@ pub(crate) struct SchLibMeta { /// Parse implementation impl SchLibMeta { - const STREAMNAME: &str = "FileHeader"; + const STREAMNAME: &'static str = "FileHeader"; /// Magic header found in all streams - const HEADER: &[u8] = + const HEADER: &'static [u8] = b"HEADER=Protel for Windows - Schematic Library Editor Binary File Version 5.0"; - const HEADER_KEY: &[u8] = b"HEADER"; + const HEADER_KEY: &'static [u8] = b"HEADER"; // /// Every header starts with this // const PFX: &[u8] = &[0x7a, 0x04, 0x00, 0x00, b'|']; // Seems like each stream starts with 4 random bytes followed by a `|`? const PFX_LEN: usize = 5; - const SFX: &[u8] = &[0x00]; + const SFX: &'static [u8] = &[0x00]; /* font-related items */ /// `FontName1=Times New Roman` - const FONT_NAME_PFX: &[u8] = b"FontName"; + const FONT_NAME_PFX: &'static [u8] = b"FontName"; /// `Size1=9` - const FONT_SIZE_PFX: &[u8] = b"Size"; + const FONT_SIZE_PFX: &'static [u8] = b"Size"; /* part-related items */ /// `Libref0=Part Name` - const COMP_LIBREF_PFX: &[u8] = b"LibRef"; + const COMP_LIBREF_PFX: &'static [u8] = b"LibRef"; /// `CompDescr0=Long description of thing Name` - const COMP_DESC_PFX: &[u8] = b"CompDescr"; + const COMP_DESC_PFX: &'static [u8] = b"CompDescr"; /// `PartCount0=2` number of subcomponents (seems to default to 2?) - const COMP_PARTCOUNT_PFX: &[u8] = b"PartCount"; + const COMP_PARTCOUNT_PFX: &'static [u8] = b"PartCount"; /// Validate a `FileHeader` and extract its information /// diff --git a/altium/src/sch/storage.rs b/altium/src/sch/storage.rs index 8fc95e3..ffab15e 100644 --- a/altium/src/sch/storage.rs +++ b/altium/src/sch/storage.rs @@ -48,7 +48,7 @@ pub enum CompressedData { } impl Storage { - const STREAMNAME: &str = "Storage"; + const STREAMNAME: &'static str = "Storage"; /// Get the data from a key (path) name if available /// From 02d1a06a4ace02933eb91df623b41d301f7fe212 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Thu, 24 Aug 2023 02:03:20 -0400 Subject: [PATCH 03/15] Improve drawing traits, adjust parsing - Restructure `Canvas` and `Draw` - Enable parsing of Altium arrays - Restructure logging --- altium/src/common.rs | 58 +++++- altium/src/draw/canvas.rs | 58 ++++++ altium/src/draw/mod.rs | 115 +---------- altium/src/draw/svg.rs | 238 ++++++++++++++++++++++ altium/src/lib.rs | 3 +- altium/src/prj/parse.rs | 12 +- altium/src/prj/prjcfg.rs | 4 +- altium/src/sch/component.rs | 18 +- altium/src/sch/pin.rs | 22 +- altium/src/sch/record.rs | 91 ++++++++- altium/src/sch/record/draw.rs | 276 ++++++++++++++++---------- altium/src/sch/schlib.rs | 13 +- altium/src/sch/schlib/section_keys.rs | 29 ++- 13 files changed, 664 insertions(+), 273 deletions(-) create mode 100644 altium/src/draw/canvas.rs create mode 100644 altium/src/draw/svg.rs diff --git a/altium/src/common.rs b/altium/src/common.rs index 4ec9093..211043d 100644 --- a/altium/src/common.rs +++ b/altium/src/common.rs @@ -1,5 +1,7 @@ use std::{fmt, str}; +use uuid::Uuid; + use crate::error::{ErrorKind, TruncBuf}; use crate::parse::{FromUtf8, ParseUtf8}; @@ -15,10 +17,12 @@ pub struct Location { } impl Location { + #[must_use] pub fn new(x: i32, y: i32) -> Self { Self { x, y } } + #[must_use] pub fn add_x(self, x: i32) -> Self { Self { x: self.x + x, @@ -26,6 +30,7 @@ impl Location { } } + #[must_use] pub fn add_y(self, y: i32) -> Self { Self { x: self.x, @@ -45,36 +50,53 @@ pub enum Visibility { /// // TODO: figure out what file types use this exact format #[derive(Clone, Copy, PartialEq)] -pub struct UniqueId([u8; 8]); +pub enum UniqueId { + /// Altium's old style UUID + Simple([u8; 8]), + Uuid(Uuid), +} impl UniqueId { - pub(crate) fn from_slice>(buf: S) -> Option { - buf.as_ref().try_into().ok().map(Self) + fn from_slice>(buf: S) -> Option { + buf.as_ref() + .try_into() + .ok() + .map(Self::Simple) + .or_else(|| Uuid::try_parse_ascii(buf.as_ref()).ok().map(Self::Uuid)) } +} - /// Get this `UniqueId` as a string - pub fn as_str(&self) -> &str { - str::from_utf8(&self.0).expect("unique IDs should always be ASCII") +impl fmt::Display for UniqueId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UniqueId::Simple(v) => str::from_utf8(v) + .expect("unique IDs should always be ASCII") + .fmt(f), + UniqueId::Uuid(v) => v.as_hyphenated().fmt(f), + } } } impl Default for UniqueId { fn default() -> Self { - Self(*b"00000000") + Self::Simple(*b"00000000") } } impl fmt::Debug for UniqueId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("UniqueId").field(&self.as_str()).finish() + f.debug_tuple("UniqueId").field(&self.to_string()).finish() } } impl FromUtf8<'_> for UniqueId { fn from_utf8(buf: &[u8]) -> Result { - Ok(Self(buf.as_ref().try_into().map_err(|_| { - ErrorKind::InvalidUniqueId(TruncBuf::new(buf)) - })?)) + buf.as_ref() + .try_into() + .ok() + .map(Self::Simple) + .or_else(|| Uuid::try_parse_ascii(buf).ok().map(Self::Uuid)) + .ok_or(ErrorKind::InvalidUniqueId(TruncBuf::new(buf))) } } @@ -225,3 +247,17 @@ pub enum PosVert { Middle, Bottom, } + +/// Verify a number pattern matches, e.g. `X100` +pub fn is_number_pattern(s: &[u8], prefix: u8) -> bool { + if let Some(stripped) = s + .strip_prefix(&[prefix]) + .map(|s| s.strip_prefix(&[b'-']).unwrap_or(s)) + { + if stripped.iter().all(u8::is_ascii_digit) { + return true; + } + } + + false +} diff --git a/altium/src/draw/canvas.rs b/altium/src/draw/canvas.rs new file mode 100644 index 0000000..173d2ec --- /dev/null +++ b/altium/src/draw/canvas.rs @@ -0,0 +1,58 @@ +use crate::{ + common::{Color, Location, PosHoriz, PosVert, Rotation}, + font::Font, + sch::Justification, +}; + +/// Generic trait for something that can be drawn. Beware, unstable! +pub trait Canvas { + fn draw_text(&mut self, item: DrawText); + fn draw_line(&mut self, item: DrawLine); + fn draw_polygon(&mut self, item: DrawPolygon); + fn draw_rectangle(&mut self, item: DrawRectangle); + fn draw_image(&mut self, item: DrawImage); + fn add_comment>(&mut self, comment: S) {} +} + +/// Helper struct to write some text +#[derive(Clone, Debug, Default)] +pub struct DrawText<'a> { + pub x: i32, + pub y: i32, + pub text: &'a str, + pub font: &'a Font, + pub anchor_h: PosHoriz, + pub anchor_v: PosVert, + pub color: Color, + pub rotation: Rotation, +} + +#[derive(Clone, Debug, Default)] +pub struct DrawLine { + pub start: Location, + pub end: Location, + pub color: Color, + pub width: u16, + // pub width: Option<&'a str>, +} + +#[derive(Clone, Debug, Default)] +pub struct DrawRectangle { + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, + pub fill_color: Color, + pub stroke_color: Color, + pub stroke_width: u16, +} + +#[derive(Clone, Debug, Default)] +pub struct DrawPolygon<'a> { + pub locations: &'a [Location], + pub fill_color: Color, + pub stroke_color: Color, + pub stroke_width: u16, +} + +pub struct DrawImage {} diff --git a/altium/src/draw/mod.rs b/altium/src/draw/mod.rs index f1bb878..c8e0510 100644 --- a/altium/src/draw/mod.rs +++ b/altium/src/draw/mod.rs @@ -1,107 +1,17 @@ -mod text; +pub(crate) mod canvas; +mod svg; use core::{ cmp::{max, min}, mem, }; -use svg::node::element::SVG as Svg; -pub use text::{DrawLine, DrawText}; +pub use canvas::{Canvas, DrawImage, DrawLine, DrawPolygon, DrawRectangle, DrawText}; +pub use self::svg::SvgCtx; +pub use crate::common::{Color, Location, PosHoriz, PosVert}; use crate::font::Font; -pub struct SvgCtx { - svg: Svg, - /// `(min, max)` values of x - x_range: Option<(i32, i32)>, - /// `(min, max)` values of y - y_range: Option<(i32, i32)>, - /// True if the image header has already been set - has_embedded_images: bool, -} - -impl SvgCtx { - pub fn new() -> Self { - Self { - svg: Svg::new(), - x_range: None, - y_range: None, - has_embedded_images: false, - } - } - - /// Add a node to this svg - pub fn add_node(&mut self, node: T) - where - T: Into>, - { - // Bad API means we need to do memory tricks... - let mut working = Svg::new(); - mem::swap(&mut self.svg, &mut working); - working = working.add(node); - mem::swap(&mut self.svg, &mut working); - } - - /// Translate from (0, 0) in bottom left to (0, 0) in top left. Makes sure - /// `x` and `x + width` are within the view box. - pub fn x_coord(&mut self, x: i32, width: i32) -> i32 { - let (mut min_x, mut max_x) = self.x_range.unwrap_or((x, x)); - let edge = x + width; // Add width (allows for negative values) - min_x = min(min(min_x, x), edge); - max_x = max(max(max_x, x), edge); - - self.x_range = Some((min_x, max_x)); - x - } - - /// Translate from (0, 0) in bottom left to (0, 0) in top left - /// - /// Updates the y location ranges if needed - pub fn y_coord(&mut self, y: i32, height: i32) -> i32 { - let new_y = -y - height; - let (mut min_y, mut max_y) = self.y_range.unwrap_or((new_y, new_y)); - let edge = new_y + height; // Add height (allows for negative values) - min_y = min(min(min_y, new_y), edge); - max_y = max(max(max_y, new_y), edge); - - self.y_range = Some((min_y, max_y)); - new_y - } - - /// Get the svg - pub fn svg(self) -> Svg { - let mut svg = self.svg; - let (min_x, max_x) = self.x_range.unwrap_or((0, 0)); - let (min_y, max_y) = self.y_range.unwrap_or((0, 0)); - - // Add a 5% border on all sides - let side_extra = (max_x - min_x) / 20; - let vert_extra = (max_y - min_y) / 20; - - svg = svg.set( - "viewBox", - format!( - "{} {} {} {}", - min_x - side_extra, - min_y - vert_extra, - (max_x - min_x) + side_extra * 2, - (max_y - min_y) + vert_extra * 2, - ), - ); - - if self.has_embedded_images { - svg = svg.set("xmlns:xlink", "http://www.w3.org/1999/xlink"); - } - - svg - } - - /// Set xlink header for embedded images - pub fn enable_inline_images(&mut self) { - self.has_embedded_images = true; - } -} - pub trait Draw { type Context<'a>; @@ -110,18 +20,5 @@ pub trait Draw { /// This has a defualt implementation that does nothing for easier /// reusability #[allow(unused)] - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &Self::Context<'_>) {} -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_coord_offsets() { - let mut svg = SvgCtx::new(); - assert_eq!(10, svg.x_coord(10, 20)); - assert_eq!(-30, svg.y_coord(10, 20)); - assert_eq!(svg.x_range, Some((10, 30))); - assert_eq!(svg.y_range, Some((-30, -10))); - } + fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) {} } diff --git a/altium/src/draw/svg.rs b/altium/src/draw/svg.rs new file mode 100644 index 0000000..0f66d19 --- /dev/null +++ b/altium/src/draw/svg.rs @@ -0,0 +1,238 @@ +use std::cmp::{max, min}; +use std::mem; + +use svg::node::element::SVG as Svg; +use svg::node::{element as el, Text}; +use svg::Node; + +use super::{canvas, Canvas}; +use crate::common::Location; +use crate::{ + common::{Color, PosHoriz, PosVert, Rotation}, + font::Font, + sch::Justification, +}; + +#[derive(Clone, Debug)] +pub struct SvgCtx { + svg: Svg, + /// `(min, max)` values of x + x_range: Option<(i32, i32)>, + /// `(min, max)` values of y + y_range: Option<(i32, i32)>, + /// True if the image header has already been set + has_embedded_images: bool, +} + +impl SvgCtx { + pub fn new() -> Self { + Self { + svg: Svg::new(), + x_range: None, + y_range: None, + has_embedded_images: false, + } + } + + /// Add a node to this svg + pub fn add_node(&mut self, node: T) + where + T: Into>, + { + // Bad API means we need to do memory tricks... + let mut working = Svg::new(); + mem::swap(&mut self.svg, &mut working); + working = working.add(node); + mem::swap(&mut self.svg, &mut working); + } + + /// Translate from (0, 0) in bottom left to (0, 0) in top left. Makes sure + /// `x` and `x + width` are within the view box. + pub fn x_coord(&mut self, x: i32, width: i32) -> i32 { + let (mut min_x, mut max_x) = self.x_range.unwrap_or((x, x)); + let edge = x + width; // Add width (allows for negative values) + min_x = min(min(min_x, x), edge); + max_x = max(max(max_x, x), edge); + + self.x_range = Some((min_x, max_x)); + x + } + + /// Translate from (0, 0) in bottom left to (0, 0) in top left + /// + /// Updates the y location ranges if needed + pub fn y_coord(&mut self, y: i32, height: i32) -> i32 { + let new_y = -y - height; + let (mut min_y, mut max_y) = self.y_range.unwrap_or((new_y, new_y)); + let edge = new_y + height; // Add height (allows for negative values) + min_y = min(min(min_y, new_y), edge); + max_y = max(max(max_y, new_y), edge); + + self.y_range = Some((min_y, max_y)); + new_y + } + + /// Get the svg + pub fn svg(self) -> Svg { + let mut svg = self.svg; + let (min_x, max_x) = self.x_range.unwrap_or((0, 0)); + let (min_y, max_y) = self.y_range.unwrap_or((0, 0)); + + // Add a 5% border on all sides + let side_extra = (max_x - min_x) / 20; + let vert_extra = (max_y - min_y) / 20; + + svg = svg.set( + "viewBox", + format!( + "{} {} {} {}", + min_x - side_extra, + min_y - vert_extra, + (max_x - min_x) + side_extra * 2, + (max_y - min_y) + vert_extra * 2, + ), + ); + + if self.has_embedded_images { + svg = svg.set("xmlns:xlink", "http://www.w3.org/1999/xlink"); + } + + svg + } + + /// Set xlink header for embedded images + pub fn enable_inline_images(&mut self) { + self.has_embedded_images = true; + } +} + +impl Canvas for SvgCtx { + #[allow(clippy::similar_names)] + fn draw_text(&mut self, item: canvas::DrawText) { + use Justification as J; + use PosHoriz::{Center, Left, Right}; + use PosVert::{Bottom, Middle, Top}; + use Rotation::{R0, R180, R270, R90}; + + let cmt = svg::node::Comment::new(format!("{item:#?}")); + self.add_node(cmt); + + let (x, y) = (item.x, item.y); + let (width, height) = text_dims(item.text, item.font.size); + let halfwidth = width / 2; + let halfheight = height / 2; + + let anchor = match item.anchor_h { + PosHoriz::Left => "start", + PosHoriz::Center => "middle", + PosHoriz::Right => "end", + }; + + /// Offset of max x from min x + let (xoffn, xoffp) = match item.anchor_h { + Left => (0, width), + Center => (halfwidth, halfwidth), + Right => (width, 0), + }; + + let (yoffn, yoffp) = match item.anchor_v { + Top => (height, 0), + Middle => (halfheight, halfheight), + Bottom => (0, height), + }; + + let (xmin, xmax, ymin, ymax) = match item.rotation { + R0 => (x - xoffn, x + xoffp, y - yoffn, y + yoffp), + R90 => (x - yoffp, x + yoffn, y - xoffn, y + xoffp), + R180 => (x - xoffp, x + xoffn, y - yoffp, y + yoffn), + R270 => (x - yoffn, x + yoffp, y - xoffp, y + xoffn), + }; + + self.x_coord(xmin, xmax - xmin); + self.x_coord(ymin, ymax - ymin); + + let txtnode = Text::new(item.text); + let mut node = el::Text::new() + .set("x", self.x_coord(x, width)) + .set("y", self.y_coord(y, height)) + .set("text-anchor", anchor) + .set("font-size", format!("{}px", item.font.size * 7 / 10)) + .set("font-family", format!("{}, sans-serif", item.font.name)) + .set("transform", format!("rotate({})", item.rotation.as_int())); + node.append(txtnode); + self.add_node(node); + + // Add a circle to the text anchor + let node2 = el::Circle::new() + .set("cx", self.x_coord(x, width)) + .set("cy", self.y_coord(y, height)) + .set("r", 0.5) + .set("fill", "red"); + self.add_node(node2); + } + + fn draw_line(&mut self, item: canvas::DrawLine) { + let dx = item.start.x - item.end.x; + let dy = item.start.y - item.end.y; + + let mut node = el::Line::new() + .set("x1", self.x_coord(item.start.x, 0)) + .set("x2", self.x_coord(item.end.x, 0)) + .set("y1", self.y_coord(item.start.y, dy)) + .set("y2", self.y_coord(item.end.y, dy)) + .set("stroke", item.color.to_hex()); + + // if let Some(w) = item.width { + // node = node.set("stroke-width", w); + // } + node = node.set("stroke-width", format!("{}px", item.width)); + + self.add_node(node); + } + + fn draw_rectangle(&mut self, item: canvas::DrawRectangle) { + let node = el::Rectangle::new() + .set("width", item.width) + .set("height", item.height) + // Need top left corner to set location + .set("x", self.x_coord(item.x, item.width)) + .set("y", self.y_coord(item.y, item.height)) + .set("fill", item.fill_color.to_hex()) + .set("stroke", item.stroke_color.to_hex()) + .set("stroke-width", item.stroke_width); + self.add_node(node); + } + + fn draw_polygon(&mut self, _item: canvas::DrawPolygon) { + // todo!() + } + + fn draw_image(&mut self, item: canvas::DrawImage) {} + + fn add_comment>(&mut self, comment: S) { + let cmt = svg::node::Comment::new(comment); + self.add_node(cmt); + } +} + +/// Estimate the size of text +fn text_dims(text: &str, font_size: u16) -> (i32, i32) { + let fsize_i32: i32 = font_size.into(); + let width = fsize_i32 * i32::try_from(text.len()).unwrap(); + let height = fsize_i32; + + (width, height) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_coord_offsets() { + let mut svg = SvgCtx::new(); + assert_eq!(10, svg.x_coord(10, 20)); + assert_eq!(-30, svg.y_coord(10, 20)); + assert_eq!(svg.x_range, Some((10, 30))); + assert_eq!(svg.y_range, Some((-30, -10))); + } +} diff --git a/altium/src/lib.rs b/altium/src/lib.rs index b8c5931..e084c3f 100644 --- a/altium/src/lib.rs +++ b/altium/src/lib.rs @@ -11,14 +11,15 @@ #![allow(clippy::missing_errors_doc)] #![allow(clippy::struct_excessive_bools)] #![allow(clippy::missing_panics_doc)] +#![allow(clippy::new_without_default)] mod common; -mod draw; mod logging; mod parse; #[doc(hidden)] pub mod __private; +pub mod draw; pub mod dwf; pub mod error; pub mod font; diff --git a/altium/src/prj/parse.rs b/altium/src/prj/parse.rs index 30a990e..af8065a 100644 --- a/altium/src/prj/parse.rs +++ b/altium/src/prj/parse.rs @@ -2,7 +2,12 @@ use std::borrow::ToOwned; use ini::Properties; -use crate::{common::UniqueId, error::ErrorKind}; +use crate::{ + common::UniqueId, + error::{AddContext, ErrorKind}, + parse::FromUtf8, + Error, +}; /// Parse a string or default to an empty string pub fn parse_string(sec: &Properties, key: &str) -> String { @@ -22,8 +27,9 @@ pub fn parse_bool(sec: &Properties, key: &str) -> bool { } /// Extract a `UniqueId` from a buffer -pub fn parse_unique_id(sec: &Properties, key: &str) -> Result { +pub fn parse_unique_id(sec: &Properties, key: &str) -> Result { sec.get(key) .ok_or_else(|| ErrorKind::MissingSection(key.to_owned())) - .map(|v| UniqueId::from_slice(v).ok_or(ErrorKind::MissingUniqueId(v.to_owned())))? + .and_then(|v| UniqueId::from_utf8(v.as_bytes())) + .map_err(|e| e.context("parse_unique_id")) } diff --git a/altium/src/prj/prjcfg.rs b/altium/src/prj/prjcfg.rs index 98af78d..863f951 100644 --- a/altium/src/prj/prjcfg.rs +++ b/altium/src/prj/prjcfg.rs @@ -177,7 +177,7 @@ impl Document { } /// Create a vector of `Document`s from an ini file - fn from_prj_ini(ini: &Ini) -> Result, ErrorKind> { + fn from_prj_ini(ini: &Ini) -> Result, Error> { let mut doc_sections: Vec<&str> = ini .sections() .filter_map(|nameopt| { @@ -207,7 +207,7 @@ impl Document { } /// Create a single `Document` from an ini section - fn from_section(sec: &Properties) -> Result { + fn from_section(sec: &Properties) -> Result { Ok(Self { document_path: parse_string(sec, "DocumentPath"), annotation_en: parse_bool(sec, "AnnotationEnabled"), diff --git a/altium/src/sch/component.rs b/altium/src/sch/component.rs index d71f590..7a25199 100644 --- a/altium/src/sch/component.rs +++ b/altium/src/sch/component.rs @@ -1,6 +1,7 @@ //! Things related to the entire component use core::fmt::Write; +use std::cmp::Ordering; use std::fs::File; use std::io; use std::path::Path; @@ -10,7 +11,7 @@ use svg::node::element::SVG as Svg; use super::storage::Storage; use super::{SchDrawCtx, SchRecord}; -use crate::draw::{Draw, SvgCtx}; +use crate::draw::{Canvas, Draw, SvgCtx}; use crate::error::AddContext; use crate::font::FontCollection; use crate::sch::pin::SchPin; @@ -53,7 +54,7 @@ impl Component { }; for record in &self.records { - record.draw_svg(&mut draw, &ctx); + record.draw(&mut draw, &ctx); } draw.svg() } @@ -64,6 +65,14 @@ impl Component { svg::write(&file, &self.svg()) } + pub fn draw(&self, canvas: &mut C) { + let ctx = SchDrawCtx { + fonts: &self.fonts, + storage: &self.storage, + }; + self.records.iter().for_each(|r| r.draw(canvas, &ctx)); + } + /// The name of this part pub fn name(&self) -> &str { &self.name @@ -84,6 +93,11 @@ impl Component { .expect("no metadata record"); meta.description.as_deref().unwrap_or("") } + + /// Compare + pub fn name_cmp(&self, other: &Self) -> Option { + self.name.partial_cmp(&other.name) + } } /// Given a buffer for a component, split the records up diff --git a/altium/src/sch/pin.rs b/altium/src/sch/pin.rs index 92e3780..6da3ab5 100644 --- a/altium/src/sch/pin.rs +++ b/altium/src/sch/pin.rs @@ -3,6 +3,8 @@ use core::fmt; use std::str::{self, Utf8Error}; +use log::warn; + use super::SchRecord; use crate::common::{Location, Rotation, Visibility}; @@ -55,10 +57,9 @@ impl SchPin { let (name, rest) = sized_buf_to_utf8(rest, "name")?; let (designator, rest) = sized_buf_to_utf8(rest, "designator")?; - assert!( - matches!(rest, [_, 0x03, b'|', b'&', b'|']), - "unexpected rest: {rest:02x?}" - ); + if !matches!(rest, [_, 0x03, b'|', b'&', b'|']) { + warn!("unexpected rest: {rest:02x?}"); + } let retval = Self { owner_index: 0, @@ -91,16 +92,15 @@ impl SchPin { /// Altium stores the position of the pin at its non-connecting end. Which /// seems dumb. This provides the connecting end. pub(crate) fn location_conn(&self) -> Location { - let (x_orig, y_orig, len) = ( - self.location_x, - self.location_y, - i32::try_from(self.length).unwrap(), - ); + let x_orig = self.location_x; + let y_orig = self.location_y; + let len = i32::try_from(self.length).unwrap(); + let (x, y) = match self.rotation { Rotation::R0 => (x_orig + len, y_orig), - Rotation::R90 => (x_orig, y_orig - len), + Rotation::R90 => (x_orig, y_orig + len), Rotation::R180 => (x_orig - len, y_orig), - Rotation::R270 => (x_orig, y_orig + len), + Rotation::R270 => (x_orig, y_orig - len), }; Location { x, y } } diff --git a/altium/src/sch/record.rs b/altium/src/sch/record.rs index 3fc236d..66ba438 100644 --- a/altium/src/sch/record.rs +++ b/altium/src/sch/record.rs @@ -60,7 +60,7 @@ use num_enum::TryFromPrimitive; use super::params::Justification; use super::pin::SchPin; -use crate::common::{ReadOnlyState, UniqueId}; +use crate::common::{Location, ReadOnlyState, UniqueId}; use crate::error::AddContext; use crate::Error; use crate::{ @@ -181,6 +181,7 @@ pub struct MetaData { area_color: Color, color: Color, current_part_id: u8, + database_table_name: Box, #[from_record(rename = b"ComponentDescription")] pub(crate) description: Option>, /// Alternative display modes @@ -193,7 +194,9 @@ pub struct MetaData { /// Number of parts part_count: u8, part_id_locked: bool, + not_use_db_table_name: bool, sheet_part_file_name: Box, + design_item_id: Box, source_library_name: Box, target_file_name: Box, unique_id: UniqueId, @@ -216,6 +219,7 @@ pub struct Label { font_id: u16, index_in_sheet: i16, is_not_accessible: bool, + is_mirrored: bool, location_x: i32, location_y: i32, owner_index: u8, @@ -228,8 +232,16 @@ pub struct Label { #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 5)] pub struct Bezier { + color: Color, + index_in_sheet: i16, + is_not_accessible: bool, + line_width: u16, + #[from_record(array = true, count = b"LocationCount")] + locations: Vec, owner_index: u8, owner_part_id: i8, + owner_part_display_mode: i8, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -238,26 +250,48 @@ pub struct PolyLine { owner_index: u8, owner_part_id: i8, is_not_accessible: bool, - line_width: i8, + index_in_sheet: i16, + line_width: u16, color: Color, - location_count: u16, - // TODO: how to handle X1 Y1 X2 Y2 headers + #[from_record(array = true, count = b"LocationCount")] + locations: Vec, unique_id: UniqueId, - index_in_sheet: i16, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 7)] pub struct Polygon { + area_color: Color, + color: Color, + index_in_sheet: i16, + is_not_accessible: bool, + is_solid: bool, + line_width: u16, + #[from_record(array = true, count = b"LocationCount")] + locations: Vec, owner_index: u8, owner_part_id: i8, + owner_part_display_mode: i8, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 8)] pub struct Ellipse { + area_color: Color, + color: Color, + index_in_sheet: i16, + is_not_accessible: bool, + is_solid: bool, + line_width: u16, + location_x: i32, + location_y: i32, owner_index: u8, owner_part_id: i8, + owner_part_display_mode: i8, + radius: i32, + secondary_radius: i32, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -270,9 +304,23 @@ pub struct Piechart { #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 10)] pub struct RectangleRounded { + area_color: Color, + color: Color, + corner_x: i32, + corner_y: i32, + corner_x_radius: i32, + corner_y_radius: i32, + index_in_sheet: i16, + is_not_accessible: bool, + is_solid: bool, + line_width: u16, + location_x: i32, + location_y: i32, owner_index: u8, owner_part_id: i8, owner_part_display_mode: i8, + transparent: bool, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -283,11 +331,13 @@ pub struct ElipticalArc { is_not_accessible: bool, index_in_sheet: i16, location_x: i32, + location_x_frac: i32, location_y: i32, + location_y_frac: i32, radius: i8, - radius_frac: i16, + radius_frac: i32, secondary_radius: i8, - secondary_radius_frac: i16, + secondary_radius_frac: i32, line_width: i8, start_angle: f32, end_angle: f32, @@ -300,13 +350,38 @@ pub struct ElipticalArc { pub struct Arc { owner_index: u8, owner_part_id: i8, + is_not_accessible: bool, + index_in_sheet: i16, + location_x: i32, + location_y: i32, + radius: i8, + radius_frac: i32, + secondary_radius: i8, + secondary_radius_frac: i32, + line_width: i8, + start_angle: f32, + end_angle: f32, + color: Color, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 13)] pub struct Line { + color: Color, + corner_x: i32, + corner_y: i32, + index_in_sheet: i16, + is_not_accessible: bool, + is_solid: bool, + line_width: u16, + location_count: u16, + location_x: i32, + location_y: i32, owner_index: u8, owner_part_id: i8, + owner_part_display_mode: i8, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -327,6 +402,7 @@ pub struct Rectangle { owner_index: u8, owner_part_id: i8, owner_part_display_mode: i8, + transparent: bool, unique_id: UniqueId, } @@ -482,6 +558,7 @@ pub struct Parameter { color: Color, font_id: u16, unique_id: UniqueId, + read_only_state: i8, name: Box, is_hidden: bool, text: Box, diff --git a/altium/src/sch/record/draw.rs b/altium/src/sch/record/draw.rs index c4aaf7e..aadb3e9 100644 --- a/altium/src/sch/record/draw.rs +++ b/altium/src/sch/record/draw.rs @@ -4,7 +4,9 @@ use svg::node::{element as el, Text}; use svg::Node; use crate::common::{Color, Location, PosHoriz, PosVert, Rotation, Visibility}; -use crate::draw::{Draw, DrawLine, DrawText, SvgCtx}; +use crate::draw::canvas::DrawRectangle; +use crate::draw::canvas::{Canvas, DrawLine, DrawText}; +use crate::draw::{Draw, DrawPolygon}; use crate::font::{Font, FontCollection}; use crate::sch::params::Justification; use crate::sch::pin::SchPin; @@ -26,39 +28,39 @@ pub struct SchDrawCtx<'a> { impl Draw for record::SchRecord { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { match self { - // record::SchRecord::MetaData(v) => v.draw_svg(svg, ctx), - record::SchRecord::Pin(v) => v.draw_svg(svg, ctx), - // record::SchRecord::IeeeSymbol(v) => v.draw_svg(svg, ctx), - record::SchRecord::Label(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Bezier(v) => v.draw_svg(svg, ctx), - // record::SchRecord::PolyLine(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Polygon(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Ellipse(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Piechart(v) => v.draw_svg(svg, ctx), - // record::SchRecord::RectangleRounded(v) => v.draw_svg(svg, ctx), - // record::SchRecord::ElipticalArc(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Arc(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Line(v) => v.draw_svg(svg, ctx), - record::SchRecord::Rectangle(v) => v.draw_svg(svg, ctx), - // record::SchRecord::SheetSymbol(v) => v.draw_svg(svg, ctx), - // record::SchRecord::SheetEntry(v) => v.draw_svg(svg, ctx), - // record::SchRecord::PowerPort(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Port(v) => v.draw_svg(svg, ctx), - // record::SchRecord::NoErc(v) => v.draw_svg(svg, ctx), - // record::SchRecord::NetLabel(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Bus(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Wire(v) => v.draw_svg(svg, ctx), - // record::SchRecord::TextFrame(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Junction(v) => v.draw_svg(svg, ctx), - record::SchRecord::Image(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Sheet(v) => v.draw_svg(svg, ctx), - // record::SchRecord::SheetName(v) => v.draw_svg(svg, ctx), - // record::SchRecord::FileName(v) => v.draw_svg(svg, ctx), - // record::SchRecord::BusEntry(v) => v.draw_svg(svg, ctx), - // record::SchRecord::Template(v) => v.draw_svg(svg, ctx), - record::SchRecord::Parameter(v) => v.draw_svg(svg, ctx), + // record::SchRecord::MetaData(v) => v.draw(canvas, ctx), + record::SchRecord::Pin(v) => v.draw(canvas, ctx), + // record::SchRecord::IeeeSymbol(v) => v.draw(canvas, ctx), + record::SchRecord::Label(v) => v.draw(canvas, ctx), + // record::SchRecord::Bezier(v) => v.draw(canvas, ctx), + record::SchRecord::PolyLine(v) => v.draw(canvas, ctx), + record::SchRecord::Polygon(v) => v.draw(canvas, ctx), + // record::SchRecord::Ellipse(v) => v.draw(canvas, ctx), + // record::SchRecord::Piechart(v) => v.draw(canvas, ctx), + record::SchRecord::RectangleRounded(v) => v.draw(canvas, ctx), + // record::SchRecord::ElipticalArc(v) => v.draw(canvas, ctx), + // record::SchRecord::Arc(v) => v.draw(canvas, ctx), + record::SchRecord::Line(v) => v.draw(canvas, ctx), + record::SchRecord::Rectangle(v) => v.draw(canvas, ctx), + // record::SchRecord::SheetSymbol(v) => v.draw(canvas, ctx), + // record::SchRecord::SheetEntry(v) => v.draw(canvas, ctx), + // record::SchRecord::PowerPort(v) => v.draw(canvas, ctx), + // record::SchRecord::Port(v) => v.draw(canvas, ctx), + // record::SchRecord::NoErc(v) => v.draw(canvas, ctx), + // record::SchRecord::NetLabel(v) => v.draw(canvas, ctx), + // record::SchRecord::Bus(v) => v.draw(canvas, ctx), + // record::SchRecord::Wire(v) => v.draw(canvas, ctx), + // record::SchRecord::TextFrame(v) => v.draw(canvas, ctx), + // record::SchRecord::Junction(v) => v.draw(canvas, ctx), + record::SchRecord::Image(v) => v.draw(canvas, ctx), + // record::SchRecord::Sheet(v) => v.draw(canvas, ctx), + // record::SchRecord::SheetName(v) => v.draw(canvas, ctx), + // record::SchRecord::FileName(v) => v.draw(canvas, ctx), + // record::SchRecord::BusEntry(v) => v.draw(canvas, ctx), + // record::SchRecord::Template(v) => v.draw(canvas, ctx), + record::SchRecord::Parameter(v) => v.draw(canvas, ctx), // record::SchRecord::ImplementationList(v) => v.draw_svg(svg, ctx), // non-printing types // record::SchRecord::Designator(_) | record::SchRecord::Undefined => (), @@ -71,13 +73,12 @@ impl Draw for record::SchRecord { impl Draw for SchPin { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { use PosHoriz::{Center, Left, Right}; use PosVert::{Bottom, Middle, Top}; use Rotation::{R0, R180, R270, R90}; - let cmt = svg::node::Comment::new(format!("{self:#?}")); - svg.add_node(cmt); + canvas.add_comment(format!("{self:#?}")); let len: i32 = self.length.try_into().unwrap(); let (x1, y1) = (self.location_x, self.location_y); @@ -85,31 +86,29 @@ impl Draw for SchPin { let start = self.location(); let end = self.location_conn(); - DrawLine { + canvas.draw_line(DrawLine { start, end, color: Color::black(), - ..Default::default() - } - .draw(svg); + width: 4, + // ..Default::default() + }); // Altium draws a small white plus at the pin's connect position, so we // do too - DrawLine { + canvas.draw_line(DrawLine { start: end.add_x(1), end: end.add_x(-1), - color: Color::blue(), - width: Some("0.5px"), - } - .draw(svg); + color: Color::white(), + width: 1, + }); - DrawLine { + canvas.draw_line(DrawLine { start: end.add_y(1), end: end.add_y(-1), - color: Color::red(), - width: Some("0.5px"), - } - .draw(svg); + color: Color::white(), + width: 1, + }); // FIXME: use actual spacing & fonts from pin spec let (name_x, name_y, des_x, des_y, txt_rotation) = match self.rotation { @@ -126,7 +125,7 @@ impl Draw for SchPin { }; // Display name to the right of the pin - DrawText { + canvas.draw_text(DrawText { x: name_x, y: name_y, text: &self.name, @@ -134,8 +133,7 @@ impl Draw for SchPin { anchor_v, rotation: txt_rotation, ..Default::default() - } - .draw(svg); + }); } if self.designator_vis == Visibility::Visible { @@ -144,7 +142,7 @@ impl Draw for SchPin { R180 | R270 => (Right, Bottom), }; - DrawText { + canvas.draw_text(DrawText { x: des_x, y: des_y, text: &self.designator, @@ -152,8 +150,7 @@ impl Draw for SchPin { anchor_v, rotation: txt_rotation, ..Default::default() - } - .draw(svg); + }); } } } @@ -164,10 +161,10 @@ impl Draw for SchPin { impl Draw for record::Label { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { let font = &ctx.fonts.get_idx(self.font_id.into()); let (anchor_h, anchor_v) = self.justification.into(); - DrawText { + canvas.draw_text(DrawText { x: self.location_x, y: self.location_y, text: &self.text, @@ -176,38 +173,97 @@ impl Draw for record::Label { anchor_v, color: self.color, ..Default::default() // rotation: todo!(), - } - .draw(svg); + }); } } // impl Draw for record::Bezier {} -// impl Draw for record::PolyLine {} -// impl Draw for record::Polygon {} + +impl Draw for record::PolyLine { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + for window in self.locations.windows(2) { + let &[a, b] = window else { unreachable!() }; + + canvas.draw_line(DrawLine { + start: a, + end: b, + color: self.color, + width: self.line_width * 4, + }); + } + } +} + +impl Draw for record::Polygon { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + canvas.draw_polygon(DrawPolygon { + locations: &self.locations, + fill_color: self.area_color, + stroke_color: self.color, + stroke_width: self.line_width, + }); + } +} + // impl Draw for record::Ellipse {} // impl Draw for record::Piechart {} -// impl Draw for record::RectangleRounded {} + +impl Draw for record::RectangleRounded { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + let width = self.corner_x - self.location_x; + let height = self.corner_y - self.location_y; + + // FIXME: rounded rectangle + canvas.draw_rectangle(DrawRectangle { + x: self.location_x, + y: self.location_y, + width, + height, + fill_color: self.area_color, + stroke_color: self.color, + stroke_width: self.line_width, + }); + } +} + // impl Draw for record::ElipticalArc {} // impl Draw for record::Arc {} -// impl Draw for record::Line {} + +impl Draw for record::Line { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + canvas.draw_line(DrawLine { + start: Location::new(self.location_x, self.location_y), + end: Location::new(self.corner_x, self.corner_y), + color: self.color, + width: self.line_width, + }); + } +} impl Draw for record::Rectangle { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { let width = self.corner_x - self.location_x; let height = self.corner_y - self.location_y; - let node = el::Rectangle::new() - .set("width", width) - .set("height", height) - // Need top left corner to set location - .set("x", svg.x_coord(self.location_x, width)) - .set("y", svg.y_coord(self.location_y, height)) - .set("fill", self.area_color.to_hex()) - .set("stroke", self.color.to_hex()) - .set("stroke-width", self.line_width); - svg.add_node(node); + canvas.draw_rectangle(DrawRectangle { + x: self.location_x, + y: self.location_y, + width, + height, + fill_color: self.area_color, + stroke_color: self.color, + stroke_width: self.line_width, + }); } } @@ -224,39 +280,40 @@ impl Draw for record::Rectangle { impl Draw for record::Image { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &Self::Context<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + // TODO // TODO: just set to the URL. Maybe set whether or not to encode // somehow? - if !self.embed_image { - return; - } - - let Some(data) = ctx.storage.get_data(&self.file_name) else { - eprintln!("unable to find image at {}", self.file_name); - return; - }; - - let width = self.corner_x - self.location_x; - let height = self.corner_y - self.location_y; - - let mut b64_str = "data:image/png;base64,".to_owned(); - STANDARD_NO_PAD.encode_string(data, &mut b64_str); - assert!( - b64_str.len() < MAX_EMBED_SIZE, - "max size {MAX_EMBED_SIZE} bytes but got {}", - b64_str.len() - ); - - let node = el::Image::new() - .set("width", width) - .set("height", height) - // Need top left corner to set location - .set("x", svg.x_coord(self.location_x, width)) - .set("y", svg.y_coord(self.location_y, height)) - .set("xlink:href", b64_str); - svg.add_node(node); - - svg.enable_inline_images(); + // if !self.embed_image { + // return; + // } + + // let Some(data) = ctx.storage.get_data(&self.file_name) else { + // eprintln!("unable to find image at {}", self.file_name); + // return; + // }; + + // let width = self.corner_x - self.location_x; + // let height = self.corner_y - self.location_y; + + // let mut b64_str = "data:image/png;base64,".to_owned(); + // STANDARD_NO_PAD.encode_string(data, &mut b64_str); + // assert!( + // b64_str.len() < MAX_EMBED_SIZE, + // "max size {MAX_EMBED_SIZE} bytes but got {}", + // b64_str.len() + // ); + + // let node = el::Image::new() + // .set("width", width) + // .set("height", height) + // // Need top left corner to set location + // .set("x", svg.x_coord(self.location_x, width)) + // .set("y", svg.y_coord(self.location_y, height)) + // .set("xlink:href", b64_str); + // svg.add_node(node); + + // svg.enable_inline_images(); } } // impl Draw for record::Sheet {} @@ -268,16 +325,15 @@ impl Draw for record::Image { impl Draw for record::Parameter { type Context<'a> = SchDrawCtx<'a>; - fn draw_svg(&self, svg: &mut SvgCtx, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { let font = &ctx.fonts.get_idx(self.font_id.into()); - DrawText { + canvas.draw_text(DrawText { x: self.location_x, y: self.location_y, text: &self.text, font, ..Default::default() - } - .draw(svg); + }); } } diff --git a/altium/src/sch/schlib.rs b/altium/src/sch/schlib.rs index 4a9fc23..5cc201f 100644 --- a/altium/src/sch/schlib.rs +++ b/altium/src/sch/schlib.rs @@ -94,10 +94,10 @@ impl SchLib { // Scope of refcell borrow let mut cfile_ref = self.cfile.borrow_mut(); let mut stream = cfile_ref.open_stream(&data_path).unwrap_or_else(|e| { - panic!( - "missing required stream `{}` with error {e}", - data_path.display() - ) + dbg!(&meta); + dbg!(&data_path); + let path_disp = data_path.display(); + panic!("missing required stream `{path_disp}` with error {e}") }); stream.read_to_end(&mut buf).unwrap(); } @@ -256,8 +256,6 @@ impl SchLibMeta { let mut stream = cfile.open_stream(Self::STREAMNAME)?; stream.read_to_end(tmp_buf)?; - println!("parsing cfile:\n{}", String::from_utf8_lossy(tmp_buf)); - let to_parse = tmp_buf .get(Self::PFX_LEN..) .ok_or(ErrorKind::new_invalid_stream(Self::STREAMNAME, 0))? @@ -351,13 +349,10 @@ impl SchLibMeta { } ret.fonts = Arc::new(fonts.into()); - println!("done parsing cfile"); Ok(ret) } } -// pub struct Components {} - /// Information available in the header about a single component: includes /// libref and part count #[derive(Clone, Debug, Default)] diff --git a/altium/src/sch/schlib/section_keys.rs b/altium/src/sch/schlib/section_keys.rs index 1c94d1d..a9aade7 100644 --- a/altium/src/sch/schlib/section_keys.rs +++ b/altium/src/sch/schlib/section_keys.rs @@ -10,32 +10,37 @@ use crate::logging::log_unsupported_key; use crate::parse::ParseUtf8; use crate::Error; -const STREAMNAME: &str = "SectionKeys"; +const SEC_KEY_STREAM: &str = "SectionKeys"; const PFX_LEN: usize = 5; const SFX: &[u8] = &[0x00]; const LIBREF: &[u8] = b"LibRef"; const SECKEY: &[u8] = b"SectionKey"; /// Update a header with section keys. +/// +/// The `SectionKeys` stream stores a map of `libref -> section key` for some +/// librefs that are too long to be a section key (stream name) themselves. Go +/// through our extracted components and make sure that `sec_key` is replaced by +/// this map where needed. pub(crate) fn update_section_keys( cfile: &mut CompoundFile, tmp_buf: &mut Vec, header: &mut SchLibMeta, ) -> Result<(), Error> { - if !cfile.exists(STREAMNAME) { + if !cfile.exists(SEC_KEY_STREAM) { return Ok(()); } - let mut stream = cfile.open_stream(STREAMNAME)?; + let mut stream = cfile.open_stream(SEC_KEY_STREAM)?; stream.read_to_end(tmp_buf)?; let to_parse = tmp_buf .get(PFX_LEN..) - .ok_or(ErrorKind::new_invalid_stream(STREAMNAME, 0))? + .ok_or(ErrorKind::new_invalid_stream(SEC_KEY_STREAM, 0))? .strip_suffix(SFX) - .ok_or(ErrorKind::new_invalid_stream(STREAMNAME, tmp_buf.len()))?; + .ok_or(ErrorKind::new_invalid_stream(SEC_KEY_STREAM, tmp_buf.len()))?; - // libref -> section key + // keep a map of libref -> section key let mut map: Vec<(&str, &str)> = Vec::new(); for (key, val) in split_altium_map(to_parse) { @@ -54,12 +59,20 @@ pub(crate) fn update_section_keys( } for comp in &mut header.components { - // Find any keys that exist in our map + // Find any keys that exist in our map and replace them let Ok(idx) = map.binary_search_by_key(&comp.libref(), |x| x.0) else { + // If they aren't in our map, fixup only + comp.sec_key = fixup_sec_key(&comp.sec_key); continue; }; - comp.sec_key = map[idx].1.into(); + + comp.sec_key = fixup_sec_key(map[idx].1); } Ok(()) } + +/// Altium does some transformations for its stream paths, e.g. `/` -> `_` +fn fixup_sec_key(path: &str) -> Box { + path.replace('/', "_").into() +} From a98d426951b6b1e2304068875115210fcc6f1851 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 18 Sep 2023 21:43:24 -0400 Subject: [PATCH 04/15] Cherrypick altium work from the ecad-gui branch --- altium-macros/src/lib.rs | 211 +++++++++++++++++++++-------- altium/Cargo.toml | 7 +- altium/src/common.rs | 27 +++- altium/src/error.rs | 60 ++++++--- altium/src/font.rs | 20 ++- altium/src/parse/bin.rs | 40 ++++-- altium/src/parse/utf8.rs | 2 +- altium/src/sch.rs | 2 +- altium/src/sch/component.rs | 49 +------ altium/src/sch/pin.rs | 69 +++++++++- altium/src/sch/record.rs | 213 +++++++++++++++++++++++++++--- altium/src/sch/record/draw.rs | 118 +++++++++++++++-- altium/src/sch/record/parse.rs | 54 ++++++++ altium/src/sch/schdoc.rs | 134 ++++++++++++++++++- altium/src/sch/schlib.rs | 22 +-- altium/src/sch/storage.rs | 18 +-- altium/tests/include_test_util.rs | 11 ++ altium/tests/test_schdoc.rs | 13 ++ altium/tests/test_schlib.rs | 12 ++ 19 files changed, 876 insertions(+), 206 deletions(-) create mode 100644 altium/src/sch/record/parse.rs create mode 100644 altium/tests/include_test_util.rs create mode 100644 altium/tests/test_schdoc.rs diff --git a/altium-macros/src/lib.rs b/altium-macros/src/lib.rs index 0dc80cc..03ef58b 100644 --- a/altium-macros/src/lib.rs +++ b/altium-macros/src/lib.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; use convert_case::{Case, Casing}; use proc_macro::TokenStream; -use proc_macro2::{Ident, Literal, Span, TokenStream as TokenStream2, TokenTree}; +use proc_macro2::{Delimiter, Ident, Literal, Span, TokenStream as TokenStream2, TokenTree}; use quote::{quote, ToTokens}; use syn::{parse2, Attribute, Data, DeriveInput, Meta, Type}; @@ -45,6 +45,7 @@ fn inner(tokens: TokenStream2) -> syn::Result { panic!("record id should be a literal"); }; + // Handle cases where we want to box the struct let use_box = match struct_attr_map.remove("use_box") { Some(TokenTree::Ident(val)) if val == "true" => true, Some(TokenTree::Ident(val)) if val == "false" => true, @@ -52,6 +53,13 @@ fn inner(tokens: TokenStream2) -> syn::Result { None => false, }; + // Handle cases where our struct doesn't have the same name as the enum variant + let record_variant = match struct_attr_map.remove("record_variant") { + Some(TokenTree::Ident(val)) => val, + Some(v) => panic!("Expected ident but got {v:?}"), + None => name.clone(), + }; + error_if_map_not_empty(&struct_attr_map); let mut match_stmts: Vec = Vec::new(); @@ -72,7 +80,11 @@ fn inner(tokens: TokenStream2) -> syn::Result { .remove("count") .expect("missing 'count' attribute"); - process_array(&name, &field_name, count_ident, &mut match_stmts); + let arr_map = field_attr_map + .remove("map") + .expect("missing 'map' attribute"); + + process_array(&name, &field_name, count_ident, arr_map, &mut match_stmts); error_if_map_not_empty(&field_attr_map); continue; } else if arr_val_str != "false" { @@ -129,7 +141,9 @@ fn inner(tokens: TokenStream2) -> syn::Result { let def_flag = quote! { let mut #flag_ident: bool = false; }; let check_flag = quote! { if #flag_ident { - ::log::debug!("skipping {} after finding utf8", #field_name_str); + ::log::debug!(concat!( + "skipping ", #field_name_str, " after finding utf8 version" + )); continue; } }; @@ -164,9 +178,9 @@ fn inner(tokens: TokenStream2) -> syn::Result { } let ret_val = if use_box { - quote! { Ok(SchRecord::#name(Box::new(ret))) } + quote! { Ok(SchRecord::#record_variant(Box::new(ret))) } } else { - quote! { Ok(SchRecord::#name(ret)) } + quote! { Ok(SchRecord::#record_variant(ret)) } }; let ret = quote! { @@ -198,7 +212,7 @@ fn inner(tokens: TokenStream2) -> syn::Result { /// Next type of token we are expecting #[derive(Clone, Debug, PartialEq)] -enum AttrState { +enum AttrParseState { Key, /// Contains the last key we had Eq(String), @@ -218,35 +232,32 @@ fn parse_attrs(attrs: Vec) -> Option> { panic!("invalid usage; use `#[from_record(...=..., ...)]`"); }; - let mut state = AttrState::Key; + let mut state = AttrParseState::Key; let mut map = BTreeMap::new(); for token in list.tokens { match state { - AttrState::Key => { + AttrParseState::Key => { let TokenTree::Ident(idtoken) = token else { panic!("expected an identifier at {token}"); }; - state = AttrState::Eq(idtoken.to_string()); + state = AttrParseState::Eq(idtoken.to_string()); } - AttrState::Eq(key) => { - match token { - TokenTree::Punct(v) if v.as_char() == '=' => (), - _ => panic!("expected `=` at {token}"), + AttrParseState::Eq(key) => { + if !matches!(&token, TokenTree::Punct(v) if v.as_char() == '=') { + panic!("expected `=` at {token}"); } - - state = AttrState::Val(key); + state = AttrParseState::Val(key); } - AttrState::Val(key) => { + AttrParseState::Val(key) => { map.insert(key, token); - state = AttrState::Comma; + state = AttrParseState::Comma; } - AttrState::Comma => { - match token { - TokenTree::Punct(v) if v.as_char() == ',' => (), - _ => panic!("expected `,` at {token}"), - }; - state = AttrState::Key; + AttrParseState::Comma => { + if !matches!(&token, TokenTree::Punct(v) if v.as_char() == ',') { + panic!("expected `,` at {token}"); + } + state = AttrParseState::Key; } } } @@ -254,6 +265,74 @@ fn parse_attrs(attrs: Vec) -> Option> { Some(map) } +/// Next type of token we are expecting +#[derive(Clone, Debug, PartialEq)] +enum MapParseState { + Key, + /// Contains the last key we had + Dash(Ident), + Gt(Ident), + Val(Ident), + Comma, +} + +/// Parse a `(X -> x, Y -> y)` map that tells us how to set members based on +/// found items in an array. +/// +/// E.g. with the above, `X1` will set `record[1].x` +fn parse_map(map: TokenTree) -> Vec<(Ident, Ident)> { + let mut ret = Vec::new(); + + let TokenTree::Group(group) = map else { + panic!("expected group but got {map:?}") + }; + + if group.delimiter() != Delimiter::Parenthesis { + panic!("expected parenthese but got {:?}", group.delimiter()); + }; + + let mut state = MapParseState::Key; + + for token in group.stream() { + match state { + MapParseState::Key => { + let TokenTree::Ident(idtoken) = token else { + panic!("expected an identifier at {token}"); + }; + state = MapParseState::Dash(idtoken); + } + MapParseState::Dash(key) => { + if !matches!(&token, TokenTree::Punct(v) if v.as_char() == '-') { + panic!("expected `->` at {token}"); + } + state = MapParseState::Gt(key); + } + MapParseState::Gt(key) => { + if !matches!(&token, TokenTree::Punct(v) if v.as_char() == '>') { + panic!("expected `->` at {token}"); + } + state = MapParseState::Val(key); + } + MapParseState::Val(key) => { + let TokenTree::Ident(ident) = token else { + panic!("expcected ident but got {token}"); + }; + ret.push((key, ident)); + state = MapParseState::Comma; + } + MapParseState::Comma => { + if !matches!(&token, TokenTree::Punct(v) if v.as_char() == ',') { + panic!("expected `,` at {token}"); + } + + state = MapParseState::Key; + } + } + } + + ret +} + fn error_if_map_not_empty(map: &BTreeMap) { assert!(map.is_empty(), "unexpected pairs {map:?}"); } @@ -263,11 +342,13 @@ fn process_array( name: &Ident, field_name: &Ident, count_ident_tt: TokenTree, + arr_map_tt: TokenTree, match_stmts: &mut Vec, ) { let TokenTree::Literal(match_pat) = count_ident_tt else { panic!("expected a literal for `count`"); }; + let arr_map = parse_map(arr_map_tt); let field_name_str = field_name.to_string(); let match_pat_str = match_pat.to_string(); @@ -279,45 +360,69 @@ fn process_array( stringify!(#name), "` (via proc macro array)" ))?; - ret.#field_name = vec![crate::common::Location::default(); count]; + ret.#field_name = vec![Default::default(); count].into(); }, + }; - // Set an X value if given - xstr if crate::common::is_number_pattern(xstr, b'X') => { - let idx: usize = xstr.strip_prefix(b"X").unwrap() - .parse_as_utf8() - .or_context(|| format!( - "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) - ))?; + match_stmts.push(count_match); - let x = val.parse_as_utf8().or_context(|| format!( - "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) - ))?; + for (match_pfx, assign_value) in arr_map { + let match_pfx_bstr = Literal::byte_string(match_pfx.to_string().as_bytes()); - ret.#field_name[idx - 1].x = x; - }, + let item_match = quote! { + match_val if crate::common::is_number_pattern(match_val, #match_pfx_bstr) => { + let idx: usize = match_val.strip_prefix(#match_pfx_bstr).unwrap() + .parse_as_utf8() + .or_context(|| format!( + "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + String::from_utf8_lossy(match_val), #field_name_str, stringify!(#name) + ))?; - // Set a Y value if given - ystr if crate::common::is_number_pattern(ystr, b'Y') => { - let idx: usize = ystr.strip_prefix(b"Y").unwrap() - .parse_as_utf8() - .or_context(|| format!( + let parsed_val = val.parse_as_utf8().or_context(|| format!( "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) + String::from_utf8_lossy(match_val), #field_name_str, stringify!(#name) ))?; - let y = val.parse_as_utf8().or_context(|| format!( - "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) - ))?; - - ret.#field_name[idx - 1].y = y; - }, - }; + ret.#field_name[idx - 1].#assign_value = parsed_val; + }, + }; + match_stmts.push(item_match); + } - match_stmts.push(count_match); + // // Set an X value if given + // xstr if crate::common::is_number_pattern(xstr, b'X') => { + // let idx: usize = xstr.strip_prefix(b"X").unwrap() + // .parse_as_utf8() + // .or_context(|| format!( + // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + // String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) + // ))?; + + // let x = val.parse_as_utf8().or_context(|| format!( + // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + // String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) + // ))?; + + // ret.#field_name[idx - 1].x = x; + // }, + + // // Set a Y value if given + // ystr if crate::common::is_number_pattern(ystr, b'Y') => { + // let idx: usize = ystr.strip_prefix(b"Y").unwrap() + // .parse_as_utf8() + // .or_context(|| format!( + // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + // String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) + // ))?; + + // let y = val.parse_as_utf8().or_context(|| format!( + // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", + // String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) + // ))?; + + // ret.#field_name[idx - 1].y = y; + // }, + // }; } /// From a field in our struct, create the name we should match by diff --git a/altium/Cargo.toml b/altium/Cargo.toml index 0468e82..ba61c43 100644 --- a/altium/Cargo.toml +++ b/altium/Cargo.toml @@ -9,7 +9,9 @@ description = "A library for processing Altium file types" [dependencies] altium-macros = { path = "../altium-macros", version = "0.1.0" } base64 = "0.21.2" -cfb = "0.8.1" +# Use custom rev so we get debug outputs +cfb = { git = "https://github.com/mdsteele/rust-cfb.git", rev = "5c5279d6" } +# cfb = "0.8.1" flate2 = "1.0.26" # image = "0.24.6" image = { version = "0.24.6", default-features = false, features = ["png", "bmp", "jpeg"] } @@ -25,6 +27,9 @@ svg = "0.13.1" uuid = { version = "1.4.1", features = ["v1", "v4", "fast-rng"]} xml-rs = "0.8.16" +[dev-dependencies] +env_logger = "0.10.0" + [package.metadata.release] shared-version = true diff --git a/altium/src/common.rs b/altium/src/common.rs index 211043d..6f51ba4 100644 --- a/altium/src/common.rs +++ b/altium/src/common.rs @@ -39,6 +39,15 @@ impl Location { } } +impl From<(i32, i32)> for Location { + fn from(value: (i32, i32)) -> Self { + Self { + x: value.0, + y: value.1, + } + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq)] pub enum Visibility { Hidden, @@ -46,6 +55,12 @@ pub enum Visibility { Visible, } +impl FromUtf8<'_> for Visibility { + fn from_utf8(buf: &[u8]) -> Result { + todo!("{}", String::from_utf8_lossy(buf)) + } +} + /// A unique ID /// // TODO: figure out what file types use this exact format @@ -202,6 +217,12 @@ impl Rotation { } } +impl FromUtf8<'_> for Rotation { + fn from_utf8(buf: &[u8]) -> Result { + todo!("{}", String::from_utf8_lossy(buf)) + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq)] pub enum ReadOnlyState { #[default] @@ -216,7 +237,7 @@ impl TryFrom for ReadOnlyState { let res = match value { x if x == Self::ReadWrite as u8 => Self::ReadWrite, x if x == Self::ReadOnly as u8 => Self::ReadOnly, - _ => return Err(ErrorKind::SheetStyle(value)), + _ => return Err(ErrorKind::ReadOnlyState(value)), }; Ok(res) @@ -249,9 +270,9 @@ pub enum PosVert { } /// Verify a number pattern matches, e.g. `X100` -pub fn is_number_pattern(s: &[u8], prefix: u8) -> bool { +pub fn is_number_pattern(s: &[u8], prefix: &[u8]) -> bool { if let Some(stripped) = s - .strip_prefix(&[prefix]) + .strip_prefix(prefix) .map(|s| s.strip_prefix(&[b'-']).unwrap_or(s)) { if stripped.iter().all(u8::is_ascii_digit) { diff --git a/altium/src/error.rs b/altium/src/error.rs index 0032022..792391f 100644 --- a/altium/src/error.rs +++ b/altium/src/error.rs @@ -1,5 +1,6 @@ //! Error types used throughout this crate +use std::borrow::Cow; use std::cmp::min; use std::fmt; use std::fmt::Write; @@ -71,39 +72,47 @@ impl fmt::Debug for Error { #[derive(Debug)] #[non_exhaustive] pub enum ErrorKind { - Io(io::Error), + BufferTooShort(usize, TruncBuf), + ElectricalType(u8), + ExpectedBool(String), + ExpectedColor(TruncBuf), + ExpectedFloat(String, ParseFloatError), + ExpectedInt(String, ParseIntError), + ExpectedNul(TruncBuf), + FileType(String, &'static str), + Image(image::ImageError), IniFormat(Box), - MissingSection(String), - MissingUniqueId(String), - InvalidUniqueId(TruncBuf), + InvalidHeader(Box, &'static str), + InvalidKey(Box), InvalidStorageData(TruncBuf), - FileType(String, &'static str), InvalidStream(Box, usize), - RequiredSplit(String), - Utf8(Utf8Error, String), - ExpectedInt(String, ParseIntError), - ExpectedFloat(String, ParseFloatError), - InvalidKey(Box), - InvalidHeader(Box), - ExpectedBool(String), - ExpectedColor(TruncBuf), - SheetStyle(u8), - ReadOnlyState(u8), + InvalidUniqueId(TruncBuf), + Io(io::Error), Justification(u8), + MissingSection(String), + MissingUniqueId, Pin(PinError), - BufferTooShort(usize, TruncBuf), - Image(image::ImageError), + ReadOnlyState(u8), + RequiredSplit(String), + SheetStyle(u8), + Utf8(Utf8Error, String), } impl fmt::Display for ErrorKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ErrorKind::IniFormat(e) => write!(f, "error parsing ini: {e}"), + ErrorKind::ElectricalType(e) => write!(f, "invalid electrical type {e}"), ErrorKind::Io(e) => write!(f, "io error: {e}"), ErrorKind::MissingSection(e) => write!(f, "missing required section `{e}`"), - ErrorKind::MissingUniqueId(e) => write!(f, "bad or missing unique ID section `{e}`"), + ErrorKind::MissingUniqueId => write!(f, "bad or missing unique ID section"), ErrorKind::InvalidUniqueId(e) => { - write!(f, "invalid unique ID section `{e}` (len {})", e.orig_len) + write!( + f, + "invalid unique ID section `{e}` (len {}, `{}`)", + e.orig_len, + e.as_str() + ) } ErrorKind::FileType(n, ty) => write!(f, "file `{n}` is not a valid {ty} file"), ErrorKind::InvalidStream(s, n) => { @@ -116,7 +125,7 @@ impl fmt::Display for ErrorKind { write!(f, "invalid storage data near `{e:x}`") } ErrorKind::Utf8(e, s) => write!(f, "utf8 error: {e} at '{s}'"), - ErrorKind::InvalidHeader(e) => write!(f, "invalid header '{e}'"), + ErrorKind::InvalidHeader(e, v) => write!(f, "invalid header '{e}'; expected `{v}`"), ErrorKind::ExpectedInt(s, e) => write!(f, "error parsing integer from `{s}`: {e}"), ErrorKind::ExpectedFloat(s, e) => write!(f, "error parsing float from `{s}`: {e}"), ErrorKind::InvalidKey(s) => write!(f, "invalid key found: `{s}`"), @@ -132,6 +141,7 @@ impl fmt::Display for ErrorKind { b.len() ), ErrorKind::Image(e) => write!(f, "image error: {e}"), + ErrorKind::ExpectedNul(e) => write!(f, "expected nul near {e}"), } } } @@ -147,8 +157,8 @@ impl ErrorKind { Self::InvalidKey(String::from_utf8_lossy(key).into()) } - pub(crate) fn new_invalid_header(header: &[u8]) -> Self { - Self::InvalidHeader(String::from_utf8_lossy(header).into()) + pub(crate) fn new_invalid_header(header: &[u8], expected: &'static str) -> Self { + Self::InvalidHeader(String::from_utf8_lossy(header).into(), expected) } } @@ -330,6 +340,12 @@ impl TruncBuf { } } +impl TruncBuf { + pub(crate) fn as_str(&self) -> Cow { + String::from_utf8_lossy(&self.buf) + } +} + impl From<&[T]> for TruncBuf { fn from(value: &[T]) -> Self { Self::new(value) diff --git a/altium/src/font.rs b/altium/src/font.rs index 08cbbd5..05d4b45 100644 --- a/altium/src/font.rs +++ b/altium/src/font.rs @@ -1,6 +1,6 @@ //! Objects related to font as Altium sees it. -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; lazy_static::lazy_static! { pub static ref DEFAULT_FONT: Font = Font { @@ -10,7 +10,7 @@ lazy_static::lazy_static! { } /// A font that is stored in a library -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Font { pub(crate) name: Box, pub(crate) size: u16, @@ -41,7 +41,7 @@ impl Default for &Font { // // Or `Arc>>>`. Yucky, but editable (edit the // font if you're the only user duplicate it if you're not) -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct FontCollection(Vec); impl FontCollection { @@ -62,3 +62,17 @@ impl From> for FontCollection { Self(value) } } + +impl Deref for FontCollection { + type Target = [Font]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FontCollection { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/altium/src/parse/bin.rs b/altium/src/parse/bin.rs index 2c29244..4220349 100644 --- a/altium/src/parse/bin.rs +++ b/altium/src/parse/bin.rs @@ -7,26 +7,40 @@ use crate::{ ErrorKind, }; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum BufLenMatch { /// Length is a 3-byte value in a 4-byte integer, and the upper value must /// be equal to 0x01. This is used to indicate binary data U24UpperOne, + /// 3-byte value in a 4-byte integer, the upper value must be equal to 0x00 + U24UpperZero, /// Length is a 4-byte value U32, /// Length is a single byte value U8, } -/// Extract a buffer that starts with a 3-byte header -pub fn extract_sized_buf(buf: &[u8], len_match: BufLenMatch) -> Result<(&[u8], &[u8]), ErrorKind> { +/// Extract a buffer that starts with a 1-, 3-, or 4-byte header. +/// +/// - `len_match`: Configure how the leading bytes define the length +/// - `expect_nul`: Configure whether or not there should be a nul terminator +pub fn extract_sized_buf( + buf: &[u8], + len_match: BufLenMatch, + expect_nul: bool, +) -> Result<(&[u8], &[u8]), ErrorKind> { let (data_len, rest): (usize, _) = match len_match { - BufLenMatch::U24UpperOne => { + BufLenMatch::U24UpperOne | BufLenMatch::U24UpperZero => { let [l0, l1, l2, l3, rest @ ..] = buf else { return Err(ErrorKind::BufferTooShort(4, TruncBuf::new(buf))); }; - assert_eq!(*l3, 0x01, "expected 0x01 in uppper bit but got {l3}"); + if len_match == BufLenMatch::U24UpperOne { + assert_eq!(*l3, 0x01, "expected 0x01 in uppper bit but got {l3}"); + } else if len_match == BufLenMatch::U24UpperZero { + assert_eq!(*l3, 0x00, "expected 0x00 in uppper bit but got {l3}"); + } + let len = u32::from_le_bytes([*l0, *l1, *l2, 0x00]) .try_into() .unwrap(); @@ -52,15 +66,25 @@ pub fn extract_sized_buf(buf: &[u8], len_match: BufLenMatch) -> Result<(&[u8], & let data = rest .get(..data_len) .ok_or(ErrorKind::BufferTooShort(data_len, rest.into()))?; - Ok((data, &rest[data_len..])) + let rest = &rest[data_len..]; + + if expect_nul { + let Some(0) = data.last() else { + return Err(ErrorKind::ExpectedNul(TruncBuf::new_end(data))); + }; + Ok((&data[..data.len() - 1], rest)) + } else { + Ok((data, rest)) + } } -/// Extract a buffer that starts with a 3-byte header to a string +/// Extract a buffer that starts with a 1-, 3- or 4-byte header to a string pub fn extract_sized_utf8_buf( buf: &[u8], len_match: BufLenMatch, + expect_nul: bool, ) -> Result<(&str, &[u8]), ErrorKind> { - let (str_buf, rest) = extract_sized_buf(buf, len_match)?; + let (str_buf, rest) = extract_sized_buf(buf, len_match, expect_nul)?; let text = str_from_utf8(str_buf)?; Ok((text, rest)) } diff --git a/altium/src/parse/utf8.rs b/altium/src/parse/utf8.rs index 64ad1e2..95c3f6c 100644 --- a/altium/src/parse/utf8.rs +++ b/altium/src/parse/utf8.rs @@ -14,7 +14,7 @@ use crate::{ /// Extension trait for `&[u8]` that will parse a string as utf8/ASCII for /// anything implementing `FromUtf8` pub trait ParseUtf8<'a> { - /// Parse this as utf8 to whatever the target type is + /// Parse this as a utf8 string to whatever the target type is fn parse_as_utf8>(self) -> Result; } diff --git a/altium/src/sch.rs b/altium/src/sch.rs index b0fd007..4dfbf89 100644 --- a/altium/src/sch.rs +++ b/altium/src/sch.rs @@ -15,5 +15,5 @@ pub use component::Component; pub use params::{Justification, SheetStyle}; pub use pin::PinError; pub(crate) use record::{SchDrawCtx, SchRecord}; -pub use schdoc::SchDoc; +pub use schdoc::{SchDoc, SchDocRecords}; pub use schlib::{ComponentMeta, ComponentsIter, SchLib}; diff --git a/altium/src/sch/component.rs b/altium/src/sch/component.rs index 7a25199..5751913 100644 --- a/altium/src/sch/component.rs +++ b/altium/src/sch/component.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use svg::node::element::SVG as Svg; +use super::record::parse_all_records; use super::storage::Storage; use super::{SchDrawCtx, SchRecord}; use crate::draw::{Canvas, Draw, SvgCtx}; @@ -99,51 +100,3 @@ impl Component { self.name.partial_cmp(&other.name) } } - -/// Given a buffer for a component, split the records up -/// -/// Name is only used for diagnostics -fn parse_all_records(buf: &[u8], name: &str) -> Result, Error> { - // Our info u32 is something like `0xttllllll`, where `tt` are 8 bits - // representing a type (currently only values 0 and 1 known) and the `l`s - // are the length - const TY_SHIFT: u32 = 24; - const TY_MAEK: u32 = 0xff000000; - const LEN_MASK: u32 = 0x00ffffff; - const UTF8_RECORD_TY: u32 = 0x00; - const PIN_RECORD_TY: u32 = 0x01; - // No magic values :) - const U32_BYTES: usize = 4; - - let mut working = buf; - let mut parsed = Vec::new(); - while !working.is_empty() { - assert!( - working.len() >= 4, - "expected at least 4 bytes, only got {}", - working.len() - ); - - let info = u32::from_le_bytes(working[..4].try_into().unwrap()); - let ty = (info & TY_MAEK) >> TY_SHIFT; - let len: usize = (info & LEN_MASK).try_into().unwrap(); - - // Don't include the null terminator (which is included in `len`) - let to_parse = &working[U32_BYTES..(U32_BYTES + len - 1)]; - - // But do do a sanity check that the null exists - assert_eq!(working[U32_BYTES + len - 1], 0, "Expected null terimation"); - - working = &working[U32_BYTES + len..]; - - let record = match ty { - UTF8_RECORD_TY => parse_any_record(to_parse), - PIN_RECORD_TY => SchPin::parse(to_parse).map_err(Into::into), - _ => panic!("unexpected record type {ty:02x}"), - }; - - parsed.push(record.context(format!("in `parse_all_records` for `{name}`"))?); - } - - Ok(parsed) -} diff --git a/altium/src/sch/pin.rs b/altium/src/sch/pin.rs index 6da3ab5..eaf776a 100644 --- a/altium/src/sch/pin.rs +++ b/altium/src/sch/pin.rs @@ -3,24 +3,44 @@ use core::fmt; use std::str::{self, Utf8Error}; +use altium_macros::FromRecord; use log::warn; +use num_enum::TryFromPrimitive; use super::SchRecord; use crate::common::{Location, Rotation, Visibility}; +use crate::error::AddContext; +use crate::parse::ParseUtf8; +use crate::parse::{FromRecord, FromUtf8}; +use crate::{ErrorKind, UniqueId}; -#[derive(Clone, Debug, Default, PartialEq)] +/// Representation of a pin +/// +/// Altium stores pins as binary in the schematic libraries but text in the +/// schematic documents, so we need to parse both. +#[derive(Clone, Debug, Default, PartialEq, FromRecord)] +#[from_record(id = 2, record_variant = Pin)] pub struct SchPin { + pub(super) formal_type: u8, pub(super) owner_index: u8, pub(super) owner_part_id: u8, pub(super) description: Box, + // #[from_record(rename = b"PinDesignator")] pub(super) designator: Box, pub(super) name: Box, pub(super) location_x: i32, pub(super) location_y: i32, + pub(super) electrical: ElectricalType, + #[from_record(rename = b"PinLength")] pub(super) length: u32, + #[from_record(rename = b"SwapIDPart")] + pub(super) swap_id_part: Box, pub(super) designator_vis: Visibility, pub(super) name_vis: Visibility, pub(super) rotation: Rotation, + #[from_record(rename = b"PinPropagationDelay")] + pub(super) propegation_delay: f32, + pub(super) unique_id: UniqueId, } impl SchPin { @@ -37,13 +57,13 @@ impl SchPin { let (description, rest) = sized_buf_to_utf8(rest, "description")?; // TODO: ty_info - let [formal_ty, ty_info, rot_hide, l0, l1, x0, x1, y0, y1, rest @ ..] = rest else { + let [formal_type, ty_info, rot_hide, l0, l1, x0, x1, y0, y1, rest @ ..] = rest else { return Err(PinError::TooShort(rest.len(), "position extraction")); }; assert_eq!( - *formal_ty, 1, - "expected formal type of 1 but got {formal_ty}" + *formal_type, 1, + "expected formal type of 1 but got {formal_type}" ); let (rotation, des_vis, name_vis) = get_rotation_and_hiding(*rot_hide); let length = u16::from_le_bytes([*l0, *l1]); @@ -62,6 +82,7 @@ impl SchPin { } let retval = Self { + formal_type: *formal_type, owner_index: 0, owner_part_id: 0, description: description.into(), @@ -76,6 +97,7 @@ impl SchPin { designator_vis: des_vis, name_vis, rotation, + ..Default::default() }; Ok(SchRecord::Pin(retval)) @@ -153,8 +175,43 @@ fn get_rotation_and_hiding(val: u8) -> (Rotation, Visibility, Visibility) { (rotation, des_vis, name_vis) } -fn _print_buf(buf: &[u8], s: &str) { - println!("pin buf at {s}: {buf:02x?}"); +#[repr(u8)] +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] +pub enum ElectricalType { + #[default] + Input = 0, + Id = 1, + Output = 2, + OpenCollector = 3, + Passive = 4, + HighZ = 5, + OpenEmitter = 6, + Power = 7, +} + +impl FromUtf8<'_> for ElectricalType { + fn from_utf8(buf: &[u8]) -> Result { + let num: u8 = buf.parse_as_utf8()?; + num.try_into() + } +} + +impl TryFrom for ElectricalType { + type Error = ErrorKind; + + fn try_from(value: u8) -> Result { + match value { + x if x == Self::Input as u8 => Ok(Self::Input), + x if x == Self::Id as u8 => Ok(Self::Id), + x if x == Self::Output as u8 => Ok(Self::Output), + x if x == Self::OpenCollector as u8 => Ok(Self::OpenCollector), + x if x == Self::Passive as u8 => Ok(Self::Passive), + x if x == Self::HighZ as u8 => Ok(Self::HighZ), + x if x == Self::OpenEmitter as u8 => Ok(Self::OpenEmitter), + x if x == Self::Power as u8 => Ok(Self::Power), + _ => Err(ErrorKind::ElectricalType(value)), + } + } } /// Errors related specifically to pin parsing diff --git a/altium/src/sch/record.rs b/altium/src/sch/record.rs index 66ba438..fcb1bb3 100644 --- a/altium/src/sch/record.rs +++ b/altium/src/sch/record.rs @@ -51,17 +51,19 @@ //! We provide a derive macro for `FromRecord`, so most types in this module //! don't need to do anything special. mod draw; +mod parse; use std::str; use altium_macros::FromRecord; pub use draw::SchDrawCtx; -use num_enum::TryFromPrimitive; +pub(super) use parse::parse_all_records; use super::params::Justification; use super::pin::SchPin; use crate::common::{Location, ReadOnlyState, UniqueId}; -use crate::error::AddContext; +use crate::error::{AddContext, TruncBuf}; +use crate::font::{Font, FontCollection}; use crate::Error; use crate::{ common::Color, @@ -107,11 +109,17 @@ pub enum SchRecord { Template(Template), Parameter(Parameter), ImplementationList(ImplementationList), + Implementation(Implementation), + ImplementationChild1(ImplementationChild1), + ImplementationChild2(ImplementationChild2), } -/// Try all known record types (excludes pins) +/// Try all known record types (excludes binary pins) pub fn parse_any_record(buf: &[u8]) -> Result { - let buf = buf.strip_prefix(b"|RECORD=").unwrap(); + let buf = buf.strip_prefix(b"|RECORD=").unwrap_or_else(|| { + let tb = TruncBuf::new(buf); + panic!("no record prefix in {tb} ({})", tb.as_str()); + }); let num_chars = buf.iter().take_while(|ch| ch.is_ascii_digit()).count(); let record_id_str = str::from_utf8(&buf[..num_chars]).unwrap(); let record_id: u32 = record_id_str @@ -123,6 +131,7 @@ pub fn parse_any_record(buf: &[u8]) -> Result { // Try parsing all our types, they will just skip to the next one if the // record ID doesn't match MetaData::parse_if_matches(record_id, to_parse) + .or_else(|| SchPin::parse_if_matches(record_id, to_parse)) .or_else(|| IeeeSymbol::parse_if_matches(record_id, to_parse)) .or_else(|| Label::parse_if_matches(record_id, to_parse)) .or_else(|| Bezier::parse_if_matches(record_id, to_parse)) @@ -154,25 +163,15 @@ pub fn parse_any_record(buf: &[u8]) -> Result { .or_else(|| Template::parse_if_matches(record_id, to_parse)) .or_else(|| Parameter::parse_if_matches(record_id, to_parse)) .or_else(|| ImplementationList::parse_if_matches(record_id, to_parse)) + .or_else(|| Implementation::parse_if_matches(record_id, to_parse)) + .or_else(|| ImplementationChild1::parse_if_matches(record_id, to_parse)) + .or_else(|| ImplementationChild2::parse_if_matches(record_id, to_parse)) .unwrap_or_else(|| { - eprintln!("unknown record id {record_id}"); + log::error!("unknown record id {record_id}"); Ok(SchRecord::Undefined) }) } -#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] -#[repr(u8)] -enum PinType { - Input = 0, - Id = 1, - Output = 2, - OpenCollector = 3, - Passive = 4, - HighZ = 5, - OpenEmitter = 6, - Power = 7, -} - /// Component metadata (AKA "Component") #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 1, use_box = true)] @@ -195,10 +194,13 @@ pub struct MetaData { part_count: u8, part_id_locked: bool, not_use_db_table_name: bool, + orientation: i32, sheet_part_file_name: Box, design_item_id: Box, source_library_name: Box, target_file_name: Box, + location_x: i32, + location_y: i32, unique_id: UniqueId, } @@ -236,7 +238,7 @@ pub struct Bezier { index_in_sheet: i16, is_not_accessible: bool, line_width: u16, - #[from_record(array = true, count = b"LocationCount")] + #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] locations: Vec, owner_index: u8, owner_part_id: i8, @@ -253,7 +255,7 @@ pub struct PolyLine { index_in_sheet: i16, line_width: u16, color: Color, - #[from_record(array = true, count = b"LocationCount")] + #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] locations: Vec, unique_id: UniqueId, } @@ -267,7 +269,7 @@ pub struct Polygon { is_not_accessible: bool, is_solid: bool, line_width: u16, - #[from_record(array = true, count = b"LocationCount")] + #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] locations: Vec, owner_index: u8, owner_part_id: i8, @@ -411,6 +413,21 @@ pub struct Rectangle { pub struct SheetSymbol { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + line_width: u16, + color: Color, + area_color: Color, + is_solid: bool, + symbol_type: Box, + show_net_name: bool, + location_y: i32, + x_size: i32, + y_size: i32, + location_x: i32, + orientation: i32, + font_id: u16, + text: Box, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -418,6 +435,19 @@ pub struct SheetSymbol { pub struct SheetEntry { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + text_color: Color, + area_color: Color, + text_font_id: u16, + text_style: Box, + name: Box, + unique_id: UniqueId, + arrow_kind: Box, + distance_from_top: i32, + color: Color, + #[from_record(rename = b"IOType")] + io_type: i32, + side: i32, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -425,13 +455,39 @@ pub struct SheetEntry { pub struct PowerPort { owner_index: u8, owner_part_id: i8, + is_cross_sheet_connector: bool, + index_in_sheet: i16, + style: i16, + show_net_name: bool, + location_y: i32, + location_x: i32, + orientation: i32, + font_id: u16, + text: Box, + unique_id: UniqueId, + color: Color, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 18)] pub struct Port { + alignment: u16, + area_color: Color, + border_width: i32, + color: Color, + font_id: u16, + height: i32, + width: i32, + index_in_sheet: i16, + #[from_record(rename = b"IOType")] + io_type: u16, + location_x: i32, + location_y: i32, + name: Box, owner_index: u8, owner_part_id: i8, + text_color: Color, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -439,6 +495,15 @@ pub struct Port { pub struct NoErc { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + orientation: i16, + symbol: Box, + is_active: bool, + suppress_all: bool, + location_x: i32, + location_y: i32, + color: Color, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -446,6 +511,13 @@ pub struct NoErc { pub struct NetLabel { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + location_x: i32, + location_y: i32, + color: Color, + font_id: u16, + text: Box, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -453,6 +525,12 @@ pub struct NetLabel { pub struct Bus { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + line_width: u16, + color: Color, + #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] + locations: Vec, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -460,13 +538,31 @@ pub struct Bus { pub struct Wire { owner_index: u8, owner_part_id: i8, + line_width: u16, + color: Color, + #[from_record(array = true, count = b"LocationCount", map = (X -> x, Y -> y))] + locations: Vec, + index_in_sheet: i16, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 28)] pub struct TextFrame { + location_x: i32, + location_y: i32, + corner_x: i32, + corner_y: i32, + area_color: Color, owner_index: u8, owner_part_id: i8, + font_id: u16, + alignment: u16, + word_wrap: bool, + text: Box, + index_in_sheet: i16, + clip_to_rect: bool, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -500,6 +596,32 @@ pub struct Image { pub struct Sheet { owner_index: u8, owner_part_id: i8, + snap_grid_size: i32, + snap_grid_on: bool, + visible_grid_on: bool, + visible_grid_size: i32, + custom_x: i32, + custom_y: i32, + custom_x_zones: u16, + custom_y_zones: u16, + custom_margin_width: u16, + hot_spot_grid_on: bool, + hot_spot_grid_size: i32, + system_font: u16, + #[from_record(array = true, count = b"FontIdCount", map = (FontName -> name, Size -> size))] + pub(super) fonts: FontCollection, + border_on: bool, + sheet_number_space_size: i32, + area_color: Color, + // FIXME: make this an enum + #[from_record(rename = b"Display_Unit")] + display_unit: u16, + #[from_record(rename = b"UseMBCS")] + use_mbcs: bool, + #[from_record(rename = b"IsBOC")] + is_boc: bool, + // FIXME: seems to be base64 + file_version_info: Box, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -507,6 +629,13 @@ pub struct Sheet { pub struct SheetName { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + location_x: i32, + location_y: i32, + color: Color, + font_id: u16, + text: Box, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -514,6 +643,13 @@ pub struct SheetName { pub struct FileName { owner_index: u8, owner_part_id: i8, + index_in_sheet: i16, + location_x: i32, + location_y: i32, + color: Color, + font_id: u16, + text: Box, + unique_id: UniqueId, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -545,6 +681,8 @@ pub struct BusEntry { pub struct Template { owner_index: u8, owner_part_id: i8, + is_not_accessible: bool, + file_name: Box, } #[derive(Clone, Debug, Default, PartialEq, FromRecord)] @@ -570,3 +708,36 @@ pub struct ImplementationList { owner_index: u8, owner_part_id: i8, } + +/// Things like models, including footprints +#[derive(Clone, Debug, Default, PartialEq, FromRecord)] +#[from_record(id = 45)] +pub struct Implementation { + owner_index: u8, + owner_part_id: i8, + use_component_library: bool, + model_name: Box, + model_type: Box, + datafile_count: u16, + model_datafile_entity0: Box, + model_datafile_kind0: Box, + is_current: bool, + datalinks_locked: bool, + database_datalinks_locked: bool, + unique_id: UniqueId, + index_in_sheet: i16, +} + +#[derive(Clone, Debug, Default, PartialEq, FromRecord)] +#[from_record(id = 46)] +pub struct ImplementationChild1 { + owner_index: u8, + owner_part_id: i8, +} + +#[derive(Clone, Debug, Default, PartialEq, FromRecord)] +#[from_record(id = 48)] +pub struct ImplementationChild2 { + owner_index: u8, + owner_part_id: i8, +} diff --git a/altium/src/sch/record/draw.rs b/altium/src/sch/record/draw.rs index aadb3e9..0bde4ec 100644 --- a/altium/src/sch/record/draw.rs +++ b/altium/src/sch/record/draw.rs @@ -44,14 +44,14 @@ impl Draw for record::SchRecord { // record::SchRecord::Arc(v) => v.draw(canvas, ctx), record::SchRecord::Line(v) => v.draw(canvas, ctx), record::SchRecord::Rectangle(v) => v.draw(canvas, ctx), - // record::SchRecord::SheetSymbol(v) => v.draw(canvas, ctx), + record::SchRecord::SheetSymbol(v) => v.draw(canvas, ctx), // record::SchRecord::SheetEntry(v) => v.draw(canvas, ctx), // record::SchRecord::PowerPort(v) => v.draw(canvas, ctx), - // record::SchRecord::Port(v) => v.draw(canvas, ctx), + record::SchRecord::Port(v) => v.draw(canvas, ctx), // record::SchRecord::NoErc(v) => v.draw(canvas, ctx), - // record::SchRecord::NetLabel(v) => v.draw(canvas, ctx), - // record::SchRecord::Bus(v) => v.draw(canvas, ctx), - // record::SchRecord::Wire(v) => v.draw(canvas, ctx), + record::SchRecord::NetLabel(v) => v.draw(canvas, ctx), + record::SchRecord::Bus(v) => v.draw(canvas, ctx), + record::SchRecord::Wire(v) => v.draw(canvas, ctx), // record::SchRecord::TextFrame(v) => v.draw(canvas, ctx), // record::SchRecord::Junction(v) => v.draw(canvas, ctx), record::SchRecord::Image(v) => v.draw(canvas, ctx), @@ -267,14 +267,112 @@ impl Draw for record::Rectangle { } } -// impl Draw for record::SheetSymbol {} +impl Draw for record::SheetSymbol { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + canvas.draw_rectangle(DrawRectangle { + x: self.location_x, + y: self.location_y - self.y_size, + width: self.x_size, + height: self.y_size, + fill_color: self.area_color, + stroke_color: self.color, + stroke_width: self.line_width, + }); + } +} + // impl Draw for record::SheetEntry {} // impl Draw for record::PowerPort {} -// impl Draw for record::Port {} + +impl Draw for record::Port { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + // match self.io_type + let h2 = self.height / 2; + let mut locations = [Location::default(); 6]; + + locations[0] = Location::new(self.location_x, self.location_y + h2); + locations[1] = Location::new(self.location_x + self.width - h2, self.location_y + h2); + locations[2] = Location::new(self.location_x + self.width, self.location_y); + locations[3] = Location::new(self.location_x + self.width - h2, self.location_y - h2); + locations[4] = Location::new(self.location_x, self.location_y - h2); + locations[5] = Location::new(self.location_x, self.location_y + h2); + + canvas.draw_polygon(DrawPolygon { + locations: &locations, + fill_color: self.area_color, + stroke_color: self.color, + stroke_width: self.border_width.try_into().unwrap(), + }); + + let font = &ctx.fonts.get_idx(self.font_id.into()); + canvas.draw_text(DrawText { + x: self.location_x, + y: self.location_y, + text: &self.name, + color: self.text_color, + font, + ..Default::default() + }); + } +} + // impl Draw for record::NoErc {} -// impl Draw for record::NetLabel {} -// impl Draw for record::Bus {} -// impl Draw for record::Wire {} + +impl Draw for record::NetLabel { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + let font = &ctx.fonts.get_idx(self.font_id.into()); + + canvas.draw_text(DrawText { + x: self.location_x, + y: self.location_y, + text: &self.text, + color: self.color, + font, + ..Default::default() + }); + } +} + +impl Draw for record::Bus { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + for window in self.locations.windows(2) { + let &[a, b] = window else { unreachable!() }; + + canvas.draw_line(DrawLine { + start: a, + end: b, + color: self.color, + width: self.line_width * 4, + }); + } + } +} + +impl Draw for record::Wire { + type Context<'a> = SchDrawCtx<'a>; + + fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + for window in self.locations.windows(2) { + let &[a, b] = window else { unreachable!() }; + + canvas.draw_line(DrawLine { + start: a, + end: b, + color: self.color, + width: self.line_width * 4, + }); + } + } +} + // impl Draw for record::TextFrame {} // impl Draw for record::Junction {} impl Draw for record::Image { diff --git a/altium/src/sch/record/parse.rs b/altium/src/sch/record/parse.rs new file mode 100644 index 0000000..77568f3 --- /dev/null +++ b/altium/src/sch/record/parse.rs @@ -0,0 +1,54 @@ +use super::SchRecord; +use crate::error::AddContext; +use crate::{ + sch::{pin::SchPin, record::parse_any_record}, + Error, +}; + +/// Given a buffer for a component, split the records up +/// +/// Name is only used for diagnostics +pub fn parse_all_records(buf: &[u8], err_name: &str) -> Result, Error> { + // Our info u32 is something like `0xttllllll`, where `tt` are 8 bits + // representing a type (currently only values 0 and 1 known) and the `l`s + // are the length + const TY_SHIFT: u32 = 24; + const TY_MAEK: u32 = 0xff000000; + const LEN_MASK: u32 = 0x00ffffff; + const UTF8_RECORD_TY: u32 = 0x00; + const PIN_RECORD_TY: u32 = 0x01; + // No magic values :) + const U32_BYTES: usize = 4; + + let mut working = buf; + let mut parsed = Vec::new(); + while !working.is_empty() { + assert!( + working.len() >= 4, + "expected at least 4 bytes, only got {}", + working.len() + ); + + let info = u32::from_le_bytes(working[..4].try_into().unwrap()); + let ty = (info & TY_MAEK) >> TY_SHIFT; + let len: usize = (info & LEN_MASK).try_into().unwrap(); + + // Don't include the null terminator (which is included in `len`) + let to_parse = &working[U32_BYTES..(U32_BYTES + len - 1)]; + + // But do do a sanity check that the null exists + assert_eq!(working[U32_BYTES + len - 1], 0, "Expected null terimation"); + + working = &working[U32_BYTES + len..]; + + let record = match ty { + UTF8_RECORD_TY => parse_any_record(to_parse), + PIN_RECORD_TY => SchPin::parse(to_parse).map_err(Into::into), + _ => panic!("unexpected record type {ty:02x}"), + }; + + parsed.push(record.or_context(|| format!("in `parse_all_records` for `{err_name}`"))?); + } + + Ok(parsed) +} diff --git a/altium/src/sch/schdoc.rs b/altium/src/sch/schdoc.rs index 658d32e..d458bf6 100644 --- a/altium/src/sch/schdoc.rs +++ b/altium/src/sch/schdoc.rs @@ -1,27 +1,147 @@ #![allow(unused)] +use core::fmt; +use std::cell::RefCell; use std::fs::File; -use std::io::{self, Read, Seek}; +use std::io::{self, Cursor, Read, Seek}; use std::path::Path; +use std::sync::Arc; use cfb::CompoundFile; -use crate::Error; +use super::record::{parse_all_records, Sheet}; +use super::storage::Storage; +use super::{SchDrawCtx, SchRecord}; +use crate::common::split_altium_map; +use crate::draw::Canvas; +use crate::draw::Draw; +use crate::error::AddContext; +use crate::parse::{extract_sized_buf, extract_sized_utf8_buf, BufLenMatch, ParseUtf8}; +use crate::{Error, ErrorKind, UniqueId}; /// Magic string found in the `FileHeader` stream -const HEADER: &str = "HEADER=Protel for Windows - Schematic Library Editor Binary File Version 5.0"; +const HEADER: &str = "Protel for Windows - Schematic Capture Binary File Version 5.0"; +/// Where most content is stored +const DATA_STREAM: &str = "FileHeader"; /// Representation of a schematic file pub struct SchDoc { - cfile: CompoundFile, + cfile: RefCell>, + sheet: Sheet, + records: Vec, + unique_id: UniqueId, + storage: Arc, } +/// Impls that are specific to a file impl SchDoc { + /// Open a file from disk pub fn open>(path: P) -> Result { - let cfile = cfb::open(path)?; + let cfile = cfb::open(&path)?; + Self::from_cfile(cfile) + .context("parsing SchLib") + .or_context(|| format!("with file {}", path.as_ref().display())) + } +} + +impl<'a> SchDoc> { + /// Open an in-memory file from a buffer + pub fn from_buffer(buf: &'a [u8]) -> Result { + let cfile = cfb::CompoundFile::open(Cursor::new(buf))?; + Self::from_cfile(cfile).context("parsing SchDoc from Cursor") + } +} + +impl SchDoc { + pub fn into_records(self) -> SchDocRecords { + SchDocRecords { + sheet: self.sheet, + records: self.records, + storage: self.storage, + } + } + + /// Create a `SchLib` representation from any `Read`able compound file. + fn from_cfile(mut cfile: CompoundFile) -> Result { + let mut tmp_buf: Vec = Vec::new(); // scratch memory + + let mut storage = Storage::parse_cfile(&mut cfile, &mut tmp_buf)?; + tmp_buf.clear(); - Ok(Self { cfile }) + { + let mut stream = cfile.open_stream(DATA_STREAM).map_err(|e| { + Error::from(e).context(format!("reading required stream `{DATA_STREAM}`")) + })?; + stream.read_to_end(&mut tmp_buf).unwrap(); + } + + let (rest, unique_id) = parse_header(&tmp_buf)?; + let mut records = parse_all_records(rest, "SchDoc::from_cfile")?; + let sheet_pos = records + .iter() + .position(|x| matches!(x, SchRecord::Sheet(_))); + let sheet = sheet_pos + .map(|idx| { + let SchRecord::Sheet(sheet) = records.remove(idx) else { + unreachable!() + }; + sheet + }) + .unwrap_or_default(); + + Ok(Self { + cfile: RefCell::new(cfile), + records, + sheet, + storage: storage.into(), + unique_id, + }) } } -impl SchDoc {} +impl fmt::Debug for SchDoc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SchDoc") + .field("unique_id", &self.unique_id) + .finish_non_exhaustive() + } +} + +/// Holdover until we figure out how we want to expose this +#[derive(Debug, Default)] +pub struct SchDocRecords { + sheet: Sheet, + records: Vec, + storage: Arc, +} + +impl SchDocRecords { + pub fn draw(&self, canvas: &mut C) { + let ctx = SchDrawCtx { + storage: &self.storage, + fonts: &self.sheet.fonts, + }; + self.records.iter().for_each(|r| r.draw(canvas, &ctx)); + } +} + +/// Extract the header, return the residual and the document unique ID +fn parse_header(buf: &[u8]) -> Result<(&[u8], UniqueId), Error> { + let mut uid = None; + let (hdr, rest) = extract_sized_buf(buf, BufLenMatch::U32, true)?; + for (key, val) in split_altium_map(hdr) { + match key { + b"HEADER" => { + if val != HEADER.as_bytes() { + return Err(ErrorKind::new_invalid_header(val, HEADER).into()); + } + } + b"UniqueID" => uid = Some(val.parse_as_utf8()?), + _ => (), + } + } + + let uid = uid.ok_or(ErrorKind::MissingUniqueId)?; + + Ok((rest, uid)) +} diff --git a/altium/src/sch/schlib.rs b/altium/src/sch/schlib.rs index 5cc201f..2daaee6 100644 --- a/altium/src/sch/schlib.rs +++ b/altium/src/sch/schlib.rs @@ -27,6 +27,7 @@ pub struct SchLib { /// Information contained in the compound file header. We use this as a /// lookup to see what we can extract from the file. header: SchLibMeta, + /// Blob storage used by Altium storage: Arc, } @@ -35,7 +36,9 @@ impl SchLib { /// Open a file from disk pub fn open>(path: P) -> Result { let cfile = cfb::open(&path)?; - Self::from_cfile(cfile).or_context(|| format!("opening {}", path.as_ref().display())) + Self::from_cfile(cfile) + .context("parsing SchLib") + .or_context(|| format!("with file {}", path.as_ref().display())) } } @@ -43,7 +46,7 @@ impl<'a> SchLib> { /// Open an in-memory file from a buffer pub fn from_buffer(buf: &'a [u8]) -> Result { let cfile = cfb::CompoundFile::open(Cursor::new(buf))?; - Self::from_cfile(cfile) + Self::from_cfile(cfile).context("parsing SchLib from Cursor") } } @@ -93,12 +96,10 @@ impl SchLib { { // Scope of refcell borrow let mut cfile_ref = self.cfile.borrow_mut(); - let mut stream = cfile_ref.open_stream(&data_path).unwrap_or_else(|e| { - dbg!(&meta); - dbg!(&data_path); + let mut stream = cfile_ref.open_stream(&data_path).map_err(|e| { let path_disp = data_path.display(); - panic!("missing required stream `{path_disp}` with error {e}") - }); + Error::from(e).context(format!("reading required stream `{path_disp}`",)) + })?; stream.read_to_end(&mut buf).unwrap(); } @@ -224,7 +225,6 @@ impl SchLibMeta { /// Magic header found in all streams const HEADER: &'static [u8] = b"HEADER=Protel for Windows - Schematic Library Editor Binary File Version 5.0"; - const HEADER_KEY: &'static [u8] = b"HEADER"; // /// Every header starts with this // const PFX: &[u8] = &[0x7a, 0x04, 0x00, 0x00, b'|']; @@ -295,7 +295,7 @@ impl SchLibMeta { } match key { - Self::HEADER_KEY => continue, + b"HEADER" => continue, b"Weight" => ret.weight = val.parse_as_utf8()?, b"MinorVersion" => ret.minor_version = val.parse_as_utf8()?, b"UniqueID" => ret.unique_id = val.parse_as_utf8()?, @@ -340,8 +340,8 @@ impl SchLibMeta { let idx: usize = key[Self::COMP_PARTCOUNT_PFX.len()..].parse_as_utf8()?; ret.components[idx].part_count = val.parse_as_utf8()?; } - _ => eprintln!( - "unsupported file header key {}:{}", + _ => log::warn!( + "unsupported SchLib file header key {}:{}", buf2lstring(key), buf2lstring(val) ), diff --git a/altium/src/sch/storage.rs b/altium/src/sch/storage.rs index ffab15e..f83d631 100644 --- a/altium/src/sch/storage.rs +++ b/altium/src/sch/storage.rs @@ -89,19 +89,15 @@ impl Storage { pub(crate) fn parse(buf: &[u8]) -> Result { let (mut header, mut rest) = - extract_sized_buf(buf, BufLenMatch::U32).context("parsing storage")?; - - assert_eq!( - header.last(), - Some(&0), - "expected null termination at {:02x}", - TruncBuf::new_end(header) - ); + extract_sized_buf(buf, BufLenMatch::U32, true).context("parsing storage")?; + header = &header[..header.len().saturating_sub(1)]; let mut map_kv = split_altium_map(header); let Some((b"HEADER", b"Icon storage")) = map_kv.next() else { - return Err(ErrorKind::new_invalid_header(header).context("parsing storage")); + return Err( + ErrorKind::new_invalid_header(header, "Icon storage").context("parsing storage") + ); }; // Weight indicates how many items are in the storage @@ -129,8 +125,8 @@ impl Storage { rest = &rest[5..]; // Path comes first, then data - (path, rest) = extract_sized_utf8_buf(rest, BufLenMatch::U8)?; - (data, rest) = extract_sized_buf(rest, BufLenMatch::U32)?; + (path, rest) = extract_sized_utf8_buf(rest, BufLenMatch::U8, false)?; + (data, rest) = extract_sized_buf(rest, BufLenMatch::U32, false)?; map.insert( path.into(), diff --git a/altium/tests/include_test_util.rs b/altium/tests/include_test_util.rs new file mode 100644 index 0000000..d6ec1b8 --- /dev/null +++ b/altium/tests/include_test_util.rs @@ -0,0 +1,11 @@ +#[allow(unused)] +fn test_init_once() { + use std::sync::OnceLock; + static ONCE: OnceLock<()> = OnceLock::new(); + + ONCE.get_or_init(|| { + env_logger::init_from_env( + env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "debug"), + ) + }); +} diff --git a/altium/tests/test_schdoc.rs b/altium/tests/test_schdoc.rs new file mode 100644 index 0000000..608b62f --- /dev/null +++ b/altium/tests/test_schdoc.rs @@ -0,0 +1,13 @@ +include!("include_test_util.rs"); + +use altium::sch::SchDoc; + +const SCHDOC_SIMPLE: &str = "tests/samples/schdoc/simple.SchDoc"; + +#[test] +fn test_parse() { + test_init_once(); + // Just test error free parsing + let schdoc = SchDoc::open(SCHDOC_SIMPLE).unwrap(); + println!("{schdoc:#?}"); +} diff --git a/altium/tests/test_schlib.rs b/altium/tests/test_schlib.rs index 998b8a6..cfdbb3a 100644 --- a/altium/tests/test_schlib.rs +++ b/altium/tests/test_schlib.rs @@ -1,3 +1,5 @@ +include!("include_test_util.rs"); + use std::cmp::min; use std::io::prelude::*; use std::{ @@ -30,6 +32,8 @@ const SIMPLE_COMP_NAME2: &str = "PinProperties"; #[test] fn test_parse() { + test_init_once(); + let wd = std::env::current_dir().unwrap(); println!("working directory: {}", wd.display()); @@ -42,6 +46,8 @@ fn test_parse() { #[test] fn test_record() { + test_init_once(); + let schlib = SchLib::open(SCHLIB_SIMPLE).unwrap(); let comp = schlib.get_component(SIMPLE_COMP_NAME1).unwrap(); println!("comp {SIMPLE_COMP_NAME1}:\n{comp:#?}"); @@ -51,6 +57,8 @@ fn test_record() { #[test] fn test_draw_single_svg() { + test_init_once(); + // Only draw my favorite symbol let schlib = SchLib::open(SCHLIB_SIMPLE).unwrap(); let mut out_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); @@ -73,6 +81,8 @@ fn test_draw_single_svg() { #[test] fn test_draw_all_svgs() { + test_init_once(); + for schlib_path in ALL_SCHLIBS { let schlib = SchLib::open(schlib_path).unwrap(); @@ -97,6 +107,8 @@ fn test_draw_all_svgs() { #[test] fn test_storage() { + test_init_once(); + let schlib = SchLib::open(SCHLIB_SIMPLE).unwrap(); let mut out_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); out_dir.extend(["test_output", "storage"]); From cfc49cd40ded80fb8303b8705cb6bdcce3d70791 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 18 Sep 2023 22:16:49 -0400 Subject: [PATCH 05/15] Updates needed to pass CI --- .github/workflows/validation-rust.yaml | 10 +- Cargo.lock | 293 +++++++++++++++++++++---- altium-macros/Cargo.toml | 8 +- altium/Cargo.toml | 14 +- altium/src/__private.rs | 2 +- altium/src/common.rs | 5 +- altium/src/logging.rs | 8 +- altium/src/parse/utf8.rs | 4 +- altium/src/sch/schlib.rs | 6 +- altium/src/sch/storage.rs | 5 +- 10 files changed, 283 insertions(+), 72 deletions(-) diff --git a/.github/workflows/validation-rust.yaml b/.github/workflows/validation-rust.yaml index 1eeeebf..a4ed748 100644 --- a/.github/workflows/validation-rust.yaml +++ b/.github/workflows/validation-rust.yaml @@ -46,10 +46,10 @@ jobs: os: ubuntu-latest target: x86_64-unknown-linux-musl extension: '' - # - build: macos - # os: macos-latest - # target: x86_64-apple-darwin - # extension: '' + - build: macos + os: macos-latest + target: x86_64-apple-darwin + extension: '' - build: windows-msvc os: windows-latest target: x86_64-pc-windows-msvc @@ -72,7 +72,7 @@ jobs: ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - uses: dtolnay/rust-toolchain@1.65.0 + - uses: dtolnay/rust-toolchain@1.70.0 - run: cargo test -- --nocapture fmt: diff --git a/Cargo.lock b/Cargo.lock index 3dbf19d..4cbe58f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" dependencies = [ "memchr", ] @@ -24,6 +24,7 @@ dependencies = [ "altium-macros", "base64", "cfb", + "env_logger", "flate2", "image", "lazy_static", @@ -64,9 +65,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "bitflags" @@ -74,6 +75,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "bytemuck" version = "1.13.1" @@ -86,11 +93,19 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + [[package]] name = "cfb" version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d9a1a046ad1c0410627f29da8dbdeee2c91fb046d0aed5381e5b558bcf3230" +source = "git+https://github.com/mdsteele/rust-cfb.git?rev=5c5279d6#5c5279d66f84a98314dd77a7f3a9c3c544070bee" dependencies = [ "byteorder", "fnv", @@ -164,12 +179,46 @@ dependencies = [ "const-random", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fdeflate" version = "0.3.0" @@ -181,9 +230,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", @@ -218,11 +267,23 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "image" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -243,6 +304,17 @@ dependencies = [ "hashbrown 0.14.0", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + [[package]] name = "jpeg-decoder" version = "0.3.0" @@ -261,6 +333,12 @@ version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + [[package]] name = "log" version = "0.4.20" @@ -269,9 +347,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "miniz_oxide" @@ -315,18 +393,18 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +checksum = "70bf6736f74634d299d00086f02986875b3c2d924781a6a2cb6c201e73da0ceb" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +checksum = "56ea360eafe1022f7cc56cd7b869ed57330fb2453d0c7831d99b74c65d2f5597" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -352,11 +430,11 @@ dependencies = [ [[package]] name = "png" -version = "0.17.9" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "fdeflate", "flate2", @@ -387,9 +465,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -405,9 +483,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -444,9 +522,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", @@ -456,9 +534,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", @@ -467,9 +545,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rust-ini" @@ -481,11 +559,27 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustix" +version = "0.38.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "serde" -version = "1.0.181" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d3e73c93c3240c0bda063c239298e633114c69a888c3e37ca8bb33f343e9890" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] [[package]] name = "serde-xml-rs" @@ -499,6 +593,17 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -513,15 +618,24 @@ checksum = "02d815ad337e8449d2374d4248448645edfe74e699343dd5719139d93fa87112" [[package]] name = "syn" -version = "2.0.28" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.44" @@ -559,9 +673,9 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", @@ -570,9 +684,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-segmentation" @@ -597,17 +711,114 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "winnow" -version = "0.5.1" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] [[package]] name = "xml-rs" -version = "0.8.16" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" +checksum = "bab77e97b50aee93da431f2cee7cd0f43b4d1da3c408042f2d7d164187774f0a" diff --git a/altium-macros/Cargo.toml b/altium-macros/Cargo.toml index c40a8f1..5d71a1e 100644 --- a/altium-macros/Cargo.toml +++ b/altium-macros/Cargo.toml @@ -11,10 +11,10 @@ proc-macro = true [dependencies] convert_case = "0.6.0" -proc-macro2 = "1.0.66" -quote = "1.0.32" -regex = "1.9.1" -syn = "2.0.28" +proc-macro2 = "1.0.67" +quote = "1.0.33" +regex = "1.9.5" +syn = "2.0.37" # syn = { version = "2.0.22", features = ["extra-traits"] } [package.metadata.release] diff --git a/altium/Cargo.toml b/altium/Cargo.toml index ba61c43..491343c 100644 --- a/altium/Cargo.toml +++ b/altium/Cargo.toml @@ -8,24 +8,24 @@ description = "A library for processing Altium file types" [dependencies] altium-macros = { path = "../altium-macros", version = "0.1.0" } -base64 = "0.21.2" +base64 = "0.21.4" # Use custom rev so we get debug outputs cfb = { git = "https://github.com/mdsteele/rust-cfb.git", rev = "5c5279d6" } # cfb = "0.8.1" -flate2 = "1.0.26" +flate2 = "1.0.27" # image = "0.24.6" -image = { version = "0.24.6", default-features = false, features = ["png", "bmp", "jpeg"] } +image = { version = "0.24.7", default-features = false, features = ["png", "bmp", "jpeg"] } lazy_static = "1.4.0" log = "0.4.20" -num_enum = "0.6.1" +num_enum = "0.7.0" quick-xml = "0.30.0" -regex = "1.9.1" +regex = "1.9.5" rust-ini = "0.19.0" -serde = "1.0.181" +serde = "1.0.188" serde-xml-rs = "0.6.0" svg = "0.13.1" uuid = { version = "1.4.1", features = ["v1", "v4", "fast-rng"]} -xml-rs = "0.8.16" +xml-rs = "0.8.18" [dev-dependencies] env_logger = "0.10.0" diff --git a/altium/src/__private.rs b/altium/src/__private.rs index 01783f8..80b7896 100644 --- a/altium/src/__private.rs +++ b/altium/src/__private.rs @@ -2,7 +2,7 @@ use std::sync::atomic::Ordering; -use crate::{common::buf2lstring, logging::UNSUPPORTED_KEYS}; +use crate::{common::buf2lstr, logging::UNSUPPORTED_KEYS}; pub fn num_unsupported_keys() -> u32 { UNSUPPORTED_KEYS.load(Ordering::Relaxed) diff --git a/altium/src/common.rs b/altium/src/common.rs index 6f51ba4..c504b33 100644 --- a/altium/src/common.rs +++ b/altium/src/common.rs @@ -118,8 +118,7 @@ impl FromUtf8<'_> for UniqueId { /// Altium uses the format `Key1=Val1|Key2=Val2...`, this handles that pub fn split_altium_map(buf: &[u8]) -> impl Iterator { buf.split(|b| *b == SEP).filter(|x| !x.is_empty()).map(|x| { - split_once(x, KV_SEP) - .unwrap_or_else(|| panic!("couldn't find `=` in `{}`", buf2lstring(buf))) + split_once(x, KV_SEP).unwrap_or_else(|| panic!("couldn't find `=` in `{}`", buf2lstr(buf))) }) } @@ -135,7 +134,7 @@ where } /// Quick helper method for a lossy string -pub fn buf2lstring(buf: &[u8]) -> String { +pub fn buf2lstr(buf: &[u8]) -> String { String::from_utf8_lossy(buf).to_string() } diff --git a/altium/src/logging.rs b/altium/src/logging.rs index a7a0f96..a8b6c5e 100644 --- a/altium/src/logging.rs +++ b/altium/src/logging.rs @@ -1,6 +1,6 @@ use std::sync::atomic::{AtomicU32, Ordering}; -use crate::common::buf2lstring; +use crate::common::buf2lstr; /// Track how many unsupported keys we have, useful for testing pub static UNSUPPORTED_KEYS: AtomicU32 = AtomicU32::new(0); @@ -8,7 +8,7 @@ pub static UNSUPPORTED_KEYS: AtomicU32 = AtomicU32::new(0); /// Log the unsupported key pub fn log_unsupported_key(key: &[u8], val: &[u8]) { UNSUPPORTED_KEYS.fetch_add(1, Ordering::Relaxed); - log::warn!("unsupported key {}={}", buf2lstring(key), buf2lstring(val)); + log::warn!("unsupported key {}={}", buf2lstr(key), buf2lstr(val)); } /// Called from our proc macro to log a key @@ -16,7 +16,7 @@ pub fn macro_unsupported_key(name: &str, key: &[u8], val: &[u8]) { UNSUPPORTED_KEYS.fetch_add(1, Ordering::Relaxed); log::warn!( "unsupported key for `{name}`: {}={} (via `FromRecord` derive)", - buf2lstring(key), - buf2lstring(val) + buf2lstr(key), + buf2lstr(val) ); } diff --git a/altium/src/parse/utf8.rs b/altium/src/parse/utf8.rs index 95c3f6c..7be1cf5 100644 --- a/altium/src/parse/utf8.rs +++ b/altium/src/parse/utf8.rs @@ -7,7 +7,7 @@ use std::str; use crate::{ - common::{buf2lstring, str_from_utf8}, + common::{buf2lstr, str_from_utf8}, ErrorKind, }; @@ -57,7 +57,7 @@ impl FromUtf8<'_> for bool { } else if buf == b"F" { Ok(false) } else { - Err(ErrorKind::ExpectedBool(buf2lstring(buf))) + Err(ErrorKind::ExpectedBool(buf2lstr(buf))) } } } diff --git a/altium/src/sch/schlib.rs b/altium/src/sch/schlib.rs index 2daaee6..767ac0f 100644 --- a/altium/src/sch/schlib.rs +++ b/altium/src/sch/schlib.rs @@ -10,7 +10,7 @@ use std::{fmt, str}; use cfb::CompoundFile; use section_keys::update_section_keys; -use crate::common::{buf2lstring, split_altium_map, Color, UniqueId}; +use crate::common::{buf2lstr, split_altium_map, Color, UniqueId}; use crate::error::{AddContext, ErrorKind}; use crate::font::{Font, FontCollection}; use crate::parse::ParseUtf8; @@ -342,8 +342,8 @@ impl SchLibMeta { } _ => log::warn!( "unsupported SchLib file header key {}:{}", - buf2lstring(key), - buf2lstring(val) + buf2lstr(key), + buf2lstr(val) ), } } diff --git a/altium/src/sch/storage.rs b/altium/src/sch/storage.rs index f83d631..c467ada 100644 --- a/altium/src/sch/storage.rs +++ b/altium/src/sch/storage.rs @@ -14,7 +14,7 @@ use std::{ use cfb::CompoundFile; use flate2::read::ZlibDecoder; -use crate::common::split_altium_map; +use crate::common::{buf2lstr, split_altium_map}; use crate::error::{AddContext, TruncBuf}; use crate::parse::{extract_sized_buf, extract_sized_utf8_buf, BufLenMatch}; use crate::{Error, ErrorKind}; @@ -91,9 +91,10 @@ impl Storage { let (mut header, mut rest) = extract_sized_buf(buf, BufLenMatch::U32, true).context("parsing storage")?; - header = &header[..header.len().saturating_sub(1)]; + // header = &header[..header.len().saturating_sub(1)]; let mut map_kv = split_altium_map(header); + let Some((b"HEADER", b"Icon storage")) = map_kv.next() else { return Err( ErrorKind::new_invalid_header(header, "Icon storage").context("parsing storage") From 8181ac1812a473c3d5cbca9819432dcf0b9d59ba Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 18 Sep 2023 22:34:39 -0400 Subject: [PATCH 06/15] Update workflow --- .github/workflows/validation-rust.yaml | 49 +++++--------------------- altium/tests/test_prjpcb.rs | 14 ++++---- 2 files changed, 16 insertions(+), 47 deletions(-) diff --git a/.github/workflows/validation-rust.yaml b/.github/workflows/validation-rust.yaml index a4ed748..c46585d 100644 --- a/.github/workflows/validation-rust.yaml +++ b/.github/workflows/validation-rust.yaml @@ -21,19 +21,11 @@ jobs: name: "Clippy (cargo clippy)" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@nightly with: components: clippy + - uses: Swatinem/rust-cache@v2 - run: cargo clippy --all-features --all-targets -- -D warnings - run: cargo clippy --no-default-features --all-targets -- -D warnings @@ -58,40 +50,24 @@ jobs: env: MYSQLCLIENT_LIB_DIR: C:\mysql\lib steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: List files run: | pwd ls - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - uses: dtolnay/rust-toolchain@1.70.0 + - uses: Swatinem/rust-cache@v2 - run: cargo test -- --nocapture fmt: name: "Format (cargo fmt)" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt + - uses: Swatinem/rust-cache@v2 - run: cargo fmt --all -- --check - uses: actions/setup-python@v3 @@ -99,17 +75,9 @@ jobs: name: "Docs (cargo doc)" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@nightly + - uses: Swatinem/rust-cache@v2 - run: cargo doc outdated: @@ -120,4 +88,5 @@ jobs: steps: - uses: actions/checkout@v3 - uses: dtolnay/install@cargo-outdated + - uses: Swatinem/rust-cache@v2 - run: cargo outdated --workspace --exit-code 1 diff --git a/altium/tests/test_prjpcb.rs b/altium/tests/test_prjpcb.rs index 28c1dfb..5bea98f 100644 --- a/altium/tests/test_prjpcb.rs +++ b/altium/tests/test_prjpcb.rs @@ -1,9 +1,9 @@ -use altium::PrjPcb; +// use altium::PrjPcb; -const PRJPCB: &str = "tests/samples/prjpcb/simple-proj.prjpcb"; +// const PRJPCB: &str = "tests/samples/prjpcb/simple-proj.prjpcb"; -#[test] -#[should_panic] // FIXME: shouldn't panic -fn test_file_ok() { - PrjPcb::from_file(PRJPCB).unwrap(); -} +// #[test] +// #[should_panic] // FIXME: shouldn't panic +// fn test_file_ok() { +// PrjPcb::from_file(PRJPCB).unwrap(); +// } From a56089cb6df8f347c1764bad1d85a2846e8e1e15 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 18 Sep 2023 22:43:35 -0400 Subject: [PATCH 07/15] Remove unneeded workflow env --- .github/workflows/validation-rust.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/validation-rust.yaml b/.github/workflows/validation-rust.yaml index c46585d..da85330 100644 --- a/.github/workflows/validation-rust.yaml +++ b/.github/workflows/validation-rust.yaml @@ -47,8 +47,6 @@ jobs: target: x86_64-pc-windows-msvc name: "Test on ${{ matrix.os }} (cargo test)" runs-on: ${{ matrix.os }} - env: - MYSQLCLIENT_LIB_DIR: C:\mysql\lib steps: - uses: actions/checkout@v3 - name: List files From e7607bb6efb3f1dddf1c2c4122fd1b800ed72a31 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 18 Sep 2023 22:46:17 -0400 Subject: [PATCH 08/15] Set CI to follow LFS checkouts --- .github/workflows/validation-rust.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/validation-rust.yaml b/.github/workflows/validation-rust.yaml index da85330..84258e9 100644 --- a/.github/workflows/validation-rust.yaml +++ b/.github/workflows/validation-rust.yaml @@ -49,10 +49,8 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - - name: List files - run: | - pwd - ls + with: + lfs: true - uses: dtolnay/rust-toolchain@1.70.0 - uses: Swatinem/rust-cache@v2 - run: cargo test -- --nocapture From 7a2e57dfbcd5ae6bf31b0b3224fb117db9a97230 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Mon, 18 Sep 2023 23:07:19 -0400 Subject: [PATCH 09/15] Add note about units --- altium/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/altium/src/lib.rs b/altium/src/lib.rs index e084c3f..259886c 100644 --- a/altium/src/lib.rs +++ b/altium/src/lib.rs @@ -2,6 +2,17 @@ //! //! It is very early in development, so please expect surprises if you are using //! it! +//! +//! # Units +//! +//! Unless otherwise stated, we try to use the following conventions: +//! +//! - For integer values, 1 = 1.0 nm +//! - For floating point values, 1.0 = 1.0 m +//! +//! 1nm precision is pretty excessive for what we need. However, it allows us to represent +//! anything from surface coating up to a 2.2 x 2.2 m PCB in an `i32`, which is more than +//! sufficient for the vast majority of use cases. #![allow(unused)] #![warn(clippy::pedantic)] From b3b4f62462e9959c39f2b5002ff245adb8ea062c Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Tue, 19 Sep 2023 02:15:46 -0400 Subject: [PATCH 10/15] Refactor proc macros to use `Location` --- altium-macros/src/lib.rs | 267 ++++++++++++++++++++-------------- altium/src/common.rs | 9 ++ altium/src/sch/record.rs | 73 +++------- altium/src/sch/record/draw.rs | 48 +++--- 4 files changed, 215 insertions(+), 182 deletions(-) diff --git a/altium-macros/src/lib.rs b/altium-macros/src/lib.rs index 03ef58b..a3aff59 100644 --- a/altium-macros/src/lib.rs +++ b/altium-macros/src/lib.rs @@ -34,7 +34,7 @@ pub fn derive_fromrecord(tokens: TokenStream) -> TokenStream { fn inner(tokens: TokenStream2) -> syn::Result { let parsed: DeriveInput = parse2(tokens)?; - let name = parsed.ident; + let struct_ident = parsed.ident; let Data::Struct(data) = parsed.data else { panic!("only usable on structs"); }; @@ -57,22 +57,28 @@ fn inner(tokens: TokenStream2) -> syn::Result { let record_variant = match struct_attr_map.remove("record_variant") { Some(TokenTree::Ident(val)) => val, Some(v) => panic!("Expected ident but got {v:?}"), - None => name.clone(), + None => struct_ident.clone(), }; error_if_map_not_empty(&struct_attr_map); - let mut match_stmts: Vec = Vec::new(); + // Collect each match arm and flag initializers that we will concat + // into our implementation + let mut match_arms: Vec = Vec::new(); let mut outer_flags: Vec = Vec::new(); + // Loop through each field in the struct for field in data.fields { let Type::Path(path) = field.ty else { panic!("invalid type") }; - let field_name = field.ident.unwrap(); + let field_ident = field.ident.unwrap(); + + // Parse attributes that exist on the field let mut field_attr_map = parse_attrs(field.attrs).unwrap_or_default(); + // Check if we need to parse an array if let Some(arr_val) = field_attr_map.remove("array") { let arr_val_str = arr_val.to_string(); if arr_val_str == "true" { @@ -84,7 +90,13 @@ fn inner(tokens: TokenStream2) -> syn::Result { .remove("map") .expect("missing 'map' attribute"); - process_array(&name, &field_name, count_ident, arr_map, &mut match_stmts); + process_array( + &struct_ident, + &field_ident, + count_ident, + arr_map, + &mut match_arms, + ); error_if_map_not_empty(&field_attr_map); continue; } else if arr_val_str != "false" { @@ -97,84 +109,60 @@ fn inner(tokens: TokenStream2) -> syn::Result { let match_pat = match field_attr_map.remove("rename") { Some(TokenTree::Literal(v)) => v, Some(v) => panic!("expected literal, got {v:?}"), - None => create_key_name(&field_name), + None => create_key_name(&field_ident), }; // If we haven't consumed all attributes, yell error_if_map_not_empty(&field_attr_map); - let match_lit = match_pat.to_string(); - let field_name_str = field_name.to_string(); let update_stmt = if path.path.segments.first().unwrap().ident == "Option" { - // Optional return - quote! { ret.#field_name = Some(parsed); } + // Wrap our field is an `Option` + quote! { ret.#field_ident = Some(parsed); } } else { - quote! { ret.#field_name = parsed; } + quote! { ret.#field_ident = parsed; } }; - // Altium does this weird thing where it will create a `%UTF8%` key and - // a key without that. let path_str = path.to_token_stream().to_string(); - let add_utf8_match = path_str.contains("String") || path_str.contains("str"); - - let (utf8_pat, utf8_def_flag, utf8_check_flag) = if add_utf8_match { - let match_pat_utf8 = create_key_name_utf8(&match_pat); - let match_lit_utf8 = match_pat.to_string(); - let flag_ident = Ident::new( - &format!("{field_name_str}_found_utf8_field"), - Span::call_site(), - ); - let pat = quote! { - #match_pat_utf8 => { - let parsed = val.parse_as_utf8() - // Add context of what we were trying to parse for errors - .context(concat!( - "while matching `", #match_lit_utf8, "` (`", #field_name_str , - "`) for `", stringify!(#name), "` (via proc macro)" - ))?; - - #flag_ident = true; - #update_stmt - }, - }; - let def_flag = quote! { let mut #flag_ident: bool = false; }; - let check_flag = quote! { - if #flag_ident { - ::log::debug!(concat!( - "skipping ", #field_name_str, " after finding utf8 version" - )); - continue; - } - }; + // Types `Location` and `LocationFract` are special cases + let is_location_fract = path_str.contains("LocationFract"); + if is_location_fract || path_str.contains("Location") { + process_location( + &struct_ident, + &field_ident, + is_location_fract, + &mut match_arms, + ); + continue; + } - (pat, def_flag, check_flag) + let Utf8Handler { + arm: utf8_arm, + define_flag: utf8_def_flag, + check_flag: utf8_check_flag, + } = if path_str.contains("String") || path_str.contains("str") { + // Altium does this weird thing where it will create a normal key and a key + // with `%UTF8%` if a value is utf8. We need to discard those redundant values + make_utf8_handler(&match_pat, &field_ident, &struct_ident, &update_stmt) } else { - ( - TokenStream2::new(), - TokenStream2::new(), - TokenStream2::new(), - ) + Utf8Handler::default() }; + let ctx_msg = make_ctx_message(&match_pat, &field_ident, &struct_ident); + let quoted = quote! { - #utf8_pat + #utf8_arm #match_pat => { #utf8_check_flag - let parsed = val.parse_as_utf8() - // Add context of what we were trying to parse for errors - .context(concat!( - "while matching `", #match_lit, "` (`", #field_name_str ,"`) for `", - stringify!(#name), "` (via proc macro)" - ))?; + let parsed = val.parse_as_utf8().context(#ctx_msg)?; #update_stmt }, }; outer_flags.push(utf8_def_flag); - match_stmts.push(quoted); + match_arms.push(quoted); } let ret_val = if use_box { @@ -184,7 +172,7 @@ fn inner(tokens: TokenStream2) -> syn::Result { }; let ret = quote! { - impl FromRecord for #name { + impl FromRecord for #struct_ident { const RECORD_ID: u32 = #id; fn from_record<'a, I: Iterator>( @@ -197,8 +185,8 @@ fn inner(tokens: TokenStream2) -> syn::Result { for (key, val) in records { match key { - #(#match_stmts)* - _ => crate::logging::macro_unsupported_key(stringify!(#name), key, val) + #(#match_arms)* + _ => crate::logging::macro_unsupported_key(stringify!(#struct_ident), key, val) } } @@ -280,7 +268,7 @@ enum MapParseState { /// found items in an array. /// /// E.g. with the above, `X1` will set `record[1].x` -fn parse_map(map: TokenTree) -> Vec<(Ident, Ident)> { +fn parse_attr_map(map: TokenTree) -> Vec<(Ident, Ident)> { let mut ret = Vec::new(); let TokenTree::Group(group) = map else { @@ -337,10 +325,48 @@ fn error_if_map_not_empty(map: &BTreeMap) { assert!(map.is_empty(), "unexpected pairs {map:?}"); } +fn process_location( + struct_ident: &Ident, + field_ident: &Ident, + is_location_fract: bool, + match_stmts: &mut Vec, +) { + let base_field_str = field_ident.to_string().to_case(Case::Pascal); + let x_str = format!("{base_field_str}.X"); + let y_str = format!("{base_field_str}.Y"); + + let check_patterns = if is_location_fract { + let x_str_frac = format!("{base_field_str}.X_Frac"); + let y_str_frac = format!("{base_field_str}.Y_Frac"); + vec![ + (x_str, quote!(ret.#field_ident.x)), + (y_str, quote!(ret.#field_ident.y)), + (x_str_frac, quote!(ret.#field_ident.x_fract)), + (y_str_frac, quote!(ret.#field_ident.y_fract)), + ] + } else { + vec![ + (x_str, quote!(ret.#field_ident.x)), + (y_str, quote!(ret.#field_ident.y)), + ] + }; + + for (pat_str, assign_field) in check_patterns { + let match_pat = Literal::byte_string(pat_str.as_bytes()); + let ctx_msg = make_ctx_message(&match_pat, field_ident, struct_ident); + + let match_arm = quote! { + #match_pat => #assign_field = val.parse_as_utf8().context(#ctx_msg)?, + }; + + match_stmts.push(match_arm); + } +} + /// Setup handling of `X1 = 1234, Y1 = 909` fn process_array( - name: &Ident, - field_name: &Ident, + struct_ident: &Ident, + field_ident: &Ident, count_ident_tt: TokenTree, arr_map_tt: TokenTree, match_stmts: &mut Vec, @@ -348,19 +374,17 @@ fn process_array( let TokenTree::Literal(match_pat) = count_ident_tt else { panic!("expected a literal for `count`"); }; - let arr_map = parse_map(arr_map_tt); + let arr_map = parse_attr_map(arr_map_tt); + + let field_name_str = field_ident.to_string(); + let ctx_msg = make_ctx_message(&match_pat, field_ident, struct_ident); - let field_name_str = field_name.to_string(); - let match_pat_str = match_pat.to_string(); let count_match = quote! { // Set the length of our array once given #match_pat => { - let count = val.parse_as_utf8().context(concat!( - "while matching `", #match_pat_str, "` (`", #field_name_str ,"`) for `", - stringify!(#name), "` (via proc macro array)" - ))?; + let count = val.parse_as_utf8().context(#ctx_msg)?; - ret.#field_name = vec![Default::default(); count].into(); + ret.#field_ident = vec![Default::default(); count].into(); }, }; @@ -375,54 +399,79 @@ fn process_array( .parse_as_utf8() .or_context(|| format!( "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - String::from_utf8_lossy(match_val), #field_name_str, stringify!(#name) + String::from_utf8_lossy(match_val), #field_name_str, stringify!(#struct_ident) ))?; let parsed_val = val.parse_as_utf8().or_context(|| format!( "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - String::from_utf8_lossy(match_val), #field_name_str, stringify!(#name) + String::from_utf8_lossy(match_val), #field_name_str, stringify!(#struct_ident) ))?; - ret.#field_name[idx - 1].#assign_value = parsed_val; + ret.#field_ident[idx - 1].#assign_value = parsed_val; }, }; match_stmts.push(item_match); } +} + +#[derive(Debug, Default)] +struct Utf8Handler { + /// The match arm + arm: TokenStream2, + /// Definition of a `fieldname_found_utf8_field` flag + define_flag: TokenStream2, + /// Checking if a flag is set + check_flag: TokenStream2, +} + +fn make_utf8_handler( + match_pat: &Literal, + field_ident: &Ident, + struct_ident: &Ident, + update_stmt: &TokenStream2, +) -> Utf8Handler { + let match_pat = create_key_name_utf8(match_pat); + let field_name_str = field_ident.to_string(); + let flag_ident = Ident::new( + &format!("{field_name_str}_found_utf8_field"), + Span::call_site(), + ); + + let ctx_msg = make_ctx_message(&match_pat, field_ident, struct_ident); + let arm = quote! { + #match_pat => { + let parsed = val.parse_as_utf8() + // Add context of what we were trying to parse for errors + .context(#ctx_msg)?; + + #flag_ident = true; + #update_stmt + }, + }; + let define_flag = quote! { let mut #flag_ident: bool = false; }; + let check_flag = quote! { + if #flag_ident { + ::log::trace!(concat!( + "skipping ", #field_name_str, " after finding utf8 version" + )); + continue; + } + }; + + Utf8Handler { + arm, + define_flag, + check_flag, + } +} - // // Set an X value if given - // xstr if crate::common::is_number_pattern(xstr, b'X') => { - // let idx: usize = xstr.strip_prefix(b"X").unwrap() - // .parse_as_utf8() - // .or_context(|| format!( - // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - // String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) - // ))?; - - // let x = val.parse_as_utf8().or_context(|| format!( - // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - // String::from_utf8_lossy(xstr), #field_name_str, stringify!(#name) - // ))?; - - // ret.#field_name[idx - 1].x = x; - // }, - - // // Set a Y value if given - // ystr if crate::common::is_number_pattern(ystr, b'Y') => { - // let idx: usize = ystr.strip_prefix(b"Y").unwrap() - // .parse_as_utf8() - // .or_context(|| format!( - // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - // String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) - // ))?; - - // let y = val.parse_as_utf8().or_context(|| format!( - // "while extracting `{}` (`{}`) for `{}` (via proc macro array)", - // String::from_utf8_lossy(ystr), #field_name_str, stringify!(#name) - // ))?; - - // ret.#field_name[idx - 1].y = y; - // }, - // }; +/// Make a message with our error context +fn make_ctx_message(pattern: &Literal, field_ident: &Ident, struct_ident: &Ident) -> Literal { + let s = format!( + "while matching `{}` (`{}`) for `{}` (via proc macro)", + pattern, field_ident, struct_ident + ); + Literal::string(&s) } /// From a field in our struct, create the name we should match by diff --git a/altium/src/common.rs b/altium/src/common.rs index c504b33..5c65f3d 100644 --- a/altium/src/common.rs +++ b/altium/src/common.rs @@ -16,6 +16,15 @@ pub struct Location { pub y: i32, } +/// Location with fraction +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct LocationFract { + pub x: i32, + pub x_fract: i32, + pub y: i32, + pub y_fract: i32, +} + impl Location { #[must_use] pub fn new(x: i32, y: i32) -> Self { diff --git a/altium/src/sch/record.rs b/altium/src/sch/record.rs index fcb1bb3..6caa702 100644 --- a/altium/src/sch/record.rs +++ b/altium/src/sch/record.rs @@ -61,7 +61,7 @@ pub(super) use parse::parse_all_records; use super::params::Justification; use super::pin::SchPin; -use crate::common::{Location, ReadOnlyState, UniqueId}; +use crate::common::{Location, LocationFract, ReadOnlyState, UniqueId}; use crate::error::{AddContext, TruncBuf}; use crate::font::{Font, FontCollection}; use crate::Error; @@ -199,8 +199,7 @@ pub struct MetaData { design_item_id: Box, source_library_name: Box, target_file_name: Box, - location_x: i32, - location_y: i32, + location: Location, unique_id: UniqueId, } @@ -208,8 +207,7 @@ pub struct MetaData { #[from_record(id = 3)] pub struct IeeeSymbol { is_not_accessible: bool, - location_x: i32, - location_y: i32, + location: Location, owner_index: u8, owner_part_id: i8, } @@ -222,8 +220,7 @@ pub struct Label { index_in_sheet: i16, is_not_accessible: bool, is_mirrored: bool, - location_x: i32, - location_y: i32, + location: Location, owner_index: u8, owner_part_id: i8, text: Box, @@ -286,8 +283,7 @@ pub struct Ellipse { is_not_accessible: bool, is_solid: bool, line_width: u16, - location_x: i32, - location_y: i32, + location: Location, owner_index: u8, owner_part_id: i8, owner_part_display_mode: i8, @@ -308,16 +304,14 @@ pub struct Piechart { pub struct RectangleRounded { area_color: Color, color: Color, - corner_x: i32, - corner_y: i32, + corner: Location, corner_x_radius: i32, corner_y_radius: i32, index_in_sheet: i16, is_not_accessible: bool, is_solid: bool, line_width: u16, - location_x: i32, - location_y: i32, + location: Location, owner_index: u8, owner_part_id: i8, owner_part_display_mode: i8, @@ -332,10 +326,7 @@ pub struct ElipticalArc { owner_part_id: i8, is_not_accessible: bool, index_in_sheet: i16, - location_x: i32, - location_x_frac: i32, - location_y: i32, - location_y_frac: i32, + location: LocationFract, radius: i8, radius_frac: i32, secondary_radius: i8, @@ -354,8 +345,7 @@ pub struct Arc { owner_part_id: i8, is_not_accessible: bool, index_in_sheet: i16, - location_x: i32, - location_y: i32, + location: Location, radius: i8, radius_frac: i32, secondary_radius: i8, @@ -392,15 +382,13 @@ pub struct Rectangle { area_color: Color, color: Color, /// Top right corner - corner_x: i32, - corner_y: i32, + corner: Location, index_in_sheet: i16, is_not_accessible: bool, is_solid: bool, line_width: u16, /// Bottom left corner - location_x: i32, - location_y: i32, + location: Location, owner_index: u8, owner_part_id: i8, owner_part_display_mode: i8, @@ -418,12 +406,11 @@ pub struct SheetSymbol { color: Color, area_color: Color, is_solid: bool, + location: Location, symbol_type: Box, show_net_name: bool, - location_y: i32, x_size: i32, y_size: i32, - location_x: i32, orientation: i32, font_id: u16, text: Box, @@ -459,8 +446,7 @@ pub struct PowerPort { index_in_sheet: i16, style: i16, show_net_name: bool, - location_y: i32, - location_x: i32, + location: Location, orientation: i32, font_id: u16, text: Box, @@ -481,8 +467,7 @@ pub struct Port { index_in_sheet: i16, #[from_record(rename = b"IOType")] io_type: u16, - location_x: i32, - location_y: i32, + location: Location, name: Box, owner_index: u8, owner_part_id: i8, @@ -500,8 +485,7 @@ pub struct NoErc { symbol: Box, is_active: bool, suppress_all: bool, - location_x: i32, - location_y: i32, + location: Location, color: Color, unique_id: UniqueId, } @@ -512,8 +496,7 @@ pub struct NetLabel { owner_index: u8, owner_part_id: i8, index_in_sheet: i16, - location_x: i32, - location_y: i32, + location: Location, color: Color, font_id: u16, text: Box, @@ -549,10 +532,8 @@ pub struct Wire { #[derive(Clone, Debug, Default, PartialEq, FromRecord)] #[from_record(id = 28)] pub struct TextFrame { - location_x: i32, - location_y: i32, - corner_x: i32, - corner_y: i32, + location: LocationFract, + corner: LocationFract, area_color: Color, owner_index: u8, owner_part_id: i8, @@ -579,10 +560,8 @@ pub struct Image { owner_part_id: i8, is_not_accessible: bool, index_in_sheet: i16, - location_x: i32, - location_y: i32, - corner_x: i32, - corner_y: i32, + location: Location, + corner: Location, keep_aspect: bool, embed_image: bool, file_name: Box, @@ -630,8 +609,7 @@ pub struct SheetName { owner_index: u8, owner_part_id: i8, index_in_sheet: i16, - location_x: i32, - location_y: i32, + location: Location, color: Color, font_id: u16, text: Box, @@ -644,8 +622,7 @@ pub struct FileName { owner_index: u8, owner_part_id: i8, index_in_sheet: i16, - location_x: i32, - location_y: i32, + location: Location, color: Color, font_id: u16, text: Box, @@ -657,8 +634,7 @@ pub struct FileName { pub struct Designator { owner_index: u8, owner_part_id: i8, - location_x: i32, - location_y: i32, + location: Location, color: Color, #[from_record(rename = b"FontID")] font_id: u16, @@ -690,8 +666,7 @@ pub struct Template { pub struct Parameter { owner_index: u8, owner_part_id: i8, - location_x: i32, - location_y: i32, + location: Location, index_in_sheet: i16, color: Color, font_id: u16, diff --git a/altium/src/sch/record/draw.rs b/altium/src/sch/record/draw.rs index 0bde4ec..addc0bc 100644 --- a/altium/src/sch/record/draw.rs +++ b/altium/src/sch/record/draw.rs @@ -165,8 +165,8 @@ impl Draw for record::Label { let font = &ctx.fonts.get_idx(self.font_id.into()); let (anchor_h, anchor_v) = self.justification.into(); canvas.draw_text(DrawText { - x: self.location_x, - y: self.location_y, + x: self.location.x, + y: self.location.y, text: &self.text, font, anchor_h, @@ -216,13 +216,13 @@ impl Draw for record::RectangleRounded { type Context<'a> = SchDrawCtx<'a>; fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { - let width = self.corner_x - self.location_x; - let height = self.corner_y - self.location_y; + let width = self.corner.x - self.location.x; + let height = self.corner.y - self.location.y; // FIXME: rounded rectangle canvas.draw_rectangle(DrawRectangle { - x: self.location_x, - y: self.location_y, + x: self.location.x, + y: self.location.y, width, height, fill_color: self.area_color, @@ -252,12 +252,12 @@ impl Draw for record::Rectangle { type Context<'a> = SchDrawCtx<'a>; fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { - let width = self.corner_x - self.location_x; - let height = self.corner_y - self.location_y; + let width = self.corner.x - self.location.x; + let height = self.corner.y - self.location.y; canvas.draw_rectangle(DrawRectangle { - x: self.location_x, - y: self.location_y, + x: self.location.x, + y: self.location.y, width, height, fill_color: self.area_color, @@ -272,8 +272,8 @@ impl Draw for record::SheetSymbol { fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { canvas.draw_rectangle(DrawRectangle { - x: self.location_x, - y: self.location_y - self.y_size, + x: self.location.x, + y: self.location.y - self.y_size, width: self.x_size, height: self.y_size, fill_color: self.area_color, @@ -294,12 +294,12 @@ impl Draw for record::Port { let h2 = self.height / 2; let mut locations = [Location::default(); 6]; - locations[0] = Location::new(self.location_x, self.location_y + h2); - locations[1] = Location::new(self.location_x + self.width - h2, self.location_y + h2); - locations[2] = Location::new(self.location_x + self.width, self.location_y); - locations[3] = Location::new(self.location_x + self.width - h2, self.location_y - h2); - locations[4] = Location::new(self.location_x, self.location_y - h2); - locations[5] = Location::new(self.location_x, self.location_y + h2); + locations[0] = Location::new(self.location.x, self.location.y + h2); + locations[1] = Location::new(self.location.x + self.width - h2, self.location.y + h2); + locations[2] = Location::new(self.location.x + self.width, self.location.y); + locations[3] = Location::new(self.location.x + self.width - h2, self.location.y - h2); + locations[4] = Location::new(self.location.x, self.location.y - h2); + locations[5] = Location::new(self.location.x, self.location.y + h2); canvas.draw_polygon(DrawPolygon { locations: &locations, @@ -310,8 +310,8 @@ impl Draw for record::Port { let font = &ctx.fonts.get_idx(self.font_id.into()); canvas.draw_text(DrawText { - x: self.location_x, - y: self.location_y, + x: self.location.x, + y: self.location.y, text: &self.name, color: self.text_color, font, @@ -329,8 +329,8 @@ impl Draw for record::NetLabel { let font = &ctx.fonts.get_idx(self.font_id.into()); canvas.draw_text(DrawText { - x: self.location_x, - y: self.location_y, + x: self.location.x, + y: self.location.y, text: &self.text, color: self.color, font, @@ -426,8 +426,8 @@ impl Draw for record::Parameter { fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { let font = &ctx.fonts.get_idx(self.font_id.into()); canvas.draw_text(DrawText { - x: self.location_x, - y: self.location_y, + x: self.location.x, + y: self.location.y, text: &self.text, font, ..Default::default() From 9ed11f958f5d14815c8fcb3530623cd48e27af88 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Tue, 19 Sep 2023 02:33:09 -0400 Subject: [PATCH 11/15] Add unit conversion --- altium-macros/src/lib.rs | 33 ++++++++++++++++++++++++--------- altium/src/common.rs | 10 +++++++++- altium/src/error.rs | 14 +++++++++----- altium/src/lib.rs | 2 +- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/altium-macros/src/lib.rs b/altium-macros/src/lib.rs index a3aff59..c9f41c2 100644 --- a/altium-macros/src/lib.rs +++ b/altium-macros/src/lib.rs @@ -325,6 +325,8 @@ fn error_if_map_not_empty(map: &BTreeMap) { assert!(map.is_empty(), "unexpected pairs {map:?}"); } +/// Handle special cases `Location` and `LocationFrac`. +/// Converts mils -> nm fn process_location( struct_ident: &Ident, field_ident: &Ident, @@ -339,24 +341,37 @@ fn process_location( let x_str_frac = format!("{base_field_str}.X_Frac"); let y_str_frac = format!("{base_field_str}.Y_Frac"); vec![ - (x_str, quote!(ret.#field_ident.x)), - (y_str, quote!(ret.#field_ident.y)), - (x_str_frac, quote!(ret.#field_ident.x_fract)), - (y_str_frac, quote!(ret.#field_ident.y_fract)), + (x_str, quote!(ret.#field_ident.x), true), + (y_str, quote!(ret.#field_ident.y), true), + (x_str_frac, quote!(ret.#field_ident.x_fract), false), + (y_str_frac, quote!(ret.#field_ident.y_fract), false), ] } else { vec![ - (x_str, quote!(ret.#field_ident.x)), - (y_str, quote!(ret.#field_ident.y)), + (x_str, quote!(ret.#field_ident.x), true), + (y_str, quote!(ret.#field_ident.y), true), ] }; - for (pat_str, assign_field) in check_patterns { + for (pat_str, assign_field, scale) in check_patterns { let match_pat = Literal::byte_string(pat_str.as_bytes()); let ctx_msg = make_ctx_message(&match_pat, field_ident, struct_ident); - let match_arm = quote! { - #match_pat => #assign_field = val.parse_as_utf8().context(#ctx_msg)?, + // Still haven't figured out how to handle fractions, but we don't + // scale them (they overflow) + let match_arm = if scale { + quote! { + #match_pat => #assign_field = + val.parse_as_utf8() + .map_err(Into::into) + .and_then(crate::common::mils_to_nm) + .context(#ctx_msg)?, + } + } else { + quote! { + #match_pat => #assign_field = + val.parse_as_utf8().context(#ctx_msg)?, + } }; match_stmts.push(match_arm); diff --git a/altium/src/common.rs b/altium/src/common.rs index 5c65f3d..261bf6f 100644 --- a/altium/src/common.rs +++ b/altium/src/common.rs @@ -2,7 +2,7 @@ use std::{fmt, str}; use uuid::Uuid; -use crate::error::{ErrorKind, TruncBuf}; +use crate::error::{AddContext, ErrorKind, Result, TruncBuf}; use crate::parse::{FromUtf8, ParseUtf8}; /// Separator in textlike streams @@ -290,3 +290,11 @@ pub fn is_number_pattern(s: &[u8], prefix: &[u8]) -> bool { false } + +/// Infallible conversion +pub fn mils_to_nm(mils: i32) -> Result { + const FACTOR: i32 = 25400; + mils.checked_mul(FACTOR).ok_or_else(|| { + ErrorKind::Overflow(mils.into(), FACTOR.into(), '*').context("converting units") + }) +} diff --git a/altium/src/error.rs b/altium/src/error.rs index 792391f..0396ad0 100644 --- a/altium/src/error.rs +++ b/altium/src/error.rs @@ -10,6 +10,8 @@ use std::str::Utf8Error; use crate::sch::PinError; +pub type Result = core::result::Result; + /// Our main error type is an error ([`ErrorKind`]) plus some context for what /// caused it, a quasi-backtrace. pub struct Error { @@ -80,8 +82,8 @@ pub enum ErrorKind { ExpectedInt(String, ParseIntError), ExpectedNul(TruncBuf), FileType(String, &'static str), - Image(image::ImageError), - IniFormat(Box), + Image(Box), + IniFormat(ini::ParseError), InvalidHeader(Box, &'static str), InvalidKey(Box), InvalidStorageData(TruncBuf), @@ -91,6 +93,7 @@ pub enum ErrorKind { Justification(u8), MissingSection(String), MissingUniqueId, + Overflow(i64, i64, char), Pin(PinError), ReadOnlyState(u8), RequiredSplit(String), @@ -142,6 +145,7 @@ impl fmt::Display for ErrorKind { ), ErrorKind::Image(e) => write!(f, "image error: {e}"), ErrorKind::ExpectedNul(e) => write!(f, "expected nul near {e}"), + ErrorKind::Overflow(a, b, op) => write!(f, "overflow at {a} {op} {b}"), } } } @@ -164,7 +168,7 @@ impl ErrorKind { impl From for ErrorKind { fn from(value: ini::ParseError) -> Self { - Self::IniFormat(Box::new(value)) + Self::IniFormat(value) } } @@ -181,7 +185,7 @@ impl From for ErrorKind { fn from(value: ini::Error) -> Self { match value { ini::Error::Io(e) => Self::Io(e), - ini::Error::Parse(e) => Self::IniFormat(Box::new(e)), + ini::Error::Parse(e) => Self::IniFormat(e), } } } @@ -230,7 +234,7 @@ impl From for Error { impl From for ErrorKind { fn from(value: image::ImageError) -> Self { - Self::Image(value) + Self::Image(Box::new(value)) } } diff --git a/altium/src/lib.rs b/altium/src/lib.rs index 259886c..5d54fc6 100644 --- a/altium/src/lib.rs +++ b/altium/src/lib.rs @@ -41,7 +41,7 @@ pub mod sch; #[doc(inline)] pub use common::UniqueId; #[doc(inline)] -pub use error::{Error, ErrorKind}; +pub use error::{Error, ErrorKind, Result}; #[doc(inline)] pub use pcb::{PcbDoc, PcbLib}; #[doc(inline)] From 933297f70123bf1a5696a45ba240882a8c568b10 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Tue, 19 Sep 2023 03:05:05 -0400 Subject: [PATCH 12/15] Updates to documentation --- altium/src/common.rs | 4 +++- altium/src/error.rs | 4 +++- altium/src/font.rs | 2 +- altium/src/lib.rs | 1 + altium/src/sch/component.rs | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/altium/src/common.rs b/altium/src/common.rs index 261bf6f..a189372 100644 --- a/altium/src/common.rs +++ b/altium/src/common.rs @@ -70,13 +70,15 @@ impl FromUtf8<'_> for Visibility { } } -/// A unique ID +/// An Altium unique ID. /// +/// Every entity in Altium has a unique ID including files, library items, and records. // TODO: figure out what file types use this exact format #[derive(Clone, Copy, PartialEq)] pub enum UniqueId { /// Altium's old style UUID Simple([u8; 8]), + /// UUID style, used by some newer files Uuid(Uuid), } diff --git a/altium/src/error.rs b/altium/src/error.rs index 0396ad0..188e12a 100644 --- a/altium/src/error.rs +++ b/altium/src/error.rs @@ -10,6 +10,7 @@ use std::str::Utf8Error; use crate::sch::PinError; +/// The main result type used by this crate pub type Result = core::result::Result; /// Our main error type is an error ([`ErrorKind`]) plus some context for what @@ -70,7 +71,7 @@ impl fmt::Debug for Error { } } -/// A raw error caused somewhere along the file +/// A raw error without context #[derive(Debug)] #[non_exhaustive] pub enum ErrorKind { @@ -300,6 +301,7 @@ impl AddContext for Result { } /// A subslice of a buffer for nicer error messages +#[doc(hidden)] #[derive(Clone, Debug)] pub struct TruncBuf { buf: Box<[T]>, diff --git a/altium/src/font.rs b/altium/src/font.rs index 05d4b45..5e56aa1 100644 --- a/altium/src/font.rs +++ b/altium/src/font.rs @@ -3,7 +3,7 @@ use std::ops::{Deref, DerefMut}; lazy_static::lazy_static! { - pub static ref DEFAULT_FONT: Font = Font { + pub(crate) static ref DEFAULT_FONT: Font = Font { name: "Calibri".into(), size: 8, }; diff --git a/altium/src/lib.rs b/altium/src/lib.rs index 5d54fc6..7e0f84c 100644 --- a/altium/src/lib.rs +++ b/altium/src/lib.rs @@ -30,6 +30,7 @@ mod parse; #[doc(hidden)] pub mod __private; +#[doc(hidden)] // docs are unfinished pub mod draw; pub mod dwf; pub mod error; diff --git a/altium/src/sch/component.rs b/altium/src/sch/component.rs index 5751913..8ef2f76 100644 --- a/altium/src/sch/component.rs +++ b/altium/src/sch/component.rs @@ -95,7 +95,7 @@ impl Component { meta.description.as_deref().unwrap_or("") } - /// Compare + /// Compare two components based on name only pub fn name_cmp(&self, other: &Self) -> Option { self.name.partial_cmp(&other.name) } From 5514f564f6c5a77d9a2baacee2dd9aa120746826 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Tue, 19 Sep 2023 04:46:02 -0400 Subject: [PATCH 13/15] Clean up unused warnings --- altium-macros/src/lib.rs | 2 +- altium/src/__private.rs | 2 +- altium/src/common.rs | 10 +++++++- altium/src/draw/canvas.rs | 3 +-- altium/src/draw/mod.rs | 6 ----- altium/src/draw/svg.rs | 14 +++------- altium/src/error.rs | 2 +- altium/src/lib.rs | 2 +- altium/src/parse/bin.rs | 7 +---- altium/src/parse/from_record.rs | 2 -- altium/src/sch/component.rs | 4 --- altium/src/sch/pin.rs | 45 +++++++++++++++------------------ altium/src/sch/record.rs | 2 +- altium/src/sch/record/draw.rs | 34 ++++++++++--------------- altium/src/sch/schlib.rs | 5 ++-- altium/src/sch/storage.rs | 15 ++++------- 16 files changed, 63 insertions(+), 92 deletions(-) diff --git a/altium-macros/src/lib.rs b/altium-macros/src/lib.rs index c9f41c2..ca2528f 100644 --- a/altium-macros/src/lib.rs +++ b/altium-macros/src/lib.rs @@ -364,7 +364,7 @@ fn process_location( #match_pat => #assign_field = val.parse_as_utf8() .map_err(Into::into) - .and_then(crate::common::mils_to_nm) + .and_then(crate::common::i32_mils_to_nm) .context(#ctx_msg)?, } } else { diff --git a/altium/src/__private.rs b/altium/src/__private.rs index 80b7896..4138391 100644 --- a/altium/src/__private.rs +++ b/altium/src/__private.rs @@ -2,7 +2,7 @@ use std::sync::atomic::Ordering; -use crate::{common::buf2lstr, logging::UNSUPPORTED_KEYS}; +use crate::logging::UNSUPPORTED_KEYS; pub fn num_unsupported_keys() -> u32 { UNSUPPORTED_KEYS.load(Ordering::Relaxed) diff --git a/altium/src/common.rs b/altium/src/common.rs index a189372..660a81d 100644 --- a/altium/src/common.rs +++ b/altium/src/common.rs @@ -83,6 +83,7 @@ pub enum UniqueId { } impl UniqueId { + #[allow(unused)] fn from_slice>(buf: S) -> Option { buf.as_ref() .try_into() @@ -294,9 +295,16 @@ pub fn is_number_pattern(s: &[u8], prefix: &[u8]) -> bool { } /// Infallible conversion -pub fn mils_to_nm(mils: i32) -> Result { +pub fn i32_mils_to_nm(mils: i32) -> Result { const FACTOR: i32 = 25400; mils.checked_mul(FACTOR).ok_or_else(|| { ErrorKind::Overflow(mils.into(), FACTOR.into(), '*').context("converting units") }) } + +pub fn u32_mils_to_nm(mils: u32) -> Result { + const FACTOR: u32 = 25400; + mils.checked_mul(FACTOR).ok_or_else(|| { + ErrorKind::Overflow(mils.into(), FACTOR.into(), '*').context("converting units") + }) +} diff --git a/altium/src/draw/canvas.rs b/altium/src/draw/canvas.rs index 173d2ec..73f0a45 100644 --- a/altium/src/draw/canvas.rs +++ b/altium/src/draw/canvas.rs @@ -1,7 +1,6 @@ use crate::{ common::{Color, Location, PosHoriz, PosVert, Rotation}, font::Font, - sch::Justification, }; /// Generic trait for something that can be drawn. Beware, unstable! @@ -11,7 +10,7 @@ pub trait Canvas { fn draw_polygon(&mut self, item: DrawPolygon); fn draw_rectangle(&mut self, item: DrawRectangle); fn draw_image(&mut self, item: DrawImage); - fn add_comment>(&mut self, comment: S) {} + fn add_comment>(&mut self, _comment: S) {} } /// Helper struct to write some text diff --git a/altium/src/draw/mod.rs b/altium/src/draw/mod.rs index c8e0510..1f37e73 100644 --- a/altium/src/draw/mod.rs +++ b/altium/src/draw/mod.rs @@ -1,16 +1,10 @@ pub(crate) mod canvas; mod svg; -use core::{ - cmp::{max, min}, - mem, -}; - pub use canvas::{Canvas, DrawImage, DrawLine, DrawPolygon, DrawRectangle, DrawText}; pub use self::svg::SvgCtx; pub use crate::common::{Color, Location, PosHoriz, PosVert}; -use crate::font::Font; pub trait Draw { type Context<'a>; diff --git a/altium/src/draw/svg.rs b/altium/src/draw/svg.rs index 0f66d19..74ca542 100644 --- a/altium/src/draw/svg.rs +++ b/altium/src/draw/svg.rs @@ -6,12 +6,7 @@ use svg::node::{element as el, Text}; use svg::Node; use super::{canvas, Canvas}; -use crate::common::Location; -use crate::{ - common::{Color, PosHoriz, PosVert, Rotation}, - font::Font, - sch::Justification, -}; +use crate::common::{PosHoriz, PosVert, Rotation}; #[derive(Clone, Debug)] pub struct SvgCtx { @@ -109,7 +104,6 @@ impl SvgCtx { impl Canvas for SvgCtx { #[allow(clippy::similar_names)] fn draw_text(&mut self, item: canvas::DrawText) { - use Justification as J; use PosHoriz::{Center, Left, Right}; use PosVert::{Bottom, Middle, Top}; use Rotation::{R0, R180, R270, R90}; @@ -128,7 +122,7 @@ impl Canvas for SvgCtx { PosHoriz::Right => "end", }; - /// Offset of max x from min x + // Offset of max x from min x let (xoffn, xoffp) = match item.anchor_h { Left => (0, width), Center => (halfwidth, halfwidth), @@ -172,7 +166,7 @@ impl Canvas for SvgCtx { } fn draw_line(&mut self, item: canvas::DrawLine) { - let dx = item.start.x - item.end.x; + // let dx = item.start.x - item.end.x; let dy = item.start.y - item.end.y; let mut node = el::Line::new() @@ -207,7 +201,7 @@ impl Canvas for SvgCtx { // todo!() } - fn draw_image(&mut self, item: canvas::DrawImage) {} + fn draw_image(&mut self, _item: canvas::DrawImage) {} fn add_comment>(&mut self, comment: S) { let cmt = svg::node::Comment::new(comment); diff --git a/altium/src/error.rs b/altium/src/error.rs index 188e12a..ebe592e 100644 --- a/altium/src/error.rs +++ b/altium/src/error.rs @@ -3,7 +3,6 @@ use std::borrow::Cow; use std::cmp::min; use std::fmt; -use std::fmt::Write; use std::io; use std::num::{ParseFloatError, ParseIntError}; use std::str::Utf8Error; @@ -158,6 +157,7 @@ impl ErrorKind { Self::InvalidStream(name.into(), pos) } + #[allow(unused)] pub(crate) fn new_invalid_key(key: &[u8]) -> Self { Self::InvalidKey(String::from_utf8_lossy(key).into()) } diff --git a/altium/src/lib.rs b/altium/src/lib.rs index 7e0f84c..cc535bd 100644 --- a/altium/src/lib.rs +++ b/altium/src/lib.rs @@ -14,7 +14,7 @@ //! anything from surface coating up to a 2.2 x 2.2 m PCB in an `i32`, which is more than //! sufficient for the vast majority of use cases. -#![allow(unused)] +// #![allow(unused)] #![warn(clippy::pedantic)] #![allow(clippy::unreadable_literal)] #![allow(clippy::module_name_repetitions)] diff --git a/altium/src/parse/bin.rs b/altium/src/parse/bin.rs index 4220349..78b9b29 100644 --- a/altium/src/parse/bin.rs +++ b/altium/src/parse/bin.rs @@ -1,11 +1,6 @@ use std::str; -use crate::{ - common::{split_once, str_from_utf8}, - error::TruncBuf, - Error, - ErrorKind, -}; +use crate::{common::str_from_utf8, error::TruncBuf, ErrorKind}; #[derive(Clone, Copy, Debug, PartialEq)] pub enum BufLenMatch { diff --git a/altium/src/parse/from_record.rs b/altium/src/parse/from_record.rs index 4609ef8..b3f6388 100644 --- a/altium/src/parse/from_record.rs +++ b/altium/src/parse/from_record.rs @@ -1,7 +1,5 @@ //! Trait for a common way to extract records -use std::fmt::Write; - use crate::{common::split_altium_map, error::AddContext, sch::SchRecord, Error}; /// Given a record with an ID, parse the items diff --git a/altium/src/sch/component.rs b/altium/src/sch/component.rs index 8ef2f76..26f72da 100644 --- a/altium/src/sch/component.rs +++ b/altium/src/sch/component.rs @@ -1,6 +1,5 @@ //! Things related to the entire component -use core::fmt::Write; use std::cmp::Ordering; use std::fs::File; use std::io; @@ -13,10 +12,7 @@ use super::record::parse_all_records; use super::storage::Storage; use super::{SchDrawCtx, SchRecord}; use crate::draw::{Canvas, Draw, SvgCtx}; -use crate::error::AddContext; use crate::font::FontCollection; -use crate::sch::pin::SchPin; -use crate::sch::record::parse_any_record; use crate::Error; /// Representation of a component diff --git a/altium/src/sch/pin.rs b/altium/src/sch/pin.rs index eaf776a..02c0f2e 100644 --- a/altium/src/sch/pin.rs +++ b/altium/src/sch/pin.rs @@ -5,14 +5,13 @@ use std::str::{self, Utf8Error}; use altium_macros::FromRecord; use log::warn; -use num_enum::TryFromPrimitive; use super::SchRecord; -use crate::common::{Location, Rotation, Visibility}; +use crate::common::{i32_mils_to_nm, u32_mils_to_nm, Location, Rotation, Visibility}; use crate::error::AddContext; use crate::parse::ParseUtf8; use crate::parse::{FromRecord, FromUtf8}; -use crate::{ErrorKind, UniqueId}; +use crate::{ErrorKind, Result, UniqueId}; /// Representation of a pin /// @@ -28,8 +27,7 @@ pub struct SchPin { // #[from_record(rename = b"PinDesignator")] pub(super) designator: Box, pub(super) name: Box, - pub(super) location_x: i32, - pub(super) location_y: i32, + pub(super) location: Location, pub(super) electrical: ElectricalType, #[from_record(rename = b"PinLength")] pub(super) length: u32, @@ -44,21 +42,21 @@ pub struct SchPin { } impl SchPin { - pub(crate) fn parse(buf: &[u8]) -> Result { + pub(crate) fn parse(buf: &[u8]) -> Result { // 6 bytes unknown let [_, _, _, _, _, _, rest @ ..] = buf else { - return Err(PinError::TooShort(buf.len(), "initial group")); + return Err(PinError::TooShort(buf.len(), "initial group").into()); }; // 6 more bytes unknown - symbols let [_, _, _, _, _, _, rest @ ..] = rest else { - return Err(PinError::TooShort(rest.len(), "second group")); + return Err(PinError::TooShort(rest.len(), "second group").into()); }; let (description, rest) = sized_buf_to_utf8(rest, "description")?; // TODO: ty_info - let [formal_type, ty_info, rot_hide, l0, l1, x0, x1, y0, y1, rest @ ..] = rest else { - return Err(PinError::TooShort(rest.len(), "position extraction")); + let [formal_type, _ty_info, rot_hide, l0, l1, x0, x1, y0, y1, rest @ ..] = rest else { + return Err(PinError::TooShort(rest.len(), "position extraction").into()); }; assert_eq!( @@ -71,7 +69,7 @@ impl SchPin { let location_y = i16::from_le_bytes([*y0, *y1]); let [_, _, _, _, rest @ ..] = rest else { - return Err(PinError::TooShort(rest.len(), "remaining buffer")); + return Err(PinError::TooShort(rest.len(), "remaining buffer").into()); }; let (name, rest) = sized_buf_to_utf8(rest, "name")?; @@ -81,6 +79,10 @@ impl SchPin { warn!("unexpected rest: {rest:02x?}"); } + let location = Location { + x: i32_mils_to_nm(i32::from(location_x))?, + y: i32_mils_to_nm(i32::from(location_y))?, + }; let retval = Self { formal_type: *formal_type, owner_index: 0, @@ -88,9 +90,8 @@ impl SchPin { description: description.into(), designator: designator.into(), name: name.into(), - location_x: i32::from(location_x), - location_y: i32::from(location_y), - length: u32::from(length), + location, + length: u32_mils_to_nm(u32::from(length))?, // location_x: i32::from(location_x) * 10, // location_y: i32::from(location_y) * 10, // length: u32::from(length) * 10, @@ -105,24 +106,20 @@ impl SchPin { /// Nonconnecting point of this pin pub(crate) fn location(&self) -> Location { - Location { - x: self.location_x, - y: self.location_y, - } + self.location } /// Altium stores the position of the pin at its non-connecting end. Which /// seems dumb. This provides the connecting end. pub(crate) fn location_conn(&self) -> Location { - let x_orig = self.location_x; - let y_orig = self.location_y; + let orig = self.location; let len = i32::try_from(self.length).unwrap(); let (x, y) = match self.rotation { - Rotation::R0 => (x_orig + len, y_orig), - Rotation::R90 => (x_orig, y_orig + len), - Rotation::R180 => (x_orig - len, y_orig), - Rotation::R270 => (x_orig, y_orig - len), + Rotation::R0 => (orig.x + len, orig.y), + Rotation::R90 => (orig.x, orig.y + len), + Rotation::R180 => (orig.x - len, orig.y), + Rotation::R270 => (orig.x, orig.y - len), }; Location { x, y } } diff --git a/altium/src/sch/record.rs b/altium/src/sch/record.rs index 6caa702..95546ac 100644 --- a/altium/src/sch/record.rs +++ b/altium/src/sch/record.rs @@ -63,7 +63,7 @@ use super::params::Justification; use super::pin::SchPin; use crate::common::{Location, LocationFract, ReadOnlyState, UniqueId}; use crate::error::{AddContext, TruncBuf}; -use crate::font::{Font, FontCollection}; +use crate::font::FontCollection; use crate::Error; use crate::{ common::Color, diff --git a/altium/src/sch/record/draw.rs b/altium/src/sch/record/draw.rs index addc0bc..7cd7270 100644 --- a/altium/src/sch/record/draw.rs +++ b/altium/src/sch/record/draw.rs @@ -1,19 +1,16 @@ //! How to draw records, components, etc -use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine}; -use svg::node::{element as el, Text}; -use svg::Node; use crate::common::{Color, Location, PosHoriz, PosVert, Rotation, Visibility}; use crate::draw::canvas::DrawRectangle; use crate::draw::canvas::{Canvas, DrawLine, DrawText}; use crate::draw::{Draw, DrawPolygon}; -use crate::font::{Font, FontCollection}; -use crate::sch::params::Justification; +use crate::font::FontCollection; use crate::sch::pin::SchPin; use crate::sch::record; use crate::sch::storage::Storage; // 500k embedded +#[allow(unused)] const MAX_EMBED_SIZE: usize = 500_000; // FIXME: This context is super bad and weird with, like, triple indirection @@ -73,16 +70,13 @@ impl Draw for record::SchRecord { impl Draw for SchPin { type Context<'a> = SchDrawCtx<'a>; - fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { - use PosHoriz::{Center, Left, Right}; - use PosVert::{Bottom, Middle, Top}; + fn draw(&self, canvas: &mut C, _ctx: &SchDrawCtx<'_>) { + use PosHoriz::{Left, Right}; + use PosVert::{Bottom, Middle}; use Rotation::{R0, R180, R270, R90}; canvas.add_comment(format!("{self:#?}")); - let len: i32 = self.length.try_into().unwrap(); - let (x1, y1) = (self.location_x, self.location_y); - let start = self.location(); let end = self.location_conn(); @@ -182,7 +176,7 @@ impl Draw for record::Label { impl Draw for record::PolyLine { type Context<'a> = SchDrawCtx<'a>; - fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + fn draw(&self, canvas: &mut C, _ctx: &Self::Context<'_>) { for window in self.locations.windows(2) { let &[a, b] = window else { unreachable!() }; @@ -199,7 +193,7 @@ impl Draw for record::PolyLine { impl Draw for record::Polygon { type Context<'a> = SchDrawCtx<'a>; - fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, _ctx: &SchDrawCtx<'_>) { canvas.draw_polygon(DrawPolygon { locations: &self.locations, fill_color: self.area_color, @@ -215,7 +209,7 @@ impl Draw for record::Polygon { impl Draw for record::RectangleRounded { type Context<'a> = SchDrawCtx<'a>; - fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, _ctx: &SchDrawCtx<'_>) { let width = self.corner.x - self.location.x; let height = self.corner.y - self.location.y; @@ -238,7 +232,7 @@ impl Draw for record::RectangleRounded { impl Draw for record::Line { type Context<'a> = SchDrawCtx<'a>; - fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + fn draw(&self, canvas: &mut C, _ctx: &Self::Context<'_>) { canvas.draw_line(DrawLine { start: Location::new(self.location_x, self.location_y), end: Location::new(self.corner_x, self.corner_y), @@ -251,7 +245,7 @@ impl Draw for record::Line { impl Draw for record::Rectangle { type Context<'a> = SchDrawCtx<'a>; - fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, _ctx: &SchDrawCtx<'_>) { let width = self.corner.x - self.location.x; let height = self.corner.y - self.location.y; @@ -270,7 +264,7 @@ impl Draw for record::Rectangle { impl Draw for record::SheetSymbol { type Context<'a> = SchDrawCtx<'a>; - fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + fn draw(&self, canvas: &mut C, _ctx: &SchDrawCtx<'_>) { canvas.draw_rectangle(DrawRectangle { x: self.location.x, y: self.location.y - self.y_size, @@ -342,7 +336,7 @@ impl Draw for record::NetLabel { impl Draw for record::Bus { type Context<'a> = SchDrawCtx<'a>; - fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + fn draw(&self, canvas: &mut C, _ctx: &Self::Context<'_>) { for window in self.locations.windows(2) { let &[a, b] = window else { unreachable!() }; @@ -359,7 +353,7 @@ impl Draw for record::Bus { impl Draw for record::Wire { type Context<'a> = SchDrawCtx<'a>; - fn draw(&self, canvas: &mut C, ctx: &Self::Context<'_>) { + fn draw(&self, canvas: &mut C, _ctx: &Self::Context<'_>) { for window in self.locations.windows(2) { let &[a, b] = window else { unreachable!() }; @@ -378,7 +372,7 @@ impl Draw for record::Wire { impl Draw for record::Image { type Context<'a> = SchDrawCtx<'a>; - fn draw(&self, canvas: &mut C, ctx: &SchDrawCtx<'_>) { + fn draw(&self, _canvas: &mut C, _ctx: &SchDrawCtx<'_>) { // TODO // TODO: just set to the URL. Maybe set whether or not to encode // somehow? diff --git a/altium/src/sch/schlib.rs b/altium/src/sch/schlib.rs index 767ac0f..471b204 100644 --- a/altium/src/sch/schlib.rs +++ b/altium/src/sch/schlib.rs @@ -52,7 +52,7 @@ impl<'a> SchLib> { impl SchLib { /// Unique ID of this schematic library - fn unique_id(&self) -> UniqueId { + pub fn unique_id(&self) -> UniqueId { self.header.unique_id } @@ -138,7 +138,7 @@ impl SchLib { let mut header = SchLibMeta::parse_cfile(&mut cfile, &mut tmp_buf)?; tmp_buf.clear(); - let mut storage = Storage::parse_cfile(&mut cfile, &mut tmp_buf)?; + let storage = Storage::parse_cfile(&mut cfile, &mut tmp_buf)?; tmp_buf.clear(); update_section_keys(&mut cfile, &mut tmp_buf, &mut header)?; @@ -381,6 +381,7 @@ impl ComponentMeta { /// Number of subparts within a component /// /// FIXME: this seems to be doubled? + #[allow(unused)] fn part_count(&self) -> u16 { self.part_count } diff --git a/altium/src/sch/storage.rs b/altium/src/sch/storage.rs index c467ada..bb7cf2d 100644 --- a/altium/src/sch/storage.rs +++ b/altium/src/sch/storage.rs @@ -2,19 +2,14 @@ //! represented as zlib-compressed data. use core::fmt; -use std::ffi::OsStr; -use std::fmt::LowerHex; -use std::io::{self, Cursor, Read, Seek, Write}; +use std::io::{Cursor, Read, Seek}; use std::sync::Mutex; -use std::{ - collections::BTreeMap, - sync::{Arc, RwLock}, -}; +use std::{collections::BTreeMap, sync::Arc}; use cfb::CompoundFile; use flate2::read::ZlibDecoder; -use crate::common::{buf2lstr, split_altium_map}; +use crate::common::split_altium_map; use crate::error::{AddContext, TruncBuf}; use crate::parse::{extract_sized_buf, extract_sized_utf8_buf, BufLenMatch}; use crate::{Error, ErrorKind}; @@ -88,7 +83,7 @@ impl Storage { } pub(crate) fn parse(buf: &[u8]) -> Result { - let (mut header, mut rest) = + let (header, mut rest) = extract_sized_buf(buf, BufLenMatch::U32, true).context("parsing storage")?; // header = &header[..header.len().saturating_sub(1)]; @@ -102,7 +97,7 @@ impl Storage { }; // Weight indicates how many items are in the storage - let Some((b"Weight", weight_val)) = map_kv.next() else { + let Some((b"Weight", _weight_val)) = map_kv.next() else { assert!( rest.is_empty(), "weight not present but rest was not empty at {}", From 2149cce7048341ae8dbdd862421f0ee6d8b962d4 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Tue, 19 Sep 2023 04:50:59 -0400 Subject: [PATCH 14/15] Clarify storage wording --- altium/src/sch/storage.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/altium/src/sch/storage.rs b/altium/src/sch/storage.rs index bb7cf2d..f1dd546 100644 --- a/altium/src/sch/storage.rs +++ b/altium/src/sch/storage.rs @@ -19,8 +19,9 @@ use crate::{Error, ErrorKind}; /// /// We try to avoid: /// -/// 1. Decompressing anything we don't have to, and +/// 1. Decompressing anything we don't have to /// 2. Decompressing anything more than once +/// 3. Storing anything more than once /// /// So, we have a solution where: /// From 06648b0da229e15339a63537cc1c88c7fa666e71 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Fri, 4 Aug 2023 03:44:00 -0400 Subject: [PATCH 15/15] Work on filling out PCBLibrary descriptions --- altium/src/pcb/pcblib.rs | 81 +++++++++- altium/tests/test_pcblib.rs | 12 ++ book/PCB.md | 298 ++++++++++++++++++++++++++++++++++++ book/PcbLib.md | 15 -- 4 files changed, 389 insertions(+), 17 deletions(-) create mode 100644 altium/tests/test_pcblib.rs create mode 100644 book/PCB.md delete mode 100644 book/PcbLib.md diff --git a/altium/src/pcb/pcblib.rs b/altium/src/pcb/pcblib.rs index 6169ccc..79f4e4a 100644 --- a/altium/src/pcb/pcblib.rs +++ b/altium/src/pcb/pcblib.rs @@ -1,2 +1,79 @@ -/// A PCB Library -pub struct PcbLib {} +use std::{cell::RefCell, fs::File, path::Path, io::{Cursor, Read, Seek}, fmt}; +use crate::{Error, error::AddContext}; +use cfb::CompoundFile; + +/// This is our top-level representation of a PCB library. +pub struct PcbLib { + /// Our open compoundfile buffer + cfile: RefCell>, + // /// Information contained in the compound file header. We use this as a + // /// lookup to see what we can extract from the file. + // header: SchLibMeta, + // storage: Arc, +} + +/// Impls that are specific to a file +impl PcbLib { + /// Open a file from disk + pub fn open>(path: P) -> Result { + let cfile = cfb::open(&path)?; + Self::from_cfile(cfile).or_context(|| format!("opening {}", path.as_ref().display())) + } +} + +impl<'a> PcbLib> { + /// Open an in-memory file from a buffer + pub fn from_buffer(buf: &'a [u8]) -> Result { + let cfile = cfb::CompoundFile::open(Cursor::new(buf))?; + Self::from_cfile(cfile) + } +} + + +impl PcbLib { + // /// Create an iterator over all components in this library. + // pub fn components(&self) -> ComponentsIter<'_, F> { + // ComponentsIter { + // schlib: self, + // current: 0, + // } + // } + + // /// Create an iterator over all fonts stored in this library. + // pub fn fonts(&self) -> impl Iterator { + // self.header.fonts.iter() + // } + + // /// Get information about the blob items stored + // pub fn storage(&self) -> &Storage { + // &self.storage + // } + + /// Create a `PcbLib` representation from any `Read`able compound file. + fn from_cfile(mut cfile: CompoundFile) -> Result { + let mut tmp_buf: Vec = Vec::new(); // scratch memory + + // let mut header = SchLibMeta::parse_cfile(&mut cfile, &mut tmp_buf)?; + // tmp_buf.clear(); + + // let mut storage = Storage::parse_cfile(&mut cfile, &mut tmp_buf)?; + // tmp_buf.clear(); + + // update_section_keys(&mut cfile, &mut tmp_buf, &mut header)?; + + // section_keys.map.entry(key) + Ok(Self { + cfile: RefCell::new(cfile), + // header, + // storage: storage.into(), + }) + } +} + +impl fmt::Debug for PcbLib { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PcbLib") + // .field("header", &self.header) + .finish_non_exhaustive() + } +} diff --git a/altium/tests/test_pcblib.rs b/altium/tests/test_pcblib.rs new file mode 100644 index 0000000..ce3f5b0 --- /dev/null +++ b/altium/tests/test_pcblib.rs @@ -0,0 +1,12 @@ +use altium::pcb::{PcbLib}; + +const PCBLIB_EMPTY: &str = "tests/samples/pcblib/Empty.PcbLib"; +const PCBLIB_SIMPLE: &str = "tests/samples/pcblib/Simple.PcbLib"; +const ALL_PCBLIBS: &[&str] = &[PCBLIB_EMPTY,PCBLIB_SIMPLE]; + +#[test] +fn test_parse() { + for path in ALL_PCBLIBS { + let _pcblib = PcbLib::open(path).unwrap(); + } +} diff --git a/book/PCB.md b/book/PCB.md new file mode 100644 index 0000000..b377a50 --- /dev/null +++ b/book/PCB.md @@ -0,0 +1,298 @@ +# `PCBDoc` and `PCBLib` + +## `PcbLib` + +```text +/ + Component Name/ <-- one entry per component + Data + Header + Parameters + WideStrings + UniqueIDPrimitiveInformation <-- optional + FileVersionInfo/ + Data + Header + Library/ + ComponentParamsTOC/ + Data + Header + LayerKindMapping/ + Data + Header + Models/ + Data + Header + 0 <-- Numbered streams are optional + 1 + ModelsNoEmbed/ + Data + Header + PadViaLibrary/ + Data + Header + Textures/ + Data + Header + Data + EmbeddedFonts + Header + FileHeader +``` + +### `FileHeader` + +Starts with a 4 byte length, then the same length (?) followed by a header, +followed by something meaningless. Seems to always be the same. + +```text +Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + +00000000 1B 00 00 00 1B 50 43 42 20 36 2E 30 20 42 69 6E .....PCB 6.0 Bin +00000010 61 72 79 20 4C 69 62 72 61 72 79 20 46 69 6C 65 ary Library File +00000020 0A D7 A3 70 3D 0A 14 40 08 00 00 00 08 4E 46 52 ...p=..@.....NFR +00000030 51 52 54 4C 4E QRTLN +``` + +### `Library` + +This seems to contain metadata about the entire PcbLib + +#### `ComponentParamsTOC` + +`Header` is empty, `Data` 4 byte length followed by the contents. Contents seems +to be a KV map of basic information for each component, with a CRLF separator +between components (0x0d 0x0a). We can probably use this to extract an overview +of what this library contains. + +```text +Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + +00000000 BD 04 00 00 4E 61 6D 65 3D 46 6F 75 72 20 70 61 ....Name=Four pa +00000010 64 73 7C 50 61 64 20 43 6F 75 6E 74 3D 34 7C 48 ds|Pad Count=4|H +00000020 65 69 67 68 74 3D 33 39 2E 33 37 30 31 7C 44 65 eight=39.3701|De +00000030 73 63 72 69 70 74 69 6F 6E 3D 46 6F 75 72 20 70 scription=Four p +00000040 61 64 73 20 6F 66 20 64 69 66 66 65 72 65 6E 74 ads of different +00000050 20 74 79 70 65 73 0D 0A 4E 61 6D 65 3D 46 6F 6F types..Name=Foo +00000060 74 70 72 69 6E 74 20 32 32 7C 50 61 64 20 43 6F tprint 22|Pad Co +00000070 75 6E 74 3D 31 7C 48 65 69 67 68 74 3D 33 39 2E unt=1|Height=39. +00000080 33 37 30 31 7C 44 65 73 63 72 69 70 74 69 6F 6E 3701|Description +00000090 3D 46 6F 6F 74 70 72 69 6E 74 20 32 0D 0A 4E 61 =Footprint 2..Na +000000A0 6D 65 3D 53 69 6E 67 6C 65 20 50 61 64 20 28 30 me=Single Pad (0 +000000B0 2C 30 29 7C 50 61 64 20 43 6F 75 6E 74 3D 31 7C ,0)|Pad Count=1| +000000C0 48 65 69 67 68 74 3D 33 39 2E 33 37 30 31 7C 44 Height=39.3701|D +000000D0 65 73 63 72 69 70 74 69 6F 6E 3D 50 61 64 20 61 escription=Pad a +000000E0 74 20 28 30 2C 30 29 20 64 69 6D 73 20 31 2E 35 t (0,0) dims 1.5 +000000F0 78 31 2E 35 20 6D 6D 0D 0A 4E 61 6D 65 3D 43 41 x1.5 mm..Name=CA +00000100 50 43 31 36 30 38 58 30 39 4C 7C 50 61 64 20 43 PC1608X09L|Pad C +00000110 6F 75 6E 74 3D 32 7C 48 65 69 67 68 74 3D 33 35 ount=2|Height=35 +00000120 2E 34 33 33 31 7C 44 65 73 63 72 69 70 74 69 6F .4331|Descriptio +00000130 6E 3D 43 68 69 70 20 43 61 70 61 63 69 74 6F 72 n=Chip Capacitor +00000140 2C 20 32 2D 4C 65 61 64 73 2C 20 42 6F 64 79 20 , 2-Leads, Body +00000150 31 2E 35 35 78 30 2E 38 30 6D 6D 2C 20 49 50 43 1.55x0.80mm, IPC +``` + +#### `LayerKindMapping` + +`Header` is empty, `Data` is binary. + +```text +Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + +00000000 08 00 00 00 31 00 2E 00 30 00 00 00 3E DB A8 F4 ....1...0...>... +00000010 02 00 00 00 48 00 00 00 0C 00 00 00 47 00 00 00 ....H.......G... +00000020 0B 00 00 00 .... +``` + +#### `Models` + +Header is empty (well, this sample starts with 0x02 while others start with +0x01...). `Data` contains repeating KV maps (u32 length, null-terminated string) +that describe each model (presumably only embedded?). Seems to contain model +metadata including a v4 UUID. I wonder if there may be one entry per use rather +than one entry per model, since rotation is described here. + +``` +Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + +00000000 9C 00 00 00 45 4D 42 45 44 3D 54 52 55 45 7C 4D ....EMBED=TRUE|M +00000010 4F 44 45 4C 53 4F 55 52 43 45 3D 55 6E 64 65 66 ODELSOURCE=Undef +00000020 69 6E 65 64 7C 49 44 3D 7B 30 42 39 38 44 30 35 ined|ID={0B98D05 +00000030 43 2D 35 34 36 41 2D 34 36 41 42 2D 41 42 34 35 C-546A-46AB-AB45 +00000040 2D 45 39 32 31 42 46 45 39 36 32 44 43 7D 7C 52 -E921BFE962DC}|R +00000050 4F 54 58 3D 30 2E 30 30 30 7C 52 4F 54 59 3D 30 OTX=0.000|ROTY=0 +00000060 2E 30 30 30 7C 52 4F 54 5A 3D 30 2E 30 30 30 7C .000|ROTZ=0.000| +00000070 44 5A 3D 30 7C 43 48 45 43 4B 53 55 4D 3D 31 36 DZ=0|CHECKSUM=16 +00000080 38 37 30 32 34 33 37 31 7C 4E 41 4D 45 3D 43 41 87024371|NAME=CA +00000090 50 43 31 36 30 38 58 30 39 4C 2E 73 74 65 70 00 PC1608X09L.step. +000000A0 AA 00 00 00 45 4D 42 45 44 3D 54 52 55 45 7C 4D ....EMBED=TRUE|M +000000B0 4F 44 45 4C 53 4F 55 52 43 45 3D 55 6E 64 65 66 ODELSOURCE=Undef +000000C0 69 6E 65 64 7C 49 44 3D 7B 45 39 45 37 32 32 35 ined|ID={E9E7225 +000000D0 39 2D 45 30 45 39 2D 34 31 38 46 2D 41 38 38 42 9-E0E9-418F-A88B +000000E0 2D 32 36 44 30 46 30 34 35 46 31 44 44 7D 7C 52 -26D0F045F1DD}|R +000000F0 4F 54 58 3D 30 2E 30 30 30 7C 52 4F 54 59 3D 30 OTX=0.000|ROTY=0 +00000100 2E 30 30 30 7C 52 4F 54 5A 3D 30 2E 30 30 30 7C .000|ROTZ=0.000| +00000110 44 5A 3D 30 7C 43 48 45 43 4B 53 55 4D 3D 2D 31 DZ=0|CHECKSUM=-1 +00000120 39 38 33 36 31 33 30 35 33 7C 4E 41 4D 45 3D 53 983613053|NAME=S +00000130 51 46 50 35 30 50 38 30 30 58 38 30 30 58 33 30 QFP50P800X800X30 +00000140 30 5F 48 53 2D 33 33 4E 2E 73 74 65 70 00 0_HS-33N.step. +``` + +This storage also contains numbered files (`0`, `1`, etc) that likely line up +with this header. These are zlib-compressed step files with the header +`ISO-10303-21` (maybe other types are allowed?), which can be extracted with +this one liner: + +```sh +python -c 'import zlib, sys; sys.stdout.buffer.write(zlib.decompress(open("path/1", "rb").read()))' + +# or, same thing, gunzip just needs this header +printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - path/1 | gunzip +``` + +#### `ModelsNoEmbed` + +Both `Data` and `Header` seem empty when unused. + +#### `PadViaLibrary` + +`Data` has information, `Header` is just null u32. + +```text +Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + +00000000 7F 00 00 00 7C 50 41 44 56 49 41 4C 49 42 52 41 ....|PADVIALIBRA +00000010 52 59 2E 4C 49 42 52 41 52 59 49 44 3D 7B 45 38 RY.LIBRARYID={E8 +00000020 31 31 30 31 30 32 2D 43 41 36 36 2D 34 30 39 31 110102-CA66-4091 +00000030 2D 42 46 33 37 2D 39 32 31 31 42 34 44 31 36 43 -BF37-9211B4D16C +00000040 41 33 7D 7C 50 41 44 56 49 41 4C 49 42 52 41 52 A3}|PADVIALIBRAR +00000050 59 2E 4C 49 42 52 41 52 59 4E 41 4D 45 3D 3C 4C Y.LIBRARYNAME=|PADVIALIBR +00000070 41 52 59 2E 44 49 53 50 4C 41 59 55 4E 49 54 53 ARY.DISPLAYUNITS +00000080 3D 31 00 =1. +``` + +#### `Textures` + +`Data` is empty when unused, `Header` is a null `u32`. Not sure what this is +used for. + +#### `Data` + +The top-level `Data` seems to contain the library stackup and some metadata. + + +```text +Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + +00000000 1D 72 01 00 7C 46 49 4C 45 4E 41 4D 45 3D 43 3A .r..|FILENAME=C: +00000010 5C 55 73 65 72 73 5C 74 67 72 6F 73 73 5C 44 6F \Users\tgross\Do +00000020 63 75 6D 65 6E 74 73 5C 50 72 6F 67 72 61 6D 6D cuments\Programm +00000030 69 6E 67 5C 50 79 41 6C 74 69 75 6D 5C 74 65 73 ing\PyAltium\tes +00000040 74 5C 66 69 6C 65 73 5C 50 63 62 4C 69 62 31 2E t\files\PcbLib1. +00000050 24 24 24 7C 4B 49 4E 44 3D 50 72 6F 74 65 6C 5F $$$|KIND=Protel_ +00000060 41 64 76 61 6E 63 65 64 5F 50 43 42 5F 4C 69 62 Advanced_PCB_Lib +00000070 72 61 72 79 7C 56 45 52 53 49 4F 4E 3D 33 2E 30 rary|VERSION=3.0 +00000080 30 7C 44 41 54 45 3D 32 30 32 32 2D 30 31 2D 30 0|DATE=2022-01-0 +00000090 36 7C 54 49 4D 45 3D 32 31 3A 33 36 3A 30 39 7C 6|TIME=21:36:09| +000000A0 56 39 5F 4D 41 53 54 45 52 53 54 41 43 4B 5F 53 V9_MASTERSTACK_S +000000B0 54 59 4C 45 3D 30 7C 56 39 5F 4D 41 53 54 45 52 TYLE=0|V9_MASTER +000000C0 53 54 41 43 4B 5F 49 44 3D 7B 38 44 38 46 44 35 STACK_ID={8D8FD5 +000000D0 42 39 2D 32 36 38 38 2D 34 45 31 30 2D 42 42 36 B9-2688-4E10-BB6 +000000E0 32 2D 42 44 46 30 39 46 42 43 32 33 46 46 7D 7C 2-BDF09FBC23FF}| +000000F0 56 39 5F 4D 41 53 54 45 52 53 54 41 43 4B 5F 4E V9_MASTERSTACK_N +00000100 41 4D 45 3D 4D 61 73 74 65 72 20 6C 61 79 65 72 AME=Master layer +00000110 20 73 74 61 63 6B 7C 56 39 5F 4D 41 53 54 45 52 stack|V9_MASTER +00000120 53 54 41 43 4B 5F 53 48 4F 57 54 4F 50 44 49 45 STACK_SHOWTOPDIE +00000130 4C 45 43 54 52 49 43 3D 46 41 4C 53 45 7C 56 39 LECTRIC=FALSE|V9 +00000140 5F 4D 41 53 54 45 52 53 54 41 43 4B 5F 53 48 4F _MASTERSTACK_SHO +00000150 57 42 4F 54 54 4F 4D 44 49 45 4C 45 43 54 52 49 WBOTTOMDIELECTRI +00000160 43 3D 46 41 4C 53 45 7C 56 39 5F 4D 41 53 54 45 C=FALSE|V9_MASTE +00000170 52 53 54 41 43 4B 5F 49 53 46 4C 45 58 3D 46 41 RSTACK_ISFLEX=FA +``` + +#### `EmbeddedFonts` + +Empty if unused + +### Component + +#### `Data` + +A bunch of binary representing the component, this will be fun. + +It does seem to start with the component name - perhaps this is more reliable +than using the storage name? + +#### `Header` + +Has a nonzero `u32`, not sure what this is for. + +#### `Parameters` + +Standard KV with basic metadata. + +```text +Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + +00000000 88 00 00 00 7C 50 41 54 54 45 52 4E 3D 43 41 50 ....|PATTERN=CAP +00000010 43 31 36 30 38 58 30 39 4C 7C 48 45 49 47 48 54 C1608X09L|HEIGHT +00000020 3D 33 35 2E 34 33 33 31 6D 69 6C 7C 44 45 53 43 =35.4331mil|DESC +00000030 52 49 50 54 49 4F 4E 3D 43 68 69 70 20 43 61 70 RIPTION=Chip Cap +00000040 61 63 69 74 6F 72 2C 20 32 2D 4C 65 61 64 73 2C acitor, 2-Leads, +00000050 20 42 6F 64 79 20 31 2E 35 35 78 30 2E 38 30 6D Body 1.55x0.80m +00000060 6D 2C 20 49 50 43 20 48 69 67 68 20 44 65 6E 73 m, IPC High Dens +00000070 69 74 79 7C 49 54 45 4D 47 55 49 44 3D 7C 52 45 ity|ITEMGUID=|RE +00000080 56 49 53 49 4F 4E 47 55 49 44 3D 00 VISIONGUID=. +``` + +#### `WideStrings` + +Empty on mine, maybe used for something with UTF-16? + +#### `UniqueIDPrimitiveInformation` + +Not sure what `Data` is, `Header` is empty + +```text +Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + +00000000 3A 00 00 00 7C 50 52 49 4D 49 54 49 56 45 49 4E :...|PRIMITIVEIN +00000010 44 45 58 3D 30 7C 50 52 49 4D 49 54 49 56 45 4F DEX=0|PRIMITIVEO +00000020 42 4A 45 43 54 49 44 3D 50 61 64 7C 55 4E 49 51 BJECTID=Pad|UNIQ +00000030 55 45 49 44 3D 49 44 58 4F 44 52 4E 45 00 UEID=IDXODRNE. +``` + +### `Header` + +Empty, not sure what this is for + +### `FileVersionInfo` + +Seems to contain a message to display for each version compatibility match +(COUNT is the total number of versions). The messages are strings encoded to +ASCII, then those numbers are written as ASCII themselves to the blob (like... +why?). + +Just pop it into python `bytearray([ ])` to print the value + +```text +Hex View 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + +00000000 09 0A 00 00 7C 43 4F 55 4E 54 3D 35 7C 56 45 52 ....|COUNT=5|VER +00000010 30 3D 38 37 2C 31 30 35 2C 31 31 30 2C 31 31 36 0=87,105,110,116 +00000020 2C 31 30 31 2C 31 31 34 2C 33 32 2C 34 38 2C 35 ,101,114,32,48,5 +00000030 37 7C 46 57 44 4D 53 47 30 3D 7C 42 4B 4D 53 47 7|FWDMSG0=|BKMSG +00000040 30 3D 36 30 2C 39 38 2C 36 32 2C 36 37 2C 36 35 0=60,98,62,67,65 +00000050 2C 38 35 2C 38 34 2C 37 33 2C 37 39 2C 37 38 2C ,85,84,73,79,78, +00000060 36 30 2C 34 37 2C 39 38 2C 36 32 2C 33 32 2C 34 60,47,98,62,32,4 +00000070 35 2C 33 32 2C 38 36 2C 31 30 35 2C 39 37 2C 31 5,32,86,105,97,1 +00000080 31 35 2C 33 32 2C 31 31 35 2C 31 31 37 2C 31 31 15,32,115,117,11 +00000090 32 2C 31 31 32 2C 31 31 31 2C 31 31 34 2C 31 31 2,112,111,114,11 +000000A0 36 2C 33 32 2C 31 31 38 2C 39 37 2C 31 31 34 2C 6,32,118,97,114, +000000B0 31 32 31 2C 31 30 35 2C 31 31 30 2C 31 30 33 2C 121,105,110,103, +000000C0 33 32 2C 31 30 30 2C 31 30 35 2C 39 37 2C 31 30 32,100,105,97,10 +000000D0 39 2C 31 30 31 2C 31 31 36 2C 31 30 31 2C 31 31 9,101,116,101,11 +000000E0 34 2C 31 31 35 2C 33 32 2C 39 37 2C 39 39 2C 31 4,115,32,97,99,1 +000000F0 31 34 2C 31 31 31 2C 31 31 35 2C 31 31 35 2C 33 14,111,115,115,3 +``` diff --git a/book/PcbLib.md b/book/PcbLib.md deleted file mode 100644 index 7f742ce..0000000 --- a/book/PcbLib.md +++ /dev/null @@ -1,15 +0,0 @@ - -Structure: - -``` - - -``` - -# Models - -ISO-10303-21 STEP file - -```sh -printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - fname | gunzip -```