1use serde::Serialize;
8
9pub type Matrix = [f64; 6];
10pub const IDENTITY: Matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
11
12#[derive(Clone, Debug, PartialEq, Serialize)]
13pub struct OuterShadow {
14 pub color: String,
15 pub opacity: f64,
16 pub blur_radius: f64,
17 pub distance: f64,
18 pub direction_degrees: f64,
19}
20
21#[derive(Clone, Debug, PartialEq, Serialize)]
22pub struct GlowEffect {
23 pub color: String,
24 pub opacity: f64,
25 pub radius: f64,
26}
27
28#[derive(Clone, Debug, PartialEq, Serialize)]
29#[serde(tag = "kind", rename_all = "snake_case")]
30pub enum ImageColorEffect {
31 Duotone {
32 dark: String,
33 light: String,
34 },
35 Grayscale,
36 Luminance {
37 brightness: f64,
38 contrast: f64,
39 },
40 ColorChange {
41 from: String,
42 to: String,
43 to_opacity: f64,
44 },
45}
46
47#[derive(Clone, Debug, Default, PartialEq, Serialize)]
48pub struct SourceMeta {
49 pub kind: String,
50 pub source_id: String,
51 pub semantic_role: String,
52 pub alt_text: String,
53 pub blend_mode: String,
54 pub mask_id: String,
55 pub image_rendering: String,
56 pub isolation: bool,
57 pub alpha_is_shape: bool,
58 pub shape_rendering: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub outer_shadow: Option<OuterShadow>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub glow: Option<GlowEffect>,
63 #[serde(skip_serializing_if = "Vec::is_empty")]
64 pub image_effects: Vec<ImageColorEffect>,
65}
66
67#[derive(Clone, Debug, Default, PartialEq, Serialize)]
68#[serde(tag = "kind", rename_all = "snake_case")]
69pub enum Paint {
70 #[default]
71 None,
72 Solid {
73 color: String,
74 opacity: f64,
75 },
76 LinearGradient(Box<LinearGradient>),
77 RadialGradient(Box<RadialGradient>),
78 PatternRef {
79 id: String,
80 opacity: f64,
81 },
82}
83
84impl Paint {
85 #[must_use]
86 pub fn solid(color: impl Into<String>) -> Self {
87 Self::Solid {
88 color: color.into(),
89 opacity: 1.0,
90 }
91 }
92}
93
94#[derive(Clone, Debug, PartialEq, Serialize)]
95pub struct GradientStop {
96 pub offset: f64,
97 pub color: String,
98 pub opacity: f64,
99}
100
101#[derive(Clone, Debug, PartialEq, Serialize)]
102pub struct LinearGradient {
103 pub x1: f64,
104 pub y1: f64,
105 pub x2: f64,
106 pub y2: f64,
107 pub stops: Vec<GradientStop>,
108}
109
110#[derive(Clone, Debug, PartialEq, Serialize)]
111pub struct RadialGradient {
112 pub fx: f64,
113 pub fy: f64,
114 pub fr: f64,
115 pub cx: f64,
116 pub cy: f64,
117 pub radius: f64,
118 pub transform: Matrix,
119 pub stops: Vec<GradientStop>,
120}
121
122#[derive(Clone, Debug, Default, PartialEq, Serialize)]
123pub struct Stroke {
124 pub paint: Paint,
125 pub width: f64,
126 pub line_cap: LineCap,
127 pub line_join: LineJoin,
128 pub miter_limit: f64,
129 pub dash_array: Vec<f64>,
130 pub dash_offset: f64,
131}
132
133#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
134#[serde(rename_all = "snake_case")]
135pub enum LineCap {
136 #[default]
137 Butt,
138 Round,
139 Square,
140}
141
142#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
143#[serde(rename_all = "snake_case")]
144pub enum LineJoin {
145 #[default]
146 Miter,
147 Round,
148 Bevel,
149}
150
151#[derive(Clone, Debug, PartialEq, Serialize)]
152pub struct TextRun {
153 pub text: String,
154 pub font_family: String,
155 pub font_size: f64,
156 pub bold: bool,
157 pub italic: bool,
158 pub fill: Paint,
159 pub baseline_shift: f64,
160 pub glyph_x_offsets: Vec<f64>,
166 pub target_advance: Option<f64>,
172}
173
174impl Default for TextRun {
175 fn default() -> Self {
176 Self {
177 text: String::new(),
178 font_family: "Arial, 'Hiragino Sans', 'Yu Gothic', sans-serif".into(),
179 font_size: 12.0,
180 bold: false,
181 italic: false,
182 fill: Paint::solid("#000000"),
183 baseline_shift: 0.0,
184 glyph_x_offsets: Vec::new(),
185 target_advance: None,
186 }
187 }
188}
189
190#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
191#[serde(rename_all = "snake_case")]
192pub enum TextAnchor {
193 #[default]
194 Start,
195 Middle,
196 End,
197}
198
199#[derive(Clone, Debug, PartialEq, Serialize)]
200#[serde(tag = "kind", rename_all = "snake_case")]
201pub enum Node {
202 Path {
203 id: String,
204 d: String,
205 fill_rule: String,
206 fill: Paint,
207 stroke: Stroke,
208 transform: Matrix,
209 clip_id: Option<String>,
210 meta: SourceMeta,
211 },
212 Text {
213 id: String,
214 x: f64,
215 y: f64,
216 runs: Vec<TextRun>,
217 anchor: TextAnchor,
218 transform: Matrix,
219 opacity: f64,
220 stroke: Stroke,
221 clip_id: Option<String>,
222 meta: SourceMeta,
223 },
224 Image {
225 id: String,
226 href: String,
227 x: f64,
228 y: f64,
229 width: f64,
230 height: f64,
231 transform: Matrix,
232 opacity: f64,
233 clip_id: Option<String>,
234 meta: SourceMeta,
235 },
236 Group {
237 id: String,
238 nodes: Vec<Node>,
239 transform: Matrix,
240 opacity: f64,
241 clip_id: Option<String>,
242 meta: SourceMeta,
243 },
244}
245
246#[derive(Clone, Debug, PartialEq, Serialize)]
247pub struct ClipPath {
248 pub id: String,
249 pub d: String,
250 pub transform: Matrix,
251 pub fill_rule: String,
252 pub parent_id: Option<String>,
253 pub additional_paths: Vec<ClipMember>,
254}
255
256#[derive(Clone, Debug, PartialEq, Serialize)]
257pub struct ClipMember {
258 pub d: String,
259 pub transform: Matrix,
260 pub fill_rule: String,
261}
262
263#[derive(Clone, Debug, PartialEq, Serialize)]
264pub struct MaskDefinition {
265 pub id: String,
266 pub mask_type: String,
267 pub nodes: Vec<Node>,
268 pub transfer_values: Vec<f64>,
269}
270
271#[derive(Clone, Debug, PartialEq, Serialize)]
272pub struct TilingPatternDefinition {
273 pub id: String,
274 pub x: f64,
275 pub y: f64,
276 pub width: f64,
277 pub height: f64,
278 pub transform: Matrix,
279 pub nodes: Vec<Node>,
280}
281
282#[derive(Clone, Debug, PartialEq, Serialize)]
283pub struct Page {
284 pub number: usize,
285 pub width: f64,
286 pub height: f64,
287 pub source_format: String,
288 pub title: String,
289 pub description: String,
290 #[serde(skip_serializing_if = "Option::is_none")]
297 pub embedded_source: Option<String>,
298 pub nodes: Vec<Node>,
299 pub clips: Vec<ClipPath>,
300 pub masks: Vec<MaskDefinition>,
301 pub patterns: Vec<TilingPatternDefinition>,
302 pub warnings: Vec<String>,
303}
304
305impl Page {
306 #[must_use]
307 pub fn new(number: usize, width: f64, height: f64, source_format: &str) -> Self {
308 Self {
309 number,
310 width,
311 height,
312 source_format: source_format.into(),
313 title: String::new(),
314 description: String::new(),
315 embedded_source: None,
316 nodes: Vec::new(),
317 clips: Vec::new(),
318 masks: Vec::new(),
319 patterns: Vec::new(),
320 warnings: Vec::new(),
321 }
322 }
323
324 pub fn warn(&mut self, warning: impl Into<String>) {
325 let warning = warning.into();
326 if !self.warnings.contains(&warning) {
327 self.warnings.push(warning);
328 }
329 }
330}
331
332#[must_use]
333pub fn compose(left: Matrix, right: Matrix) -> Matrix {
334 let [la, lb, lc, ld, le, lf] = left;
335 let [ra, rb, rc, rd, re, rf] = right;
336 [
337 la * ra + lc * rb,
338 lb * ra + ld * rb,
339 la * rc + lc * rd,
340 lb * rc + ld * rd,
341 la * re + lc * rf + le,
342 lb * re + ld * rf + lf,
343 ]
344}
345
346#[must_use]
347pub fn transform_point(matrix: Matrix, x: f64, y: f64) -> (f64, f64) {
348 let [a, b, c, d, e, f] = matrix;
349 (a * x + c * y + e, b * x + d * y + f)
350}