mod annotations;
mod beams;
mod collision;
mod geometry;
mod glyphs;
mod render;
mod spans;
mod tab_technique;
mod tuplets;
use acorde_core::{
HarpPedalDiagram, NoteAddr, NoteHead, ObjectStyleOverride, Score, ScoreView, StaffPresentation,
TextStyle, ValidationError, ViewStyle, ViewStyleOverride,
};
use acorde_layout::{
GlyphCollisionClass, GlyphCollisionDirection, LayoutConfig, LayoutResult, compute_layout,
};
use serde::{Deserialize, Serialize};
use std::fmt;
pub const SVG_CONTRACT_VERSION: u32 = 22;
pub const GLYPH_COVERAGE_CONTRACT_VERSION: u32 = 3;
pub const BUILTIN_GLYPH_RESOURCE_ID: &str = "acorde-vector-glyphs-v1";
pub const TAB_METRICS_CONTRACT_VERSION: u32 = 1;
pub const SVG_ANNOTATION_COLLISION_GAP_PX: f32 = 2.0;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct TabFretMetrics {
pub fret: u8,
pub digit_count: u8,
pub advance_units: f32,
pub side_gap_units: f32,
}
pub fn tab_fret_metrics(fret: u8) -> TabFretMetrics {
let digit_count = fret.to_string().len() as u8;
TabFretMetrics {
fret,
digit_count,
advance_units: f32::from(digit_count) * 0.48,
side_gap_units: 0.22,
}
}
pub fn percussion_notehead_resource_id(note_head: &NoteHead) -> &'static str {
match note_head {
NoteHead::Normal => "acorde-percussion-notehead-normal",
NoteHead::Diamond => "acorde-percussion-notehead-diamond",
NoteHead::X => "acorde-percussion-notehead-x",
NoteHead::Slash => "acorde-percussion-notehead-slash",
NoteHead::Cross => "acorde-percussion-notehead-cross",
NoteHead::Triangle => "acorde-percussion-notehead-triangle",
}
}
const MAX_RENDER_ANNOTATIONS: usize = 10_000;
const MAX_ANNOTATION_TEXT_BYTES: usize = 16 * 1024;
pub(crate) fn is_valid_xml_char(character: char) -> bool {
matches!(character, '\u{9}' | '\u{A}' | '\u{D}')
|| ('\u{20}'..='\u{D7FF}').contains(&character)
|| ('\u{E000}'..='\u{FFFD}').contains(&character)
|| ('\u{10000}'..='\u{10FFFF}').contains(&character)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SvgRenderOptions {
pub width: f32,
pub staff_size: f32,
pub measures_per_system: usize,
pub interactive: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenderMetadata {
pub contract_version: u32,
pub width: f32,
pub height: f32,
pub part_count: usize,
pub staff_count: usize,
pub measure_count: usize,
pub note_count: usize,
#[serde(default)]
pub view: Option<RenderedViewMetadata>,
pub accessible_text: String,
pub address_bounds: Vec<AddressBounds>,
#[serde(default)]
pub text_annotations: Vec<TextAnnotation>,
#[serde(default)]
pub score_texts: Vec<ScoreTextMetadata>,
#[serde(default)]
pub tablature_positions: Vec<TablaturePositionMetadata>,
#[serde(default)]
pub tablature_staves: Vec<TablatureStaffMetadata>,
#[serde(default)]
pub tablature_changes: Vec<TablatureChangeMetadata>,
#[serde(default)]
pub harmony_ranges: Vec<HarmonyRangeMetadata>,
#[serde(default)]
pub tablature_technique_connections: Vec<TablatureTechniqueConnectionMetadata>,
#[serde(default)]
pub note_semantics: Vec<NoteSemanticMetadata>,
#[serde(default)]
pub object_style_overrides: Vec<ObjectStyleOverride>,
#[serde(default)]
pub harp_pedal_diagrams: Vec<HarpPedalDiagramMetadata>,
#[serde(default)]
pub section_breaks: Vec<usize>,
#[serde(default)]
pub staff_presentations: Vec<StaffPresentationMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StaffPresentationMetadata {
pub part: usize,
pub staff: usize,
pub presentation: StaffPresentation,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RenderedViewMetadata {
pub id: String,
pub name: String,
pub measures_per_system: usize,
pub style: ViewStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NoteSemanticMetadata {
pub part: usize,
pub staff: usize,
pub measure: usize,
pub voice: usize,
pub note: usize,
pub is_unpitched: bool,
#[serde(default)]
pub tie_start: bool,
#[serde(default)]
pub tie_end: bool,
#[serde(default)]
pub duration_beats: f64,
#[serde(default)]
pub pitch_midi_cents: Vec<i32>,
#[serde(default)]
pub offset_x: Option<f64>,
#[serde(default)]
pub offset_y: Option<f64>,
#[serde(default)]
pub relative_x: Option<f64>,
#[serde(default)]
pub relative_y: Option<f64>,
#[serde(default)]
pub dynamic: Option<String>,
#[serde(default)]
pub lyric: Option<String>,
#[serde(default)]
pub chord_label: Option<String>,
#[serde(default)]
pub technique_text: Option<String>,
#[serde(default)]
pub articulations: Vec<String>,
#[serde(default)]
pub guitar_technique: Option<acorde_core::GuitarTechnique>,
#[serde(default)]
pub guitar_bend_alter_cents: Option<i16>,
#[serde(default)]
pub guitar_bend_curve: Vec<acorde_core::GuitarBendPoint>,
#[serde(default)]
pub fingerings: Vec<u8>,
#[serde(default)]
pub instrument_id: Option<String>,
#[serde(default)]
pub microtone_cents: Vec<i16>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HarmonyRangeMetadata {
pub start: NoteAddr,
pub end: NoteAddr,
pub label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TablaturePositionMetadata {
pub part: usize,
pub staff: usize,
pub measure: usize,
pub voice: usize,
pub note: usize,
pub position: usize,
pub string: u8,
pub fret: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TablatureStaffMetadata {
pub part: usize,
pub staff: usize,
pub lines: u8,
#[serde(default)]
pub tuning_midi: Vec<i16>,
pub capo: u8,
#[serde(default)]
pub rhythm_display: acorde_core::TablatureRhythmDisplay,
#[serde(default)]
pub fret_mark_style: acorde_core::TablatureFretMarkStyle,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TablatureChangeMetadata {
pub part: usize,
pub staff: usize,
pub measure: usize,
pub lines: u8,
#[serde(default)]
pub tuning_midi: Vec<i16>,
pub capo: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TablatureTechniqueConnectionMetadata {
pub start: NoteAddr,
pub end: NoteAddr,
pub technique: acorde_core::GuitarTechnique,
pub string: u8,
#[serde(default)]
pub cross_measure: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TextAnnotation {
pub part: usize,
pub staff: usize,
pub measure: usize,
pub style: TextStyle,
pub text: String,
#[serde(default)]
pub placement: Option<String>,
#[serde(default)]
pub offset_x: Option<f64>,
#[serde(default)]
pub offset_y: Option<f64>,
#[serde(default)]
pub relative_x: Option<f64>,
#[serde(default)]
pub relative_y: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HarpPedalDiagramMetadata {
pub part: usize,
pub staff: usize,
pub measure: usize,
pub diagram: HarpPedalDiagram,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ScoreTextMetadata {
pub style: TextStyle,
pub text: String,
#[serde(default)]
pub placement: Option<String>,
#[serde(default)]
pub offset_x: Option<f64>,
#[serde(default)]
pub offset_y: Option<f64>,
#[serde(default)]
pub relative_x: Option<f64>,
#[serde(default)]
pub relative_y: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MicrotoneMarkerCoverage {
pub supported: bool,
pub representation: String,
pub exact_cents: bool,
}
impl Default for MicrotoneMarkerCoverage {
fn default() -> Self {
Self {
supported: true,
representation: "svg-text-cents".to_owned(),
exact_cents: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct GlyphCoverage {
pub contract_version: u32,
pub resource_id: String,
pub vector_glyphs: bool,
pub supported_clefs: Vec<String>,
pub accidental_min: i8,
pub accidental_max: i8,
#[serde(default)]
pub microtone_marker: MicrotoneMarkerCoverage,
#[serde(default)]
pub percussion_noteheads: Vec<String>,
}
pub fn glyph_coverage() -> GlyphCoverage {
GlyphCoverage {
contract_version: GLYPH_COVERAGE_CONTRACT_VERSION,
resource_id: BUILTIN_GLYPH_RESOURCE_ID.to_owned(),
vector_glyphs: true,
supported_clefs: vec![
"treble".to_owned(),
"bass".to_owned(),
"alto".to_owned(),
"tenor".to_owned(),
],
accidental_min: -2,
accidental_max: 2,
microtone_marker: MicrotoneMarkerCoverage::default(),
percussion_noteheads: [
NoteHead::Normal,
NoteHead::Diamond,
NoteHead::X,
NoteHead::Slash,
NoteHead::Cross,
NoteHead::Triangle,
]
.iter()
.map(|head| percussion_notehead_resource_id(head).to_owned())
.collect(),
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum RenderPreflightKind {
UnsupportedClef,
UnsupportedAccidental,
InvalidTabPosition,
MeasureTextTooLarge,
InvalidMeasureTextOffset,
InvalidNotePlacement,
InvalidXmlCharacter,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RenderPreflightIssue {
pub kind: RenderPreflightKind,
pub source_location: String,
pub preserved_value: String,
}
pub fn render_preflight(score: &Score) -> Vec<RenderPreflightIssue> {
let mut issues = Vec::new();
push_text_preflight_issue(&mut issues, "/score/title", &score.metadata.title);
for (part_index, part) in score.parts.iter().enumerate() {
push_text_preflight_issue(
&mut issues,
&format!("/score/part/{}/name", part_index + 1),
&part.name,
);
push_text_preflight_issue(
&mut issues,
&format!("/score/part/{}/short-name", part_index + 1),
&part.short_name,
);
for (staff_index, staff) in part.staves.iter().enumerate() {
let staff_path = format!("/score/part/{}/staff/{}", part_index + 1, staff_index + 1);
for (measure_index, measure) in staff.measures.iter().enumerate() {
for (text_index, styled) in measure.texts.iter().enumerate() {
let text_path = format!(
"{staff_path}/measure/{}/text/{}",
measure_index + 1,
text_index + 1
);
push_text_preflight_issue(&mut issues, &text_path, &styled.text);
for (field, value) in [
("offset_x", styled.offset_x),
("offset_y", styled.offset_y),
("relative_x", styled.relative_x),
("relative_y", styled.relative_y),
] {
if let Some(value) = value {
if !value.is_finite() || !(value as f32).is_finite() {
issues.push(RenderPreflightIssue {
kind: RenderPreflightKind::InvalidMeasureTextOffset,
source_location: format!("{text_path}/{field}"),
preserved_value: value.to_string(),
});
}
}
}
}
for (field, value) in [
("tempo-text", measure.tempo_text.as_deref()),
("rehearsal", measure.rehearsal.as_deref()),
("navigation", measure.navigation.as_deref()),
("expression-text", measure.expression_text.as_deref()),
] {
if let Some(value) = value {
push_text_preflight_issue(
&mut issues,
&format!("{staff_path}/measure/{}/{}", measure_index + 1, field),
value,
);
}
}
for (voice_index, voice) in measure.voices.iter().enumerate() {
for (note_index, note) in voice.iter().enumerate() {
let note_path = format!(
"{staff_path}/measure/{}/voice/{}/note/{}",
measure_index + 1,
voice_index + 1,
note_index + 1
);
if let Some(lyric) = ¬e.lyric {
push_text_preflight_issue(
&mut issues,
&format!("{note_path}/lyric"),
&lyric.text,
);
}
for entry in ¬e.additional_lyrics {
push_text_preflight_issue(
&mut issues,
&format!("{note_path}/lyric/{}", entry.verse),
&entry.lyric.text,
);
}
if let Some(technique) = ¬e.technique_text {
push_text_preflight_issue(
&mut issues,
&format!("{note_path}/technique-text"),
technique,
);
}
if let Some(chord) = ¬e.chord_symbol {
let chord_text = chord.display_text();
push_text_preflight_issue(
&mut issues,
&format!("{note_path}/chord-symbol"),
&chord_text,
);
}
for (field, value) in [
("default-x", note.offset_x),
("default-y", note.offset_y),
("relative-x", note.relative_x),
("relative-y", note.relative_y),
] {
if let Some(value) = value {
if !value.is_finite() || !(value as f32).is_finite() {
issues.push(RenderPreflightIssue {
kind: RenderPreflightKind::InvalidNotePlacement,
source_location: format!("{note_path}/{field}"),
preserved_value: value.to_string(),
});
}
}
}
for (pitch_index, pitch) in note.pitches.iter().enumerate() {
if !(-2..=2).contains(&pitch.alter) {
issues.push(RenderPreflightIssue {
kind: RenderPreflightKind::UnsupportedAccidental,
source_location: format!(
"{note_path}/pitch/{}",
pitch_index + 1
),
preserved_value: pitch.alter.to_string(),
});
}
}
if let Some(tab) = &staff.tablature {
let positions = if note.tab_positions.is_empty() {
note.tab_position.iter().collect::<Vec<_>>()
} else {
note.tab_positions.iter().collect::<Vec<_>>()
};
for position in positions {
if position.string == 0 || position.string > tab.lines {
issues.push(RenderPreflightIssue {
kind: RenderPreflightKind::InvalidTabPosition,
source_location: format!("{note_path}/tab-position"),
preserved_value: format!(
"string={},lines={}",
position.string, tab.lines
),
});
}
}
}
}
}
}
}
}
issues
}
fn push_text_preflight_issue(
issues: &mut Vec<RenderPreflightIssue>,
source_location: &str,
text: &str,
) {
if text.len() > MAX_ANNOTATION_TEXT_BYTES {
issues.push(RenderPreflightIssue {
kind: RenderPreflightKind::MeasureTextTooLarge,
source_location: source_location.to_owned(),
preserved_value: format!("bytes={}", text.len()),
});
}
if let Some(character) = text
.chars()
.find(|&character| !is_valid_xml_char(character))
{
issues.push(RenderPreflightIssue {
kind: RenderPreflightKind::InvalidXmlCharacter,
source_location: source_location.to_owned(),
preserved_value: format!("U+{:04X}", character as u32),
});
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AddressBounds {
pub part: usize,
pub staff: usize,
pub measure: usize,
pub voice: usize,
pub note: usize,
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SvgAnnotation {
pub id: String,
pub x: f32,
pub y: f32,
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct SvgAnnotationMetrics {
pub left_px: f32,
pub top_px: f32,
pub width_px: f32,
pub height_px: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SvgAnnotationCollisionPolicy {
pub class: GlyphCollisionClass,
pub direction: GlyphCollisionDirection,
pub priority: u8,
}
pub trait RenderAnnotation {
fn id(&self) -> &str;
fn annotate(
&self,
score: &Score,
layout: &LayoutResult,
metadata: &RenderMetadata,
) -> Vec<SvgAnnotation>;
fn collision_policy(
&self,
_annotation: &SvgAnnotation,
) -> Option<(SvgAnnotationMetrics, SvgAnnotationCollisionPolicy)> {
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenderAnnotationError {
EmptyProviderId,
DuplicateProviderId(String),
EmptyAnnotationId,
DuplicateAnnotationId(String),
NonFiniteCoordinate { id: String },
TooManyAnnotations { count: usize },
AnnotationTextTooLarge { id: String, size: usize },
InvalidXmlCharacter { id: String, codepoint: u32 },
InvalidCollisionMetrics { id: String },
CollisionResolution { id: String },
}
impl fmt::Display for RenderAnnotationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyProviderId => write!(f, "render annotation provider id is empty"),
Self::DuplicateProviderId(id) => {
write!(f, "duplicate render annotation provider: {id}")
}
Self::EmptyAnnotationId => write!(f, "render annotation id is empty"),
Self::DuplicateAnnotationId(id) => write!(f, "duplicate render annotation: {id}"),
Self::NonFiniteCoordinate { id } => {
write!(f, "render annotation {id} has a non-finite coordinate")
}
Self::TooManyAnnotations { count } => {
write!(f, "render annotation count exceeds limit: {count}")
}
Self::AnnotationTextTooLarge { id, size } => {
write!(f, "render annotation {id} text exceeds limit: {size} bytes")
}
Self::InvalidXmlCharacter { id, codepoint } => write!(
f,
"render annotation {id} contains invalid XML character U+{codepoint:04X}"
),
Self::InvalidCollisionMetrics { id } => {
write!(f, "render annotation {id} has invalid collision metrics")
}
Self::CollisionResolution { id } => {
write!(f, "render annotation {id} could not be collision-resolved")
}
}
}
}
impl std::error::Error for RenderAnnotationError {}
impl Default for SvgRenderOptions {
fn default() -> Self {
Self {
width: 900.0,
staff_size: 24.0,
measures_per_system: 4,
interactive: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenderError {
EmptyScore,
InvalidRow { row: usize },
ViewResolution { view_id: String, reason: String },
InvalidLayout { reason: String },
InvalidOptions { reason: String },
UnsupportedClef,
UnsupportedAccidental { alter: i8 },
InvalidTabPosition { string: u8, lines: u8 },
TabMetricsOverflow,
MeasureTextTooLarge { size: usize },
InvalidMeasureTextOffset { field: &'static str },
InvalidNotePlacement { field: &'static str },
InvalidXmlCharacter { codepoint: u32 },
Annotation(RenderAnnotationError),
}
impl std::fmt::Display for RenderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RenderError::EmptyScore => write!(f, "score has no staves to render"),
RenderError::InvalidRow { row } => write!(f, "layout row {row} does not exist"),
RenderError::ViewResolution { view_id, reason } => {
write!(f, "cannot resolve score view '{view_id}': {reason}")
}
RenderError::InvalidLayout { reason } => write!(f, "invalid layout: {reason}"),
RenderError::InvalidOptions { reason } => write!(f, "invalid render options: {reason}"),
RenderError::UnsupportedClef => write!(
f,
"unsupported clef (no staff-position mapping is available)"
),
RenderError::UnsupportedAccidental { alter } => {
write!(
f,
"unsupported accidental alter={alter} (supported range is -2..=2)"
)
}
RenderError::InvalidTabPosition { string, lines } => write!(
f,
"tablature string {string} is outside the owning staff line range 1..={lines}"
),
RenderError::TabMetricsOverflow => write!(f, "tablature glyph metrics overflowed"),
RenderError::MeasureTextTooLarge { size } => write!(
f,
"measure-level text is too large ({size} bytes; maximum is {MAX_ANNOTATION_TEXT_BYTES})"
),
RenderError::InvalidMeasureTextOffset { field } => {
write!(f, "measure-level text offset {field} is not finite")
}
RenderError::InvalidNotePlacement { field } => {
write!(f, "note placement offset {field} is not finite")
}
RenderError::InvalidXmlCharacter { codepoint } => write!(
f,
"score text contains invalid XML character U+{codepoint:04X}"
),
RenderError::Annotation(error) => write!(f, "invalid render annotation: {error}"),
}
}
}
impl std::error::Error for RenderError {}
pub fn render_svg(score: &Score, options: &SvgRenderOptions) -> Result<String, RenderError> {
let config = LayoutConfig {
measures_per_row: options.measures_per_system.max(1),
..Default::default()
};
let layout = compute_layout(score, &config);
render_svg_with_layout(score, &layout, options)
}
pub fn render_svg_view(
score: &Score,
view_id: &str,
options: &SvgRenderOptions,
) -> Result<String, RenderError> {
let view = score
.views
.iter()
.find(|view| view.id == view_id)
.ok_or_else(|| RenderError::ViewResolution {
view_id: view_id.to_owned(),
reason: "view does not exist".into(),
})?;
let projected = score
.resolve_view(view_id)
.map_err(|error| RenderError::ViewResolution {
view_id: view_id.to_owned(),
reason: error.to_string(),
})?;
let options = options_for_view(options, view)?;
render_svg(&projected, &options)
}
pub fn render_svg_view_metadata(
score: &Score,
view_id: &str,
options: &SvgRenderOptions,
) -> Result<RenderMetadata, RenderError> {
let view = score
.views
.iter()
.find(|view| view.id == view_id)
.ok_or_else(|| RenderError::ViewResolution {
view_id: view_id.to_owned(),
reason: "view does not exist".into(),
})?;
let projected = score
.resolve_view(view_id)
.map_err(|error| RenderError::ViewResolution {
view_id: view_id.to_owned(),
reason: error.to_string(),
})?;
let options = options_for_view(options, view)?;
let layout = compute_layout(
&projected,
&LayoutConfig {
measures_per_row: options.measures_per_system,
..Default::default()
},
);
let mut metadata = render_svg_metadata(&projected, &layout, &options)?;
metadata.view = Some(RenderedViewMetadata {
id: view.id.clone(),
name: view.name.clone(),
measures_per_system: options.measures_per_system,
style: score.resolved_view_style(&view.layout),
});
Ok(metadata)
}
fn options_for_view(
options: &SvgRenderOptions,
view: &ScoreView,
) -> Result<SvgRenderOptions, RenderError> {
validate_style_overrides(&view.layout.typed_style_overrides)?;
let mut resolved = options.clone();
if let Some(measures_per_row) = view.layout.measures_per_row {
resolved.measures_per_system = measures_per_row;
}
resolved.staff_size *= view.layout.resolved_style().staff_space;
Ok(resolved)
}
fn options_for_score(
options: &SvgRenderOptions,
score: &Score,
) -> Result<SvgRenderOptions, RenderError> {
validate_style_overrides(&score.style_overrides)?;
if acorde_core::validate(score)
.errors
.iter()
.any(|error| matches!(error, ValidationError::InvalidObjectStyleOverride { .. }))
{
return Err(RenderError::InvalidOptions {
reason: "object style overrides must target an existing object and use valid values and provenance".into(),
});
}
let mut resolved = options.clone();
resolved.staff_size *= score.resolved_view_style(&Default::default()).staff_space;
Ok(resolved)
}
fn validate_style_overrides(overrides: &[ViewStyleOverride]) -> Result<(), RenderError> {
if overrides
.iter()
.any(|override_| !override_.value.is_finite() || !(0.05..=64.0).contains(&override_.value))
{
return Err(RenderError::InvalidOptions {
reason: "typed style override values must be finite and within 0.05..=64".into(),
});
}
Ok(())
}
pub fn render_svg_with_layout(
score: &Score,
layout: &LayoutResult,
options: &SvgRenderOptions,
) -> Result<String, RenderError> {
let options = options_for_score(options, score)?;
render::build_svg(score, layout, &options)
}
pub fn render_svg_with_annotations(
score: &Score,
layout: &LayoutResult,
options: &SvgRenderOptions,
providers: &[&dyn RenderAnnotation],
) -> Result<String, RenderError> {
let options = options_for_score(options, score)?;
let (mut svg, metadata) = render::build_svg_with_metadata(score, layout, &options)?;
let annotations = render::collect_annotations(score, layout, &metadata, providers)
.map_err(RenderError::Annotation)?;
if annotations.is_empty() {
return Ok(svg);
}
let body = annotations
.iter()
.map(|annotation| {
format!(
r#"<text class="acorde-render-annotation" data-acorde-kind="render-annotation" data-acorde-annotation-id="{}" x="{}" y="{}">{}</text>"#,
render::escape_xml(&annotation.id),
annotation.x,
annotation.y,
render::escape_xml(&annotation.text),
)
})
.collect::<String>();
let marker = "</g></svg>";
let insertion = svg
.rfind(marker)
.ok_or_else(|| RenderError::InvalidLayout {
reason: "renderer output has no score root".into(),
})?;
svg.insert_str(insertion, &body);
Ok(svg)
}
pub fn render_svg_row(
score: &Score,
layout: &LayoutResult,
row: usize,
options: &SvgRenderOptions,
) -> Result<String, RenderError> {
let mut subset = layout.clone();
subset.rows = vec![
layout
.rows
.get(row)
.cloned()
.ok_or(RenderError::InvalidRow { row })?,
];
render_svg_with_layout(score, &subset, options)
}
pub fn render_svg_metadata(
score: &Score,
layout: &LayoutResult,
options: &SvgRenderOptions,
) -> Result<RenderMetadata, RenderError> {
let options = options_for_score(options, score)?;
render::build_svg_with_metadata(score, layout, &options).map(|(_, metadata)| metadata)
}