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, RowLayout,
compute_layout,
};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::ops::Deref;
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)]
pub struct SvgRenderConfig {
options: SvgRenderOptions,
staff_gap: f32,
system_breaking: SvgSystemBreakStrategy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SvgSystemBreakStrategy {
FixedMeasureCount,
NotationDensity,
}
impl SvgRenderConfig {
pub fn new(options: SvgRenderOptions) -> Self {
Self {
options,
staff_gap: 10.0,
system_breaking: SvgSystemBreakStrategy::FixedMeasureCount,
}
}
pub fn with_staff_gap(mut self, staff_gap: f32) -> Self {
self.staff_gap = staff_gap;
self
}
pub fn with_notation_density_breaks(mut self) -> Self {
self.system_breaking = SvgSystemBreakStrategy::NotationDensity;
self
}
pub fn options(&self) -> &SvgRenderOptions {
&self.options
}
pub fn staff_gap(&self) -> f32 {
self.staff_gap
}
pub fn system_breaking(&self) -> SvgSystemBreakStrategy {
self.system_breaking
}
fn with_options(mut self, options: SvgRenderOptions) -> Self {
self.options = options;
self
}
}
impl Deref for SvgRenderConfig {
type Target = SvgRenderOptions;
fn deref(&self) -> &Self::Target {
&self.options
}
}
#[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
&& (!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
&& (!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 },
MeasureOverflow {
measure: usize,
required_units: usize,
available_units: usize,
},
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::MeasureOverflow {
measure,
required_units,
available_units,
} => write!(
f,
"measure {} needs {required_units} staff-space units but only {available_units} fit in one system",
measure + 1,
),
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_with_config(
score: &Score,
config: &SvgRenderConfig,
) -> Result<String, RenderError> {
let options = options_for_score(config.options(), score)?;
let config = config.clone().with_options(options);
let layout = layout_for_config(score, &config)?;
render::build_svg_with_config(score, &layout, &config)
}
fn layout_for_config(score: &Score, config: &SvgRenderConfig) -> Result<LayoutResult, RenderError> {
let mut layout = compute_layout(
score,
&LayoutConfig {
measures_per_row: config.measures_per_system.max(1),
..Default::default()
},
);
if config.system_breaking() == SvgSystemBreakStrategy::NotationDensity {
layout.rows = density_aware_rows(score, &layout.vis_slots, config.options())?;
}
Ok(layout)
}
fn density_aware_rows(
score: &Score,
vis_slots: &[usize],
options: &SvgRenderOptions,
) -> Result<Vec<RowLayout>, RenderError> {
if vis_slots.is_empty() {
return Ok(vec![RowLayout {
measure_indices: Vec::new(),
}]);
}
let available_units = ((options.width / options.staff_size) - 14.0)
.floor()
.max(1.0) as usize;
let column_limit = options.measures_per_system.max(1);
let mut spans = Vec::new();
for &measure in vis_slots {
if let Some((last, slots)) = spans.last_mut()
&& *last == measure
{
*slots += 1;
} else {
spans.push((measure, 1usize));
}
}
let mut rows = Vec::new();
let mut row = Vec::new();
let mut row_columns = 0usize;
let mut row_units = 0usize;
for (measure, visual_columns) in spans {
let units = measure_density_units(score, measure).max(visual_columns * 3);
if units > available_units {
return Err(RenderError::MeasureOverflow {
measure,
required_units: units,
available_units,
});
}
if !row.is_empty()
&& (row_columns.saturating_add(visual_columns) > column_limit
|| row_units.saturating_add(units) > available_units)
{
rows.push(RowLayout {
measure_indices: std::mem::take(&mut row),
});
row_columns = 0;
row_units = 0;
}
row.push(measure);
row_columns += visual_columns;
row_units += units;
if measure_forces_system_break(score, measure) {
rows.push(RowLayout {
measure_indices: std::mem::take(&mut row),
});
row_columns = 0;
row_units = 0;
}
}
if !row.is_empty() {
rows.push(RowLayout {
measure_indices: row,
});
}
Ok(rows)
}
fn measure_forces_system_break(score: &Score, measure: usize) -> bool {
score
.parts
.iter()
.flat_map(|part| &part.staves)
.filter_map(|staff| staff.measures.get(measure))
.any(|measure| measure.system_break || measure.page_break)
}
fn measure_density_units(score: &Score, measure: usize) -> usize {
let mut widest_staff = 3usize;
for staff in score.parts.iter().flat_map(|part| &part.staves) {
let Some(measure) = staff.measures.get(measure) else {
continue;
};
let voice_width = measure
.voices
.iter()
.map(|voice| {
voice
.iter()
.filter(|note| !note.hidden)
.map(|note| {
let notehead_width = if note.is_rest { 1 } else { 2 };
let chord_width = note.pitches.len().saturating_sub(1);
let accidental_width =
note.pitches.iter().filter(|pitch| pitch.alter != 0).count();
let lyric_width = note
.lyric
.as_ref()
.map_or(0, |lyric| lyric.text.chars().count().div_ceil(3));
notehead_width
+ chord_width
+ accidental_width
+ usize::from(note.dot_count)
+ lyric_width
+ usize::from(note.chord_symbol.is_some()) * 2
+ note.articulations.len().div_ceil(2)
})
.sum::<usize>()
})
.max()
.unwrap_or(0);
let text_width = measure
.texts
.iter()
.map(|text| text.text.chars().count().div_ceil(4))
.sum::<usize>();
widest_staff = widest_staff.max(voice_width + text_width + 3);
}
widest_staff
}
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_layout_config(
score: &Score,
layout: &LayoutResult,
config: &SvgRenderConfig,
) -> Result<String, RenderError> {
let options = options_for_score(config.options(), score)?;
let config = config.clone().with_options(options);
render::build_svg_with_config(score, layout, &config)
}
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)
}
pub fn render_svg_metadata_with_config(
score: &Score,
layout: &LayoutResult,
config: &SvgRenderConfig,
) -> Result<RenderMetadata, RenderError> {
let options = options_for_score(config.options(), score)?;
let config = config.clone().with_options(options);
render::build_svg_with_metadata_config(score, layout, &config).map(|(_, metadata)| metadata)
}