chord_gen/
lib.rs

1use std::fs::File;
2use std::io::Write;
3use std::path::Path;
4use svg::{
5    svg_draw_barres, svg_draw_bg, svg_draw_finger, svg_draw_min_fret, svg_draw_note, svg_draw_title,
6};
7use tera::{Context as TeraContext, Tera};
8use types::{Chord, GuitarString, Hand};
9use utils::{get_filename, get_palette};
10
11mod svg;
12pub mod types;
13mod utils;
14
15fn generate_svg(chord_settings: Chord) -> std::result::Result<String, Box<dyn std::error::Error>> {
16    let string_space = 40;
17    let margin = 30;
18
19    let palette = get_palette(chord_settings.mode);
20
21    // var for switching between handedness
22    let total_strings = 5;
23
24    let mut fingers = "".to_string();
25    for (i, finger) in chord_settings.fingers.iter().enumerate() {
26        let string: GuitarString = if chord_settings.hand == Hand::Right {
27            i.into()
28        } else {
29            (total_strings - i).into()
30        };
31        fingers += &svg_draw_finger(finger, string, &string_space, &palette);
32    }
33
34    let lowest_fret: &i32 = chord_settings
35        .frets
36        .iter()
37        .filter(|fret| **fret > 0)
38        .min()
39        .unwrap_or(&0);
40
41    let show_nut = (chord_settings.frets.contains(&0) && lowest_fret < &3)
42        || chord_settings.frets.contains(&1);
43    let nut_width = if show_nut { 9 } else { 2 };
44    let nut_shape = if show_nut { "round" } else { "butt" };
45
46    let mut notes = "".to_string();
47    for (i, note) in chord_settings.frets.iter().enumerate() {
48        if note != &0 {
49            let string: GuitarString = if chord_settings.hand == Hand::Right {
50                i.into()
51            } else {
52                (total_strings - i).into()
53            };
54            notes += &svg_draw_note(note, string, &string_space, lowest_fret, &palette);
55        }
56    }
57
58    let mut min_fret_marker = "".to_string();
59    if *lowest_fret > 2 || *lowest_fret > 1 && !show_nut {
60        min_fret_marker = svg_draw_min_fret(lowest_fret, &string_space, &palette);
61    }
62
63    let chord_title = svg_draw_title(&chord_settings, &palette);
64    // if barre
65    // for each barre
66    let barres = match chord_settings.barres {
67        Some(barres) => svg_draw_barres(
68            &barres[0],
69            &chord_settings.frets,
70            &string_space,
71            &lowest_fret,
72            &palette,
73        ),
74        None => String::from(""),
75    };
76
77    let mut context = TeraContext::new();
78    context.insert("name", &chord_title);
79    context.insert("padding", &margin);
80    context.insert("nutWidth", &nut_width);
81    context.insert("nutShape", &nut_shape);
82    context.insert("fingers", &fingers);
83    context.insert("notes", &notes);
84    context.insert("minFret", &min_fret_marker);
85    context.insert("foreground", &palette.fg);
86    context.insert(
87        "background",
88        &svg_draw_bg(chord_settings.use_background, &palette),
89    );
90    context.insert("barres", &barres);
91
92    match Tera::one_off(include_str!("../templates/chord.svg"), &context, false) {
93        Ok(result) => Ok(result),
94        Err(e) => {
95            println!("{:?}", e);
96            Err(Box::new(e))
97        }
98    }
99}
100
101pub fn render_svg(
102    chord_settings: Chord,
103    output_dir: &str,
104) -> Result<u64, Box<dyn std::error::Error>> {
105    let hashed_title = get_filename(&chord_settings);
106
107    match generate_svg(chord_settings) {
108        Ok(result) => {
109            let path = Path::new(output_dir).join(format!("{}.svg", hashed_title));
110            let mut output = File::create(path)?;
111            write!(output, "{}", result)?;
112            Ok(hashed_title)
113        }
114
115        Err(e) => {
116            println!("Failed to create SVG: {:?}", e);
117            Err(e)
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use crate::{
125        generate_svg,
126        types::{Chord, Hand, Mode},
127    };
128
129    #[test]
130    fn should_render_svg_correctly() {
131        let title = String::from("Hendrix");
132        let chord = Chord {
133            title: Some(&title),
134            frets: vec![-1, 7, 6, 7, 8, -1],
135            fingers: vec!["x", "2", "1", "3", "4", "x"],
136            ..Default::default()
137        };
138        let image = generate_svg(chord);
139        let expected = std::fs::read_to_string("fixtures/8429847222939097413.svg")
140            .expect("couldn't open fixture");
141        assert_eq!(image.unwrap(), expected);
142
143        let title = String::from("E");
144        let chord = Chord {
145            title: Some(&title),
146            frets: vec![0, 2, 2, 1, 0, 0],
147            fingers: vec!["0", "2", "3", "1", "0", "0"],
148            ..Default::default()
149        };
150        let image = generate_svg(chord);
151        let expected = std::fs::read_to_string("fixtures/6264984114944231516.svg")
152            .expect("couldn't open fixture");
153        assert_eq!(image.unwrap(), expected);
154
155        let title = String::from("C");
156        let suffix = String::from("°7");
157        let chord = Chord {
158            title: Some(&title),
159            frets: vec![-1, 3, 4, 2, 3, -1],
160            suffix: Some(&suffix),
161            fingers: vec!["x", "2", "3", "1", "4", "x"],
162            ..Default::default()
163        };
164        let image = generate_svg(chord);
165        let expected = std::fs::read_to_string("fixtures/14747384251356703516.svg")
166            .expect("couldn't open fixture");
167        assert_eq!(image.unwrap(), expected);
168
169        let title = String::from("E9");
170        let chord = Chord {
171            title: Some(&title),
172            frets: vec![-1, 7, 6, 7, 7, 7],
173            fingers: vec!["x", "2", "1", "3", "3", "3"],
174            ..Default::default()
175        };
176        let image = generate_svg(chord);
177        let expected = std::fs::read_to_string("fixtures/1119813197946994512.svg")
178            .expect("couldn't open fixture");
179        assert_eq!(image.unwrap(), expected);
180
181        let title = String::from("D7");
182        let chord = Chord {
183            title: Some(&title),
184            frets: vec![10, 12, 10, 11, 10, 10],
185            fingers: vec!["1", "3", "1", "2", "1", "1"],
186            ..Default::default()
187        };
188        let image = generate_svg(chord);
189        let expected = std::fs::read_to_string("fixtures/14132601197530680953.svg")
190            .expect("couldn't open fixture");
191        assert_eq!(image.unwrap(), expected);
192
193        let title = String::from("Bond");
194        let chord = Chord {
195            title: Some(&title),
196            frets: vec![0, 10, 9, 8, 7, -1],
197            fingers: vec!["0", "4", "3", "2", "1", "x"],
198            hand: Hand::Right,
199            ..Default::default()
200        };
201        let image = generate_svg(chord);
202        let expected = std::fs::read_to_string("fixtures/14960314899480148130.svg")
203            .expect("couldn't open fixture");
204        assert_eq!(image.unwrap(), expected);
205    }
206
207    #[test]
208    fn should_render_barres() {
209        // B9 barre regression
210        let suffix = String::from("9");
211        let title = String::from("B");
212        let chord = Chord {
213            title: Some(&title),
214            frets: vec![-1, 9, 8, 9, 9, 9],
215            fingers: vec!["x", "2", "1", "3", "3", "3"],
216            suffix: Some(&suffix),
217            barres: Some(vec![9]),
218            ..Default::default()
219        };
220        let image = generate_svg(chord);
221        let expected = std::fs::read_to_string("fixtures/9333158008996547180.svg")
222            .expect("couldn't open fixture");
223        assert_eq!(image.unwrap(), expected);
224
225        let title = String::from("C");
226        let suffix = String::from("m");
227        let chord = Chord {
228            title: Some(&title),
229            suffix: Some(&suffix),
230            frets: vec![-1, 3, 5, 5, 4, 3],
231            fingers: vec!["x", "1", "3", "4", "2", "1"],
232            barres: Some(vec![3]),
233            ..Default::default()
234        };
235        let image = generate_svg(chord);
236        let expected = std::fs::read_to_string("fixtures/12230973133991337290.svg")
237            .expect("couldn't open fixture");
238        assert_eq!(image.unwrap(), expected);
239    }
240
241    #[test]
242    fn should_render_barre_at_nut() {
243        let title = String::from("F");
244        let chord = Chord {
245            title: Some(&title),
246            frets: vec![1, 3, 3, 2, 1, 1],
247            fingers: vec!["1", "3", "4", "2", "1", "1"],
248            barres: Some(vec![1]),
249            ..Default::default()
250        };
251        let image = generate_svg(chord);
252        let expected = std::fs::read_to_string("fixtures/18011157745197688127.svg")
253            .expect("couldn't open fixture");
254        assert_eq!(image.unwrap(), expected);
255    }
256
257    #[test]
258    fn should_not_show_nut_when_min_is_higher_than_3() {
259        // nut regression
260        let title = String::from("D");
261        let suffix = String::from("m69");
262        let chord = Chord {
263            title: Some(&title),
264            frets: vec![-1, 5, 3, 4, 5, 0],
265            fingers: vec!["x", "3", "1", "2", "4", "0"],
266            suffix: Some(&suffix),
267            ..Default::default()
268        };
269        let image = generate_svg(chord);
270        let expected = std::fs::read_to_string("fixtures/13801489451752273984.svg")
271            .expect("couldn't open fixture");
272        assert_eq!(image.unwrap(), expected);
273    }
274
275    #[test]
276    fn should_render_bg() {
277        // no bg rh
278        let title = String::from("E♭");
279        let suffix = String::from("7");
280
281        let chord = Chord {
282            title: Some(&title),
283            suffix: Some(&suffix),
284            frets: vec![-1, 6, 5, 6, -1, -1],
285            fingers: vec!["x", "2", "1", "3", "x", "x"],
286            ..Default::default()
287        };
288        let image = generate_svg(chord);
289        let expected = std::fs::read_to_string("fixtures/14385705171269355287.svg")
290            .expect("couldn't open fixture");
291        assert_eq!(image.unwrap(), expected);
292
293        // no bg lh
294        let chord = Chord {
295            title: Some(&title),
296            suffix: Some(&suffix),
297            frets: vec![-1, 6, 5, 6, -1, -1],
298            fingers: vec!["x", "2", "1", "3", "x", "x"],
299            hand: Hand::Left,
300            ..Default::default()
301        };
302        let image = generate_svg(chord);
303        let expected = std::fs::read_to_string("fixtures/16884747429378599218.svg")
304            .expect("couldn't open fixture");
305        assert_eq!(image.unwrap(), expected);
306
307        // bg rh
308        let chord = Chord {
309            title: Some(&title),
310            suffix: Some(&suffix),
311            frets: vec![-1, 6, 5, 6, -1, -1],
312            fingers: vec!["x", "2", "1", "3", "x", "x"],
313            use_background: true,
314            ..Default::default()
315        };
316        let image = generate_svg(chord);
317        let expected = std::fs::read_to_string("fixtures/2476955617190468140.svg")
318            .expect("couldn't open fixture");
319        assert_eq!(image.unwrap(), expected);
320
321        // bg lh
322        let chord = Chord {
323            title: Some(&title),
324            suffix: Some(&suffix),
325            frets: vec![-1, 6, 5, 6, -1, -1],
326            fingers: vec!["x", "2", "1", "3", "x", "x"],
327            hand: Hand::Left,
328            use_background: true,
329            ..Default::default()
330        };
331        let image = generate_svg(chord);
332        let expected = std::fs::read_to_string("fixtures/3472658043626440162.svg")
333            .expect("couldn't open fixture");
334        assert_eq!(image.unwrap(), expected);
335    }
336
337    #[test]
338    fn should_use_dark_mode() {
339        // light no bg rh
340        let title = String::from("E♭");
341        let suffix = String::from("7");
342
343        let chord = Chord {
344            title: Some(&title),
345            suffix: Some(&suffix),
346            frets: vec![-1, 6, 5, 6, -1, -1],
347            fingers: vec!["x", "2", "1", "3", "x", "x"],
348            ..Default::default()
349        };
350        let image = generate_svg(chord);
351        let expected = std::fs::read_to_string("fixtures/14385705171269355287.svg")
352            .expect("couldn't open fixture");
353        assert_eq!(image.unwrap(), expected);
354
355        // light bg rh
356        let chord = Chord {
357            title: Some(&title),
358            suffix: Some(&suffix),
359            frets: vec![-1, 6, 5, 6, -1, -1],
360            fingers: vec!["x", "2", "1", "3", "x", "x"],
361            use_background: true,
362            ..Default::default()
363        };
364        let image = generate_svg(chord);
365        let expected = std::fs::read_to_string("fixtures/2476955617190468140.svg")
366            .expect("couldn't open fixture");
367        assert_eq!(image.unwrap(), expected);
368
369        // dark no bg rh
370        let chord = Chord {
371            title: Some(&title),
372            suffix: Some(&suffix),
373            frets: vec![-1, 6, 5, 6, -1, -1],
374            fingers: vec!["x", "2", "1", "3", "x", "x"],
375            mode: Mode::Dark,
376            ..Default::default()
377        };
378        let image = generate_svg(chord);
379        let expected = std::fs::read_to_string("fixtures/13340061165425864902.svg")
380            .expect("couldn't open fixture");
381        assert_eq!(image.unwrap(), expected);
382
383        // dark bg rh
384        let chord = Chord {
385            title: Some(&title),
386            suffix: Some(&suffix),
387            frets: vec![-1, 6, 5, 6, -1, -1],
388            fingers: vec!["x", "2", "1", "3", "x", "x"],
389            use_background: true,
390            mode: Mode::Dark,
391            ..Default::default()
392        };
393        let image = generate_svg(chord);
394        let expected = std::fs::read_to_string("fixtures/1048205031866609166.svg")
395            .expect("couldn't open fixture");
396        assert_eq!(image.unwrap(), expected);
397    }
398
399    #[test]
400    fn should_render_left_handed() {
401        let title = String::from("A");
402        let chord = Chord {
403            title: Some(&title),
404            frets: vec![-1, 0, 2, 2, 2, 0],
405            fingers: vec!["x", "0", "2", "1", "3", "0"],
406            hand: Hand::Left,
407            ..Default::default()
408        };
409        let image = generate_svg(chord);
410        let expected = std::fs::read_to_string("fixtures/left/12943706944351374242.svg")
411            .expect("couldn't open fixture");
412        assert_eq!(image.unwrap(), expected);
413
414        let title = String::from("Hendrix");
415        let chord = Chord {
416            title: Some(&title),
417            frets: vec![-1, 7, 6, 7, 8, -1],
418            fingers: vec!["x", "2", "1", "3", "4", "x"],
419            hand: Hand::Left,
420            ..Default::default()
421        };
422        let image = generate_svg(chord);
423        let expected = std::fs::read_to_string("fixtures/left/12438538594686784945.svg")
424            .expect("couldn't open fixture");
425        assert_eq!(image.unwrap(), expected);
426    }
427}
428
429// ♭ \u266D
430// ♯ \u266F
431// natural ♮ \u266E
432// dim o U+E870
433// aug + U+E872 +