document-svg 2.0.2

Convert PDF, Word, Excel, PowerPoint, diagram and CAD files into one SVG per page, locally — a Rust library and the docsvg command
Documentation
//! SVG to Gerber RS-274X PCB artwork reverse converter.
//!
//! Converts SVG vector drawings (lines, rectangles, pads/circles, and closed paths)
//! into standard Extended Gerber RS-274X files for PCB manufacturing and CAM review.

use std::collections::HashMap;
use std::io::Write;

use crate::cad::svg_reader::{SvgElement, parse_svg_elements};
use crate::error::Result;

/// Converts SVG document content to standard Gerber RS-274X PCB artwork.
pub fn write_svg_to_gerber<W: Write>(svg_content: &str, mut writer: W) -> Result<()> {
    let doc = parse_svg_elements(svg_content)?;
    let view_height = doc.height;

    // Track flashes: (x, y, diameter_mm)
    let mut flashes: Vec<(f64, f64, f64)> = Vec::new();
    // Track lines/traces: (x1, y1, x2, y2)
    let mut traces: Vec<(f64, f64, f64, f64)> = Vec::new();
    // Track polygon fills: Vec<Vec<(f64, f64)>>
    let mut polygons: Vec<Vec<(f64, f64)>> = Vec::new();

    for elem in doc.elements {
        match elem {
            SvgElement::Line { p1, p2, .. } => {
                if p1.x.is_finite() && p1.y.is_finite() && p2.x.is_finite() && p2.y.is_finite() {
                    traces.push((p1.x, view_height - p1.y, p2.x, view_height - p2.y));
                }
            }
            SvgElement::Circle { center, radius, .. } => {
                if center.x.is_finite()
                    && center.y.is_finite()
                    && radius.is_finite()
                    && radius > 0.0
                {
                    flashes.push((center.x, view_height - center.y, radius * 2.0));
                }
            }
            SvgElement::Rect {
                x,
                y,
                width,
                height,
                ..
            } => {
                if x.is_finite()
                    && y.is_finite()
                    && width.is_finite()
                    && height.is_finite()
                    && width > 0.0
                    && height > 0.0
                {
                    let y0 = view_height - y;
                    let y1 = view_height - (y + height);
                    polygons.push(vec![
                        (x, y0),
                        (x + width, y0),
                        (x + width, y1),
                        (x, y1),
                        (x, y0),
                    ]);
                }
            }
            SvgElement::Polyline {
                points, is_closed, ..
            } => {
                let valid_pts: Vec<_> = points
                    .into_iter()
                    .filter(|p| p.x.is_finite() && p.y.is_finite())
                    .collect();
                if valid_pts.len() >= 2 {
                    if is_closed && valid_pts.len() >= 3 {
                        let mut poly: Vec<(f64, f64)> =
                            valid_pts.iter().map(|p| (p.x, view_height - p.y)).collect();
                        if let Some(&first) = poly.first() {
                            poly.push(first);
                        }
                        polygons.push(poly);
                    } else {
                        for w in valid_pts.windows(2) {
                            traces.push((
                                w[0].x,
                                view_height - w[0].y,
                                w[1].x,
                                view_height - w[1].y,
                            ));
                        }
                    }
                }
            }
            SvgElement::Text { .. } => {}
        }
    }

    // Assign aperture numbers: D10 for 0.2mm default traces
    // Round circle flashes into discrete apertures
    let mut diam_to_aperture: HashMap<u64, usize> = HashMap::new();
    let mut next_aperture = 11;

    for (_, _, diam) in &flashes {
        let key = (diam * 1000.0).round() as u64;
        diam_to_aperture.entry(key).or_insert_with(|| {
            let ap = next_aperture;
            next_aperture += 1;
            ap
        });
    }

    // Write Gerber header
    writeln!(writer, "G04 Gerber RS-274X generated by document-svg*")?;
    writeln!(writer, "%FSLAX25Y25*%")?; // Format: Leading zero omission, Absolute, 2.5 format
    writeln!(writer, "%MOMM*%")?; // Metric mode (millimeters)
    writeln!(writer, "%LPD*%")?; // Dark polarity
    writeln!(writer, "%ADD10C,0.20000*%")?; // Default trace aperture

    for (key, d_code) in &diam_to_aperture {
        let diam_mm = (*key as f64) / 1000.0;
        writeln!(writer, "%ADD{}C,{:.5}*%", d_code, diam_mm)?;
    }

    let fmt_g = |val: f64| -> i64 { (val * 100_000.0).round() as i64 };

    // Output traces
    if !traces.is_empty() {
        writeln!(writer, "D10*")?;
        for (x1, y1, x2, y2) in traces {
            writeln!(writer, "X{}Y{}D02*", fmt_g(x1), fmt_g(y1))?;
            writeln!(writer, "X{}Y{}D01*", fmt_g(x2), fmt_g(y2))?;
        }
    }

    // Output flashes (pads)
    for (x, y, diam) in flashes {
        let key = (diam * 1000.0).round() as u64;
        if let Some(ap) = diam_to_aperture.get(&key) {
            writeln!(writer, "D{}*", ap)?;
            writeln!(writer, "X{}Y{}D03*", fmt_g(x), fmt_g(y))?;
        }
    }

    // Output filled polygon regions (G36 / G37)
    for poly in polygons {
        if poly.len() < 3 {
            continue;
        }
        writeln!(writer, "G36*")?; // Begin polygon
        writeln!(writer, "X{}Y{}D02*", fmt_g(poly[0].0), fmt_g(poly[0].1))?;
        for pt in &poly[1..] {
            writeln!(writer, "X{}Y{}D01*", fmt_g(pt.0), fmt_g(pt.1))?;
        }
        writeln!(writer, "G37*")?; // End polygon
    }

    writeln!(writer, "M02*")?; // End of Gerber file
    Ok(())
}