Skip to main content

oxihuman_export/
eps_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! EPS (Encapsulated PostScript) vector export stub.
6
7/// EPS document options.
8#[allow(dead_code)]
9pub struct EpsOptions {
10    pub title: String,
11    pub width_pt: f32,
12    pub height_pt: f32,
13    pub line_width: f32,
14}
15
16impl Default for EpsOptions {
17    fn default() -> Self {
18        EpsOptions {
19            title: "OxiHuman Export".to_string(),
20            width_pt: 595.0,
21            height_pt: 842.0,
22            line_width: 1.0,
23        }
24    }
25}
26
27/// A 2-D path in EPS coordinates.
28#[allow(dead_code)]
29pub struct EpsPath {
30    pub points: Vec<[f32; 2]>,
31    pub closed: bool,
32    pub stroke_rgb: [f32; 3],
33    pub fill_rgb: Option<[f32; 3]>,
34}
35
36/// EPS document accumulator.
37#[allow(dead_code)]
38pub struct EpsDocument {
39    pub options: EpsOptions,
40    pub paths: Vec<EpsPath>,
41}
42
43/// Create a new EPS document.
44#[allow(dead_code)]
45pub fn new_eps_document(options: EpsOptions) -> EpsDocument {
46    EpsDocument {
47        options,
48        paths: Vec::new(),
49    }
50}
51
52/// Add a path to the document.
53#[allow(dead_code)]
54pub fn add_eps_path(doc: &mut EpsDocument, path: EpsPath) {
55    doc.paths.push(path);
56}
57
58/// Serialize the EPS document to a string.
59#[allow(dead_code)]
60pub fn export_eps(doc: &EpsDocument) -> String {
61    let mut out = String::new();
62    out.push_str("%!PS-Adobe-3.0 EPSF-3.0\n");
63    out.push_str(&format!(
64        "%%BoundingBox: 0 0 {} {}\n",
65        doc.options.width_pt as i32, doc.options.height_pt as i32
66    ));
67    out.push_str(&format!("%%Title: {}\n", doc.options.title));
68    out.push_str("%%EndComments\n");
69    out.push_str(&format!("{} setlinewidth\n", doc.options.line_width));
70    for path in &doc.paths {
71        if path.points.is_empty() {
72            continue;
73        }
74        let [r, g, b] = path.stroke_rgb;
75        out.push_str(&format!("{:.4} {:.4} {:.4} setrgbcolor\n", r, g, b));
76        let p0 = path.points[0];
77        out.push_str(&format!("{:.4} {:.4} moveto\n", p0[0], p0[1]));
78        for &p in &path.points[1..] {
79            out.push_str(&format!("{:.4} {:.4} lineto\n", p[0], p[1]));
80        }
81        if path.closed {
82            out.push_str("closepath\n");
83            if let Some([fr, fg, fb]) = path.fill_rgb {
84                out.push_str("gsave\n");
85                out.push_str(&format!("{:.4} {:.4} {:.4} setrgbcolor\n", fr, fg, fb));
86                out.push_str("fill\ngrestore\n");
87            }
88        }
89        out.push_str("stroke\n");
90    }
91    out.push_str("%%EOF\n");
92    out
93}
94
95/// Path count.
96#[allow(dead_code)]
97pub fn eps_path_count(doc: &EpsDocument) -> usize {
98    doc.paths.len()
99}
100
101/// Compute the bounding box of all paths in the document.
102#[allow(dead_code)]
103pub fn eps_bounding_box(doc: &EpsDocument) -> ([f32; 2], [f32; 2]) {
104    let mut mn = [f32::INFINITY; 2];
105    let mut mx = [f32::NEG_INFINITY; 2];
106    for path in &doc.paths {
107        for &p in &path.points {
108            mn[0] = mn[0].min(p[0]);
109            mn[1] = mn[1].min(p[1]);
110            mx[0] = mx[0].max(p[0]);
111            mx[1] = mx[1].max(p[1]);
112        }
113    }
114    if mn[0].is_infinite() {
115        mn = [0.0; 2];
116        mx = [0.0; 2];
117    }
118    (mn, mx)
119}
120
121/// Convert mesh silhouette edges to EPS paths.
122#[allow(dead_code)]
123pub fn edges_to_eps_paths(
124    positions_2d: &[[f32; 2]],
125    edges: &[[u32; 2]],
126    stroke: [f32; 3],
127) -> Vec<EpsPath> {
128    edges
129        .iter()
130        .map(|&[a, b]| EpsPath {
131            points: vec![positions_2d[a as usize], positions_2d[b as usize]],
132            closed: false,
133            stroke_rgb: stroke,
134            fill_rgb: None,
135        })
136        .collect()
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    fn simple_path() -> EpsPath {
144        EpsPath {
145            points: vec![[0.0, 0.0], [100.0, 0.0], [100.0, 100.0]],
146            closed: false,
147            stroke_rgb: [0.0, 0.0, 0.0],
148            fill_rgb: None,
149        }
150    }
151
152    #[test]
153    fn eps_has_header() {
154        let doc = new_eps_document(EpsOptions::default());
155        let eps = export_eps(&doc);
156        assert!(eps.contains("%!PS-Adobe"));
157    }
158
159    #[test]
160    fn eps_has_bounding_box() {
161        let doc = new_eps_document(EpsOptions::default());
162        let eps = export_eps(&doc);
163        assert!(eps.contains("BoundingBox"));
164    }
165
166    #[test]
167    fn eps_has_eof() {
168        let doc = new_eps_document(EpsOptions::default());
169        let eps = export_eps(&doc);
170        assert!(eps.contains("%%EOF"));
171    }
172
173    #[test]
174    fn add_path_increases_count() {
175        let mut doc = new_eps_document(EpsOptions::default());
176        add_eps_path(&mut doc, simple_path());
177        assert_eq!(eps_path_count(&doc), 1);
178    }
179
180    #[test]
181    fn eps_contains_moveto() {
182        let mut doc = new_eps_document(EpsOptions::default());
183        add_eps_path(&mut doc, simple_path());
184        let eps = export_eps(&doc);
185        assert!(eps.contains("moveto"));
186    }
187
188    #[test]
189    fn eps_contains_lineto() {
190        let mut doc = new_eps_document(EpsOptions::default());
191        add_eps_path(&mut doc, simple_path());
192        let eps = export_eps(&doc);
193        assert!(eps.contains("lineto"));
194    }
195
196    #[test]
197    fn bounding_box_empty() {
198        let doc = new_eps_document(EpsOptions::default());
199        let (mn, mx) = eps_bounding_box(&doc);
200        assert_eq!(mn, [0.0, 0.0]);
201        assert_eq!(mx, [0.0, 0.0]);
202    }
203
204    #[test]
205    fn bounding_box_correct() {
206        let mut doc = new_eps_document(EpsOptions::default());
207        add_eps_path(&mut doc, simple_path());
208        let (mn, mx) = eps_bounding_box(&doc);
209        assert!((mn[0] - 0.0).abs() < 1e-5);
210        assert!((mx[0] - 100.0).abs() < 1e-5);
211    }
212
213    #[test]
214    fn edges_to_eps_paths_count() {
215        let pts = vec![[0.0f32, 0.0], [100.0, 0.0], [100.0, 100.0]];
216        let edges = vec![[0u32, 1], [1, 2]];
217        let paths = edges_to_eps_paths(&pts, &edges, [0.0, 0.0, 0.0]);
218        assert_eq!(paths.len(), 2);
219    }
220
221    #[test]
222    fn closed_path_has_closepath() {
223        let mut doc = new_eps_document(EpsOptions::default());
224        let p = EpsPath {
225            points: vec![[0.0, 0.0], [50.0, 0.0], [25.0, 50.0]],
226            closed: true,
227            stroke_rgb: [0.0, 0.0, 0.0],
228            fill_rgb: None,
229        };
230        add_eps_path(&mut doc, p);
231        let eps = export_eps(&doc);
232        assert!(eps.contains("closepath"));
233    }
234
235    #[test]
236    fn title_in_eps() {
237        let opts = EpsOptions {
238            title: "MyTest".to_string(),
239            ..Default::default()
240        };
241        let doc = new_eps_document(opts);
242        let eps = export_eps(&doc);
243        assert!(eps.contains("MyTest"));
244    }
245}