Skip to content

Commit f527f11

Browse files
committed
feat: 自动抠出阻焊区域
1 parent 59868c4 commit f527f11

File tree

8 files changed

+708
-403
lines changed

8 files changed

+708
-403
lines changed

src/colorful/encrypt.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use aes_gcm::aead::generic_array::{typenum::U16, GenericArray};
2+
use aes_gcm::{
3+
aead::{Aead, KeyInit},
4+
aes::Aes128,
5+
AesGcm,
6+
};
7+
use anyhow::{anyhow, Context, Result};
8+
use rand::{rngs::OsRng, RngCore};
9+
use rsa::pkcs8::DecodePublicKey;
10+
use rsa::{Oaep, RsaPublicKey};
11+
use sha2::Sha256;
12+
use std::fs::File;
13+
use std::io::Write;
14+
use std::path::Path;
15+
16+
pub(crate) struct KeyMaterial {
17+
aes_key: [u8; 16],
18+
aes_iv: [u8; 16],
19+
enc_key: Vec<u8>,
20+
enc_iv: Vec<u8>,
21+
}
22+
23+
impl KeyMaterial {
24+
pub(crate) fn generate(public_key_pem: &str) -> Result<Self> {
25+
let mut aes_key = [0u8; 16];
26+
let mut aes_iv = [0u8; 16];
27+
OsRng.fill_bytes(&mut aes_key);
28+
OsRng.fill_bytes(&mut aes_iv);
29+
30+
let public_key = RsaPublicKey::from_public_key_pem(public_key_pem)
31+
.context("Invalid embedded RSA public key")?;
32+
let enc_key = public_key
33+
.encrypt(&mut OsRng, Oaep::new::<Sha256>(), &aes_key)
34+
.context("Encrypt AES key")?;
35+
let enc_iv = public_key
36+
.encrypt(&mut OsRng, Oaep::new::<Sha256>(), &aes_iv)
37+
.context("Encrypt AES IV")?;
38+
39+
Ok(Self {
40+
aes_key,
41+
aes_iv,
42+
enc_key,
43+
enc_iv,
44+
})
45+
}
46+
}
47+
48+
pub(crate) fn encrypt_and_write(
49+
svg: &str,
50+
key_material: &KeyMaterial,
51+
output: &Path,
52+
) -> Result<()> {
53+
// AES-128-GCM with 16-byte nonce to mirror the original script (WebCrypto)
54+
type Aes128Gcm16 = AesGcm<Aes128, U16>;
55+
let cipher = Aes128Gcm16::new_from_slice(&key_material.aes_key).context("Create AES cipher")?;
56+
let nonce = GenericArray::<u8, U16>::from_slice(&key_material.aes_iv);
57+
let ciphertext = cipher
58+
.encrypt(nonce, svg.as_bytes())
59+
.map_err(|e| anyhow!("Encrypt silkscreen SVG: {:?}", e))?;
60+
61+
let mut file = File::create(output).with_context(|| format!("Create {}", output.display()))?;
62+
file.write_all(&key_material.enc_key)
63+
.with_context(|| format!("Write AES key for {}", output.display()))?;
64+
file.write_all(&key_material.enc_iv)
65+
.with_context(|| format!("Write AES IV for {}", output.display()))?;
66+
file.write_all(&ciphertext)
67+
.with_context(|| format!("Write ciphertext for {}", output.display()))?;
68+
69+
Ok(())
70+
}

src/colorful/mask.rs

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
use super::types::{mm_to_mil_10, MaskPaths};
2+
use anyhow::{anyhow, Result};
3+
use gerber_parser::{gerber_types::*, parse};
4+
use std::io::{BufReader, Cursor};
5+
6+
fn coords_to_mm(coords: &Option<Coordinates>, last: (f64, f64), units: Unit) -> (f64, f64) {
7+
let mut x = last.0;
8+
let mut y = last.1;
9+
if let Some(coords) = coords {
10+
if let Some(cx) = coords.x {
11+
let mut val: f64 = cx.into();
12+
if matches!(units, Unit::Inches) {
13+
val *= 25.4;
14+
}
15+
x = val;
16+
}
17+
if let Some(cy) = coords.y {
18+
let mut val: f64 = cy.into();
19+
if matches!(units, Unit::Inches) {
20+
val *= 25.4;
21+
}
22+
y = val;
23+
}
24+
}
25+
(x, y)
26+
}
27+
28+
fn to_svg_space(x_mm: f64, y_mm: f64) -> (f64, f64) {
29+
(mm_to_mil_10(x_mm), -mm_to_mil_10(y_mm))
30+
}
31+
32+
fn rect_path(center: (f64, f64), w_mm: f64, h_mm: f64) -> String {
33+
let (cx, cy) = center;
34+
let hw = mm_to_mil_10(w_mm) / 2.0;
35+
let hh = mm_to_mil_10(h_mm) / 2.0;
36+
let x0 = cx - hw;
37+
let x1 = cx + hw;
38+
let y0 = cy - hh;
39+
let y1 = cy + hh;
40+
format!("M {x0} {y0} L {x1} {y0} {x1} {y1} {x0} {y1} {x0} {y0} ")
41+
}
42+
43+
fn path_from_region(points: &[(f64, f64)]) -> Option<String> {
44+
if points.is_empty() {
45+
return None;
46+
}
47+
let mut d = String::new();
48+
for (idx, (x, y)) in points.iter().enumerate() {
49+
if idx == 0 {
50+
d.push_str(&format!("M {} {} ", x, y));
51+
} else {
52+
d.push_str(&format!("L {} {} ", x, y));
53+
}
54+
}
55+
d.push('Z');
56+
Some(d)
57+
}
58+
59+
fn aperture_bbox(ap: &Aperture) -> Option<(f64, f64)> {
60+
match ap {
61+
Aperture::Circle(c) => Some((c.diameter, c.diameter)),
62+
Aperture::Rectangle(r) | Aperture::Obround(r) => Some((r.x, r.y)),
63+
Aperture::Macro(name, args) if name == "RoundRect" => {
64+
parse_round_rect_macro(args.as_deref())
65+
}
66+
_ => None,
67+
}
68+
}
69+
70+
fn parse_round_rect_macro(args: Option<&[MacroDecimal]>) -> Option<(f64, f64)> {
71+
let args = args?;
72+
if args.len() < 3 {
73+
return None;
74+
}
75+
// Skip radius (first value)
76+
let coords = &args[1..];
77+
let mut xs = Vec::new();
78+
let mut ys = Vec::new();
79+
for pair in coords.chunks(2) {
80+
if pair.len() == 2 {
81+
match (&pair[0], &pair[1]) {
82+
(MacroDecimal::Value(x), MacroDecimal::Value(y)) => {
83+
xs.push(x);
84+
ys.push(y);
85+
}
86+
_ => {}
87+
}
88+
}
89+
}
90+
if xs.is_empty() || ys.is_empty() {
91+
return None;
92+
}
93+
let w = xs.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(*b))
94+
- xs.iter().fold(f64::INFINITY, |a, &b| a.min(*b));
95+
let h = ys.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(*b))
96+
- ys.iter().fold(f64::INFINITY, |a, &b| a.min(*b));
97+
Some((w.abs(), h.abs()))
98+
}
99+
100+
pub fn parse_solder_mask(content: &str) -> Result<MaskPaths> {
101+
let reader = BufReader::new(Cursor::new(content));
102+
let doc = match parse(reader) {
103+
Ok(doc) => doc,
104+
Err((partial, err)) => {
105+
if partial.commands().is_empty() {
106+
return Err(anyhow!("Failed to parse solder mask: {err}"));
107+
}
108+
partial
109+
}
110+
};
111+
112+
let mut units = doc.units.unwrap_or(Unit::Millimeters);
113+
let mut current_aperture: Option<&Aperture> = None;
114+
let mut current_pos: (f64, f64) = (0.0, 0.0);
115+
let mut region_active = false;
116+
let mut region_points: Vec<(f64, f64)> = Vec::new();
117+
let mut shapes: MaskPaths = Vec::new();
118+
let mut interp_mode = InterpolationMode::Linear;
119+
120+
let apertures = &doc.apertures;
121+
122+
for command in doc.commands() {
123+
match command {
124+
Command::ExtendedCode(ExtendedCode::Unit(u)) => units = *u,
125+
Command::FunctionCode(FunctionCode::GCode(g)) => match g {
126+
GCode::InterpolationMode(m) => interp_mode = *m,
127+
GCode::RegionMode(on) => {
128+
if !on && region_active && region_points.len() > 1 {
129+
if let Some(d) = path_from_region(&region_points) {
130+
shapes.push(d);
131+
}
132+
region_points.clear();
133+
}
134+
region_active = *on;
135+
}
136+
_ => {}
137+
},
138+
Command::FunctionCode(FunctionCode::DCode(d)) => match d {
139+
DCode::SelectAperture(code) => current_aperture = apertures.get(code),
140+
DCode::Operation(op) => match op {
141+
Operation::Move(coords) => {
142+
current_pos = coords_to_mm(coords, current_pos, units);
143+
}
144+
Operation::Interpolate(coords, _) => {
145+
let next = coords_to_mm(coords, current_pos, units);
146+
if region_active {
147+
if region_points.is_empty() {
148+
let (sx, sy) = to_svg_space(current_pos.0, current_pos.1);
149+
region_points.push((sx, sy));
150+
}
151+
let (nx, ny) = to_svg_space(next.0, next.1);
152+
region_points.push((nx, ny));
153+
}
154+
current_pos = next;
155+
let _ = interp_mode; // currently unused (linearized)
156+
}
157+
Operation::Flash(coords) => {
158+
let pos_mm = coords_to_mm(coords, current_pos, units);
159+
let (cx, cy) = to_svg_space(pos_mm.0, pos_mm.1);
160+
let ap_ref = current_aperture.or_else(|| {
161+
apertures
162+
.iter()
163+
.max_by_key(|(code, _)| *code)
164+
.map(|(_, ap)| ap)
165+
});
166+
if let Some(ap) = ap_ref {
167+
if let Some((w, h)) = aperture_bbox(ap) {
168+
shapes.push(rect_path((cx, cy), w, h));
169+
}
170+
}
171+
current_pos = pos_mm;
172+
}
173+
},
174+
},
175+
_ => {}
176+
}
177+
}
178+
179+
// Close any pending region
180+
if region_active && region_points.len() > 1 {
181+
if let Some(d) = path_from_region(&region_points) {
182+
shapes.push(d);
183+
}
184+
}
185+
186+
Ok(shapes)
187+
}

src/colorful/mod.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//! Colorful silkscreen generation (FuckJLCColorfulSilkscreen port)
2+
//!
3+
//! Generates encrypted colorful silkscreen files for top/bottom layers
4+
//! based on board outline, user-specified images, and solder mask openings.
5+
6+
use crate::patterns::LayerType;
7+
use anyhow::{Context, Result};
8+
use std::fs;
9+
use std::path::{Path, PathBuf};
10+
11+
mod encrypt;
12+
pub mod mask;
13+
mod svg;
14+
mod types;
15+
16+
pub use mask::parse_solder_mask;
17+
use types::{compute_mark_points, load_image, MaskPaths};
18+
19+
const RSA_PUB_KEY: &str = r#"-----BEGIN PUBLIC KEY-----
20+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzPtuUqJecaR/wWtctGT8
21+
QuVslmDH3Ut3s8c1Ls4A+M9rwpeLjgDUqfcrSrTHBrl5k/dOeJEWMeNF7STWS5jo
22+
WZE0H60cvf2bhormC9S6CRwq4Lw0ua0YQMo66R/qCtLVa5w6WkaPCz4b0xaHWtej
23+
JH49C0T67rU2DkepXuMPpwNCflMU+WgEQioZEldUTD6gYpu2U5GrW4AE0AQiIo+j
24+
e7tgN8PlBMbMaEfu0LokZyth1ugfuLAgyogWnedAegQmPZzAUe36Sni94AsDlhxm
25+
mjFl+WQZzD3MclbEY6KQB5XL8zCR/J6pCUUwfHantLxY/gQi0XJG5hWWtDyH/fR2
26+
lwIDAQAB
27+
-----END PUBLIC KEY-----"#;
28+
29+
/// Inputs for colorful silkscreen generation
30+
#[derive(Debug, Clone)]
31+
pub struct ColorfulOptions {
32+
pub top_image: Option<PathBuf>,
33+
pub bottom_image: Option<PathBuf>,
34+
pub top_solder_mask: Option<PathBuf>,
35+
pub bottom_solder_mask: Option<PathBuf>,
36+
}
37+
38+
/// Generate colorful silkscreen encrypted outputs
39+
pub struct ColorfulSilkscreenGenerator {
40+
options: ColorfulOptions,
41+
}
42+
43+
impl ColorfulSilkscreenGenerator {
44+
pub fn new(options: ColorfulOptions) -> Self {
45+
Self { options }
46+
}
47+
48+
/// Generate colorful silkscreen files in the given output directory.
49+
/// Returns the written file paths with their logical layer types.
50+
pub fn generate(
51+
&self,
52+
outline_path: &Path,
53+
output_dir: &Path,
54+
) -> Result<Vec<(LayerType, PathBuf)>> {
55+
if self.options.top_image.is_none() && self.options.bottom_image.is_none() {
56+
return Ok(Vec::new());
57+
}
58+
59+
let outline_content = fs::read_to_string(outline_path)
60+
.with_context(|| format!("Read outline {}", outline_path.display()))?;
61+
let bounds = types::parse_outline_bounds(&outline_content)?;
62+
let mark_points = compute_mark_points(&bounds);
63+
64+
fs::create_dir_all(output_dir)
65+
.with_context(|| format!("Create output dir {}", output_dir.display()))?;
66+
67+
let key_material = encrypt::KeyMaterial::generate(RSA_PUB_KEY)?;
68+
let mut written: Vec<(LayerType, PathBuf)> = Vec::new();
69+
70+
if let Some(top_path) = &self.options.top_image {
71+
let image = load_image(top_path)?;
72+
let mask = load_mask_paths(self.options.top_solder_mask.as_deref())?;
73+
let svg = svg::build_top_svg(&bounds, &image, &mask);
74+
let target = output_dir.join("Fabrication_ColorfulTopSilkscreen.FCTS");
75+
encrypt::encrypt_and_write(&svg, &key_material, &target)?;
76+
written.push((LayerType::ColorfulTopSilkscreen, target));
77+
}
78+
79+
if let Some(bottom_path) = &self.options.bottom_image {
80+
let image = load_image(bottom_path)?;
81+
let mask = load_mask_paths(self.options.bottom_solder_mask.as_deref())?;
82+
let svg = svg::build_bottom_svg(&bounds, &image, &mask);
83+
let target = output_dir.join("Fabrication_ColorfulBottomSilkscreen.FCBS");
84+
encrypt::encrypt_and_write(&svg, &key_material, &target)?;
85+
written.push((LayerType::ColorfulBottomSilkscreen, target));
86+
}
87+
88+
// Colorful board outline layer (encrypted SVG)
89+
let outline_svg = svg::build_board_outline_svg(&bounds);
90+
let outline_target = output_dir.join("Fabrication_ColorfulBoardOutlineLayer.FCBO");
91+
encrypt::encrypt_and_write(&outline_svg, &key_material, &outline_target)?;
92+
written.push((LayerType::ColorfulBoardOutline, outline_target));
93+
94+
// Colorful board outline mark layer (plain Gerber)
95+
let mark_gerber = svg::build_outline_mark_gerber(&bounds, &mark_points);
96+
let mark_target = output_dir.join("Fabrication_ColorfulBoardOutlineMark.FCBM");
97+
fs::write(&mark_target, mark_gerber)
98+
.with_context(|| format!("Write {}", mark_target.display()))?;
99+
written.push((LayerType::ColorfulBoardOutlineMark, mark_target));
100+
101+
Ok(written)
102+
}
103+
}
104+
105+
fn load_mask_paths(path: Option<&Path>) -> Result<MaskPaths> {
106+
let Some(path) = path else {
107+
return Ok(Vec::new());
108+
};
109+
let content =
110+
fs::read_to_string(path).with_context(|| format!("Read solder mask {}", path.display()))?;
111+
mask::parse_solder_mask(&content)
112+
}

0 commit comments

Comments
 (0)