1mod annotations;
29mod beams;
30mod collision;
31mod geometry;
32mod glyphs;
33mod render;
34mod spans;
35mod tab_technique;
36mod tuplets;
37
38use acorde_core::{
39 HarpPedalDiagram, NoteAddr, NoteHead, ObjectStyleOverride, Score, ScoreView, StaffPresentation,
40 TextStyle, ValidationError, ViewStyle, ViewStyleOverride,
41};
42use acorde_layout::{
43 GlyphCollisionClass, GlyphCollisionDirection, LayoutConfig, LayoutResult, compute_layout,
44};
45use serde::{Deserialize, Serialize};
46use std::fmt;
47
48pub const SVG_CONTRACT_VERSION: u32 = 22;
50pub const GLYPH_COVERAGE_CONTRACT_VERSION: u32 = 3;
52pub const BUILTIN_GLYPH_RESOURCE_ID: &str = "acorde-vector-glyphs-v1";
54pub const TAB_METRICS_CONTRACT_VERSION: u32 = 1;
56pub const SVG_ANNOTATION_COLLISION_GAP_PX: f32 = 2.0;
58
59#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
61pub struct TabFretMetrics {
62 pub fret: u8,
63 pub digit_count: u8,
64 pub advance_units: f32,
66 pub side_gap_units: f32,
68}
69
70pub fn tab_fret_metrics(fret: u8) -> TabFretMetrics {
72 let digit_count = fret.to_string().len() as u8;
73 TabFretMetrics {
74 fret,
75 digit_count,
76 advance_units: f32::from(digit_count) * 0.48,
77 side_gap_units: 0.22,
78 }
79}
80
81pub fn percussion_notehead_resource_id(note_head: &NoteHead) -> &'static str {
86 match note_head {
87 NoteHead::Normal => "acorde-percussion-notehead-normal",
88 NoteHead::Diamond => "acorde-percussion-notehead-diamond",
89 NoteHead::X => "acorde-percussion-notehead-x",
90 NoteHead::Slash => "acorde-percussion-notehead-slash",
91 NoteHead::Cross => "acorde-percussion-notehead-cross",
92 NoteHead::Triangle => "acorde-percussion-notehead-triangle",
93 }
94}
95
96const MAX_RENDER_ANNOTATIONS: usize = 10_000;
97const MAX_ANNOTATION_TEXT_BYTES: usize = 16 * 1024;
98
99pub(crate) fn is_valid_xml_char(character: char) -> bool {
100 matches!(character, '\u{9}' | '\u{A}' | '\u{D}')
101 || ('\u{20}'..='\u{D7FF}').contains(&character)
102 || ('\u{E000}'..='\u{FFFD}').contains(&character)
103 || ('\u{10000}'..='\u{10FFFF}').contains(&character)
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
109#[serde(default)]
110pub struct SvgRenderOptions {
111 pub width: f32,
113 pub staff_size: f32,
116 pub measures_per_system: usize,
118 pub interactive: bool,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct RenderMetadata {
127 pub contract_version: u32,
129 pub width: f32,
130 pub height: f32,
131 pub part_count: usize,
132 pub staff_count: usize,
133 pub measure_count: usize,
134 pub note_count: usize,
135 #[serde(default)]
138 pub view: Option<RenderedViewMetadata>,
139 pub accessible_text: String,
141 pub address_bounds: Vec<AddressBounds>,
142 #[serde(default)]
144 pub text_annotations: Vec<TextAnnotation>,
145 #[serde(default)]
147 pub score_texts: Vec<ScoreTextMetadata>,
148 #[serde(default)]
150 pub tablature_positions: Vec<TablaturePositionMetadata>,
151 #[serde(default)]
153 pub tablature_staves: Vec<TablatureStaffMetadata>,
154 #[serde(default)]
156 pub tablature_changes: Vec<TablatureChangeMetadata>,
157 #[serde(default)]
159 pub harmony_ranges: Vec<HarmonyRangeMetadata>,
160 #[serde(default)]
162 pub tablature_technique_connections: Vec<TablatureTechniqueConnectionMetadata>,
163 #[serde(default)]
165 pub note_semantics: Vec<NoteSemanticMetadata>,
166 #[serde(default)]
168 pub object_style_overrides: Vec<ObjectStyleOverride>,
169 #[serde(default)]
171 pub harp_pedal_diagrams: Vec<HarpPedalDiagramMetadata>,
172 #[serde(default)]
174 pub section_breaks: Vec<usize>,
175 #[serde(default)]
177 pub staff_presentations: Vec<StaffPresentationMetadata>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
181pub struct StaffPresentationMetadata {
182 pub part: usize,
183 pub staff: usize,
184 pub presentation: StaffPresentation,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189pub struct RenderedViewMetadata {
190 pub id: String,
191 pub name: String,
192 pub measures_per_system: usize,
193 pub style: ViewStyle,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
198pub struct NoteSemanticMetadata {
199 pub part: usize,
200 pub staff: usize,
201 pub measure: usize,
202 pub voice: usize,
203 pub note: usize,
204 pub is_unpitched: bool,
205 #[serde(default)]
207 pub tie_start: bool,
208 #[serde(default)]
209 pub tie_end: bool,
210 #[serde(default)]
212 pub duration_beats: f64,
213 #[serde(default)]
215 pub pitch_midi_cents: Vec<i32>,
216 #[serde(default)]
218 pub offset_x: Option<f64>,
219 #[serde(default)]
220 pub offset_y: Option<f64>,
221 #[serde(default)]
222 pub relative_x: Option<f64>,
223 #[serde(default)]
224 pub relative_y: Option<f64>,
225 #[serde(default)]
226 pub dynamic: Option<String>,
227 #[serde(default)]
228 pub lyric: Option<String>,
229 #[serde(default)]
230 pub chord_label: Option<String>,
231 #[serde(default)]
232 pub technique_text: Option<String>,
233 #[serde(default)]
235 pub articulations: Vec<String>,
236 #[serde(default)]
238 pub guitar_technique: Option<acorde_core::GuitarTechnique>,
239 #[serde(default)]
241 pub guitar_bend_alter_cents: Option<i16>,
242 #[serde(default)]
244 pub guitar_bend_curve: Vec<acorde_core::GuitarBendPoint>,
245 #[serde(default)]
246 pub fingerings: Vec<u8>,
247 #[serde(default)]
248 pub instrument_id: Option<String>,
249 #[serde(default)]
251 pub microtone_cents: Vec<i16>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
256pub struct HarmonyRangeMetadata {
257 pub start: NoteAddr,
258 pub end: NoteAddr,
259 pub label: String,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
264pub struct TablaturePositionMetadata {
265 pub part: usize,
266 pub staff: usize,
267 pub measure: usize,
268 pub voice: usize,
269 pub note: usize,
270 pub position: usize,
271 pub string: u8,
272 pub fret: u8,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
277pub struct TablatureStaffMetadata {
278 pub part: usize,
279 pub staff: usize,
280 pub lines: u8,
281 #[serde(default)]
282 pub tuning_midi: Vec<i16>,
283 pub capo: u8,
284 #[serde(default)]
285 pub rhythm_display: acorde_core::TablatureRhythmDisplay,
286 #[serde(default)]
287 pub fret_mark_style: acorde_core::TablatureFretMarkStyle,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
292pub struct TablatureChangeMetadata {
293 pub part: usize,
294 pub staff: usize,
295 pub measure: usize,
296 pub lines: u8,
297 #[serde(default)]
298 pub tuning_midi: Vec<i16>,
299 pub capo: u8,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
304pub struct TablatureTechniqueConnectionMetadata {
305 pub start: NoteAddr,
306 pub end: NoteAddr,
307 pub technique: acorde_core::GuitarTechnique,
308 pub string: u8,
309 #[serde(default)]
311 pub cross_measure: bool,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
316pub struct TextAnnotation {
317 pub part: usize,
318 pub staff: usize,
319 pub measure: usize,
320 pub style: TextStyle,
321 pub text: String,
322 #[serde(default)]
323 pub placement: Option<String>,
324 #[serde(default)]
325 pub offset_x: Option<f64>,
326 #[serde(default)]
327 pub offset_y: Option<f64>,
328 #[serde(default)]
329 pub relative_x: Option<f64>,
330 #[serde(default)]
331 pub relative_y: Option<f64>,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
336pub struct HarpPedalDiagramMetadata {
337 pub part: usize,
338 pub staff: usize,
339 pub measure: usize,
340 pub diagram: HarpPedalDiagram,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
345pub struct ScoreTextMetadata {
346 pub style: TextStyle,
347 pub text: String,
348 #[serde(default)]
349 pub placement: Option<String>,
350 #[serde(default)]
351 pub offset_x: Option<f64>,
352 #[serde(default)]
353 pub offset_y: Option<f64>,
354 #[serde(default)]
355 pub relative_x: Option<f64>,
356 #[serde(default)]
357 pub relative_y: Option<f64>,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
362pub struct MicrotoneMarkerCoverage {
363 pub supported: bool,
365 pub representation: String,
367 pub exact_cents: bool,
369}
370
371impl Default for MicrotoneMarkerCoverage {
372 fn default() -> Self {
373 Self {
374 supported: true,
375 representation: "svg-text-cents".to_owned(),
376 exact_cents: true,
377 }
378 }
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
386pub struct GlyphCoverage {
387 pub contract_version: u32,
388 pub resource_id: String,
389 pub vector_glyphs: bool,
390 pub supported_clefs: Vec<String>,
391 pub accidental_min: i8,
392 pub accidental_max: i8,
393 #[serde(default)]
395 pub microtone_marker: MicrotoneMarkerCoverage,
396 #[serde(default)]
398 pub percussion_noteheads: Vec<String>,
399}
400
401pub fn glyph_coverage() -> GlyphCoverage {
403 GlyphCoverage {
404 contract_version: GLYPH_COVERAGE_CONTRACT_VERSION,
405 resource_id: BUILTIN_GLYPH_RESOURCE_ID.to_owned(),
406 vector_glyphs: true,
407 supported_clefs: vec![
408 "treble".to_owned(),
409 "bass".to_owned(),
410 "alto".to_owned(),
411 "tenor".to_owned(),
412 ],
413 accidental_min: -2,
414 accidental_max: 2,
415 microtone_marker: MicrotoneMarkerCoverage::default(),
416 percussion_noteheads: [
417 NoteHead::Normal,
418 NoteHead::Diamond,
419 NoteHead::X,
420 NoteHead::Slash,
421 NoteHead::Cross,
422 NoteHead::Triangle,
423 ]
424 .iter()
425 .map(|head| percussion_notehead_resource_id(head).to_owned())
426 .collect(),
427 }
428}
429
430#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
432pub enum RenderPreflightKind {
433 UnsupportedClef,
434 UnsupportedAccidental,
435 InvalidTabPosition,
436 MeasureTextTooLarge,
437 InvalidMeasureTextOffset,
438 InvalidNotePlacement,
439 InvalidXmlCharacter,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
444pub struct RenderPreflightIssue {
445 pub kind: RenderPreflightKind,
446 pub source_location: String,
447 pub preserved_value: String,
448}
449
450pub fn render_preflight(score: &Score) -> Vec<RenderPreflightIssue> {
452 let mut issues = Vec::new();
453 push_text_preflight_issue(&mut issues, "/score/title", &score.metadata.title);
454 for (part_index, part) in score.parts.iter().enumerate() {
455 push_text_preflight_issue(
456 &mut issues,
457 &format!("/score/part/{}/name", part_index + 1),
458 &part.name,
459 );
460 push_text_preflight_issue(
461 &mut issues,
462 &format!("/score/part/{}/short-name", part_index + 1),
463 &part.short_name,
464 );
465 for (staff_index, staff) in part.staves.iter().enumerate() {
466 let staff_path = format!("/score/part/{}/staff/{}", part_index + 1, staff_index + 1);
467 for (measure_index, measure) in staff.measures.iter().enumerate() {
468 for (text_index, styled) in measure.texts.iter().enumerate() {
469 let text_path = format!(
470 "{staff_path}/measure/{}/text/{}",
471 measure_index + 1,
472 text_index + 1
473 );
474 push_text_preflight_issue(&mut issues, &text_path, &styled.text);
475 for (field, value) in [
476 ("offset_x", styled.offset_x),
477 ("offset_y", styled.offset_y),
478 ("relative_x", styled.relative_x),
479 ("relative_y", styled.relative_y),
480 ] {
481 if let Some(value) = value {
482 if !value.is_finite() || !(value as f32).is_finite() {
483 issues.push(RenderPreflightIssue {
484 kind: RenderPreflightKind::InvalidMeasureTextOffset,
485 source_location: format!("{text_path}/{field}"),
486 preserved_value: value.to_string(),
487 });
488 }
489 }
490 }
491 }
492 for (field, value) in [
493 ("tempo-text", measure.tempo_text.as_deref()),
494 ("rehearsal", measure.rehearsal.as_deref()),
495 ("navigation", measure.navigation.as_deref()),
496 ("expression-text", measure.expression_text.as_deref()),
497 ] {
498 if let Some(value) = value {
499 push_text_preflight_issue(
500 &mut issues,
501 &format!("{staff_path}/measure/{}/{}", measure_index + 1, field),
502 value,
503 );
504 }
505 }
506 for (voice_index, voice) in measure.voices.iter().enumerate() {
507 for (note_index, note) in voice.iter().enumerate() {
508 let note_path = format!(
509 "{staff_path}/measure/{}/voice/{}/note/{}",
510 measure_index + 1,
511 voice_index + 1,
512 note_index + 1
513 );
514 if let Some(lyric) = ¬e.lyric {
515 push_text_preflight_issue(
516 &mut issues,
517 &format!("{note_path}/lyric"),
518 &lyric.text,
519 );
520 }
521 for entry in ¬e.additional_lyrics {
522 push_text_preflight_issue(
523 &mut issues,
524 &format!("{note_path}/lyric/{}", entry.verse),
525 &entry.lyric.text,
526 );
527 }
528 if let Some(technique) = ¬e.technique_text {
529 push_text_preflight_issue(
530 &mut issues,
531 &format!("{note_path}/technique-text"),
532 technique,
533 );
534 }
535 if let Some(chord) = ¬e.chord_symbol {
536 let chord_text = chord.display_text();
537 push_text_preflight_issue(
538 &mut issues,
539 &format!("{note_path}/chord-symbol"),
540 &chord_text,
541 );
542 }
543 for (field, value) in [
544 ("default-x", note.offset_x),
545 ("default-y", note.offset_y),
546 ("relative-x", note.relative_x),
547 ("relative-y", note.relative_y),
548 ] {
549 if let Some(value) = value {
550 if !value.is_finite() || !(value as f32).is_finite() {
551 issues.push(RenderPreflightIssue {
552 kind: RenderPreflightKind::InvalidNotePlacement,
553 source_location: format!("{note_path}/{field}"),
554 preserved_value: value.to_string(),
555 });
556 }
557 }
558 }
559 for (pitch_index, pitch) in note.pitches.iter().enumerate() {
560 if !(-2..=2).contains(&pitch.alter) {
561 issues.push(RenderPreflightIssue {
562 kind: RenderPreflightKind::UnsupportedAccidental,
563 source_location: format!(
564 "{note_path}/pitch/{}",
565 pitch_index + 1
566 ),
567 preserved_value: pitch.alter.to_string(),
568 });
569 }
570 }
571 if let Some(tab) = &staff.tablature {
572 let positions = if note.tab_positions.is_empty() {
573 note.tab_position.iter().collect::<Vec<_>>()
574 } else {
575 note.tab_positions.iter().collect::<Vec<_>>()
576 };
577 for position in positions {
578 if position.string == 0 || position.string > tab.lines {
579 issues.push(RenderPreflightIssue {
580 kind: RenderPreflightKind::InvalidTabPosition,
581 source_location: format!("{note_path}/tab-position"),
582 preserved_value: format!(
583 "string={},lines={}",
584 position.string, tab.lines
585 ),
586 });
587 }
588 }
589 }
590 }
591 }
592 }
593 }
594 }
595 issues
596}
597
598fn push_text_preflight_issue(
599 issues: &mut Vec<RenderPreflightIssue>,
600 source_location: &str,
601 text: &str,
602) {
603 if text.len() > MAX_ANNOTATION_TEXT_BYTES {
604 issues.push(RenderPreflightIssue {
605 kind: RenderPreflightKind::MeasureTextTooLarge,
606 source_location: source_location.to_owned(),
607 preserved_value: format!("bytes={}", text.len()),
608 });
609 }
610 if let Some(character) = text
611 .chars()
612 .find(|&character| !is_valid_xml_char(character))
613 {
614 issues.push(RenderPreflightIssue {
615 kind: RenderPreflightKind::InvalidXmlCharacter,
616 source_location: source_location.to_owned(),
617 preserved_value: format!("U+{:04X}", character as u32),
618 });
619 }
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct AddressBounds {
626 pub part: usize,
627 pub staff: usize,
628 pub measure: usize,
629 pub voice: usize,
630 pub note: usize,
631 pub x: f32,
632 pub y: f32,
633 pub width: f32,
634 pub height: f32,
635}
636
637#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
642pub struct SvgAnnotation {
643 pub id: String,
645 pub x: f32,
646 pub y: f32,
647 pub text: String,
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
655pub struct SvgAnnotationMetrics {
656 pub left_px: f32,
657 pub top_px: f32,
658 pub width_px: f32,
659 pub height_px: f32,
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
664pub struct SvgAnnotationCollisionPolicy {
665 pub class: GlyphCollisionClass,
666 pub direction: GlyphCollisionDirection,
667 pub priority: u8,
669}
670
671pub trait RenderAnnotation {
673 fn id(&self) -> &str;
675
676 fn annotate(
678 &self,
679 score: &Score,
680 layout: &LayoutResult,
681 metadata: &RenderMetadata,
682 ) -> Vec<SvgAnnotation>;
683
684 fn collision_policy(
690 &self,
691 _annotation: &SvgAnnotation,
692 ) -> Option<(SvgAnnotationMetrics, SvgAnnotationCollisionPolicy)> {
693 None
694 }
695}
696
697#[derive(Debug, Clone, PartialEq, Eq)]
699pub enum RenderAnnotationError {
700 EmptyProviderId,
701 DuplicateProviderId(String),
702 EmptyAnnotationId,
703 DuplicateAnnotationId(String),
704 NonFiniteCoordinate { id: String },
705 TooManyAnnotations { count: usize },
706 AnnotationTextTooLarge { id: String, size: usize },
707 InvalidXmlCharacter { id: String, codepoint: u32 },
708 InvalidCollisionMetrics { id: String },
709 CollisionResolution { id: String },
710}
711
712impl fmt::Display for RenderAnnotationError {
713 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
714 match self {
715 Self::EmptyProviderId => write!(f, "render annotation provider id is empty"),
716 Self::DuplicateProviderId(id) => {
717 write!(f, "duplicate render annotation provider: {id}")
718 }
719 Self::EmptyAnnotationId => write!(f, "render annotation id is empty"),
720 Self::DuplicateAnnotationId(id) => write!(f, "duplicate render annotation: {id}"),
721 Self::NonFiniteCoordinate { id } => {
722 write!(f, "render annotation {id} has a non-finite coordinate")
723 }
724 Self::TooManyAnnotations { count } => {
725 write!(f, "render annotation count exceeds limit: {count}")
726 }
727 Self::AnnotationTextTooLarge { id, size } => {
728 write!(f, "render annotation {id} text exceeds limit: {size} bytes")
729 }
730 Self::InvalidXmlCharacter { id, codepoint } => write!(
731 f,
732 "render annotation {id} contains invalid XML character U+{codepoint:04X}"
733 ),
734 Self::InvalidCollisionMetrics { id } => {
735 write!(f, "render annotation {id} has invalid collision metrics")
736 }
737 Self::CollisionResolution { id } => {
738 write!(f, "render annotation {id} could not be collision-resolved")
739 }
740 }
741 }
742}
743
744impl std::error::Error for RenderAnnotationError {}
745
746impl Default for SvgRenderOptions {
747 fn default() -> Self {
748 Self {
749 width: 900.0,
750 staff_size: 24.0,
751 measures_per_system: 4,
752 interactive: true,
753 }
754 }
755}
756
757#[derive(Debug, Clone, PartialEq, Eq)]
763pub enum RenderError {
764 EmptyScore,
766 InvalidRow { row: usize },
768 ViewResolution { view_id: String, reason: String },
770 InvalidLayout { reason: String },
772 InvalidOptions { reason: String },
774 UnsupportedClef,
776 UnsupportedAccidental { alter: i8 },
778 InvalidTabPosition { string: u8, lines: u8 },
780 TabMetricsOverflow,
782 MeasureTextTooLarge { size: usize },
784 InvalidMeasureTextOffset { field: &'static str },
786 InvalidNotePlacement { field: &'static str },
788 InvalidXmlCharacter { codepoint: u32 },
790 Annotation(RenderAnnotationError),
792}
793
794impl std::fmt::Display for RenderError {
795 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
796 match self {
797 RenderError::EmptyScore => write!(f, "score has no staves to render"),
798 RenderError::InvalidRow { row } => write!(f, "layout row {row} does not exist"),
799 RenderError::ViewResolution { view_id, reason } => {
800 write!(f, "cannot resolve score view '{view_id}': {reason}")
801 }
802 RenderError::InvalidLayout { reason } => write!(f, "invalid layout: {reason}"),
803 RenderError::InvalidOptions { reason } => write!(f, "invalid render options: {reason}"),
804 RenderError::UnsupportedClef => write!(
805 f,
806 "unsupported clef (no staff-position mapping is available)"
807 ),
808 RenderError::UnsupportedAccidental { alter } => {
809 write!(
810 f,
811 "unsupported accidental alter={alter} (supported range is -2..=2)"
812 )
813 }
814 RenderError::InvalidTabPosition { string, lines } => write!(
815 f,
816 "tablature string {string} is outside the owning staff line range 1..={lines}"
817 ),
818 RenderError::TabMetricsOverflow => write!(f, "tablature glyph metrics overflowed"),
819 RenderError::MeasureTextTooLarge { size } => write!(
820 f,
821 "measure-level text is too large ({size} bytes; maximum is {MAX_ANNOTATION_TEXT_BYTES})"
822 ),
823 RenderError::InvalidMeasureTextOffset { field } => {
824 write!(f, "measure-level text offset {field} is not finite")
825 }
826 RenderError::InvalidNotePlacement { field } => {
827 write!(f, "note placement offset {field} is not finite")
828 }
829 RenderError::InvalidXmlCharacter { codepoint } => write!(
830 f,
831 "score text contains invalid XML character U+{codepoint:04X}"
832 ),
833 RenderError::Annotation(error) => write!(f, "invalid render annotation: {error}"),
834 }
835 }
836}
837
838impl std::error::Error for RenderError {}
839
840pub fn render_svg(score: &Score, options: &SvgRenderOptions) -> Result<String, RenderError> {
847 let config = LayoutConfig {
848 measures_per_row: options.measures_per_system.max(1),
849 ..Default::default()
850 };
851 let layout = compute_layout(score, &config);
852 render_svg_with_layout(score, &layout, options)
853}
854
855pub fn render_svg_view(
864 score: &Score,
865 view_id: &str,
866 options: &SvgRenderOptions,
867) -> Result<String, RenderError> {
868 let view = score
869 .views
870 .iter()
871 .find(|view| view.id == view_id)
872 .ok_or_else(|| RenderError::ViewResolution {
873 view_id: view_id.to_owned(),
874 reason: "view does not exist".into(),
875 })?;
876 let projected = score
877 .resolve_view(view_id)
878 .map_err(|error| RenderError::ViewResolution {
879 view_id: view_id.to_owned(),
880 reason: error.to_string(),
881 })?;
882 let options = options_for_view(options, view)?;
883 render_svg(&projected, &options)
884}
885
886pub fn render_svg_view_metadata(
889 score: &Score,
890 view_id: &str,
891 options: &SvgRenderOptions,
892) -> Result<RenderMetadata, RenderError> {
893 let view = score
894 .views
895 .iter()
896 .find(|view| view.id == view_id)
897 .ok_or_else(|| RenderError::ViewResolution {
898 view_id: view_id.to_owned(),
899 reason: "view does not exist".into(),
900 })?;
901 let projected = score
902 .resolve_view(view_id)
903 .map_err(|error| RenderError::ViewResolution {
904 view_id: view_id.to_owned(),
905 reason: error.to_string(),
906 })?;
907 let options = options_for_view(options, view)?;
908 let layout = compute_layout(
909 &projected,
910 &LayoutConfig {
911 measures_per_row: options.measures_per_system,
912 ..Default::default()
913 },
914 );
915 let mut metadata = render_svg_metadata(&projected, &layout, &options)?;
916 metadata.view = Some(RenderedViewMetadata {
917 id: view.id.clone(),
918 name: view.name.clone(),
919 measures_per_system: options.measures_per_system,
920 style: score.resolved_view_style(&view.layout),
921 });
922 Ok(metadata)
923}
924
925fn options_for_view(
926 options: &SvgRenderOptions,
927 view: &ScoreView,
928) -> Result<SvgRenderOptions, RenderError> {
929 validate_style_overrides(&view.layout.typed_style_overrides)?;
930 let mut resolved = options.clone();
931 if let Some(measures_per_row) = view.layout.measures_per_row {
932 resolved.measures_per_system = measures_per_row;
933 }
934 resolved.staff_size *= view.layout.resolved_style().staff_space;
935 Ok(resolved)
936}
937
938fn options_for_score(
939 options: &SvgRenderOptions,
940 score: &Score,
941) -> Result<SvgRenderOptions, RenderError> {
942 validate_style_overrides(&score.style_overrides)?;
943 if acorde_core::validate(score)
944 .errors
945 .iter()
946 .any(|error| matches!(error, ValidationError::InvalidObjectStyleOverride { .. }))
947 {
948 return Err(RenderError::InvalidOptions {
949 reason: "object style overrides must target an existing object and use valid values and provenance".into(),
950 });
951 }
952 let mut resolved = options.clone();
953 resolved.staff_size *= score.resolved_view_style(&Default::default()).staff_space;
954 Ok(resolved)
955}
956
957fn validate_style_overrides(overrides: &[ViewStyleOverride]) -> Result<(), RenderError> {
958 if overrides
959 .iter()
960 .any(|override_| !override_.value.is_finite() || !(0.05..=64.0).contains(&override_.value))
961 {
962 return Err(RenderError::InvalidOptions {
963 reason: "typed style override values must be finite and within 0.05..=64".into(),
964 });
965 }
966 Ok(())
967}
968
969pub fn render_svg_with_layout(
974 score: &Score,
975 layout: &LayoutResult,
976 options: &SvgRenderOptions,
977) -> Result<String, RenderError> {
978 let options = options_for_score(options, score)?;
979 render::build_svg(score, layout, &options)
980}
981
982pub fn render_svg_with_annotations(
988 score: &Score,
989 layout: &LayoutResult,
990 options: &SvgRenderOptions,
991 providers: &[&dyn RenderAnnotation],
992) -> Result<String, RenderError> {
993 let options = options_for_score(options, score)?;
994 let (mut svg, metadata) = render::build_svg_with_metadata(score, layout, &options)?;
995 let annotations = render::collect_annotations(score, layout, &metadata, providers)
996 .map_err(RenderError::Annotation)?;
997 if annotations.is_empty() {
998 return Ok(svg);
999 }
1000 let body = annotations
1001 .iter()
1002 .map(|annotation| {
1003 format!(
1004 r#"<text class="acorde-render-annotation" data-acorde-kind="render-annotation" data-acorde-annotation-id="{}" x="{}" y="{}">{}</text>"#,
1005 render::escape_xml(&annotation.id),
1006 annotation.x,
1007 annotation.y,
1008 render::escape_xml(&annotation.text),
1009 )
1010 })
1011 .collect::<String>();
1012 let marker = "</g></svg>";
1013 let insertion = svg
1014 .rfind(marker)
1015 .ok_or_else(|| RenderError::InvalidLayout {
1016 reason: "renderer output has no score root".into(),
1017 })?;
1018 svg.insert_str(insertion, &body);
1019 Ok(svg)
1020}
1021
1022pub fn render_svg_row(
1025 score: &Score,
1026 layout: &LayoutResult,
1027 row: usize,
1028 options: &SvgRenderOptions,
1029) -> Result<String, RenderError> {
1030 let mut subset = layout.clone();
1031 subset.rows = vec![
1032 layout
1033 .rows
1034 .get(row)
1035 .cloned()
1036 .ok_or(RenderError::InvalidRow { row })?,
1037 ];
1038 render_svg_with_layout(score, &subset, options)
1039}
1040
1041pub fn render_svg_metadata(
1043 score: &Score,
1044 layout: &LayoutResult,
1045 options: &SvgRenderOptions,
1046) -> Result<RenderMetadata, RenderError> {
1047 let options = options_for_score(options, score)?;
1048 render::build_svg_with_metadata(score, layout, &options).map(|(_, metadata)| metadata)
1049}