1use crate::{LayoutConfig, compute_layout};
2use acorde_core::Score;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
7pub enum PaperSize {
8 A4,
9 Letter,
10 Legal,
11 Custom { width_mm: f32, height_mm: f32 },
12}
13
14impl PaperSize {
15 fn dimensions_mm(self) -> (f32, f32) {
16 match self {
17 Self::A4 => (210.0, 297.0),
18 Self::Letter => (215.9, 279.4),
19 Self::Legal => (215.9, 355.6),
20 Self::Custom {
21 width_mm,
22 height_mm,
23 } => (width_mm, height_mm),
24 }
25 }
26}
27
28#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
30pub enum PageOrientation {
31 Portrait,
32 Landscape,
33}
34
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
37pub enum PageNumbering {
38 None,
39 OneBased,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47#[serde(default)]
48pub struct PrintConfig {
49 pub paper_size: PaperSize,
50 pub orientation: PageOrientation,
51 pub margin_top_mm: f32,
52 pub margin_right_mm: f32,
53 pub margin_bottom_mm: f32,
54 pub margin_left_mm: f32,
55 pub bleed_top_mm: f32,
56 pub bleed_right_mm: f32,
57 pub bleed_bottom_mm: f32,
58 pub bleed_left_mm: f32,
59 pub safe_top_mm: f32,
60 pub safe_right_mm: f32,
61 pub safe_bottom_mm: f32,
62 pub safe_left_mm: f32,
63 pub system_height_mm: f32,
64 pub scale: f32,
66 pub measures_per_system: usize,
67 pub systems_per_page: Option<usize>,
70 pub page_numbering: PageNumbering,
71}
72
73impl Default for PrintConfig {
74 fn default() -> Self {
75 Self {
76 paper_size: PaperSize::A4,
77 orientation: PageOrientation::Portrait,
78 margin_top_mm: 16.0,
79 margin_right_mm: 14.0,
80 margin_bottom_mm: 16.0,
81 margin_left_mm: 14.0,
82 bleed_top_mm: 0.0,
83 bleed_right_mm: 0.0,
84 bleed_bottom_mm: 0.0,
85 bleed_left_mm: 0.0,
86 safe_top_mm: 0.0,
87 safe_right_mm: 0.0,
88 safe_bottom_mm: 0.0,
89 safe_left_mm: 0.0,
90 system_height_mm: 24.0,
91 scale: 1.0,
92 measures_per_system: 4,
93 systems_per_page: None,
94 page_numbering: PageNumbering::OneBased,
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub struct SystemLayout {
102 pub address: SystemAddress,
103 pub system_index: usize,
104 pub page_index: usize,
105 pub measure_indices: Vec<usize>,
106 pub top_mm: f32,
107 pub height_mm: f32,
108 pub break_reason: BreakReason,
109}
110
111#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
113pub struct PageAddress {
114 pub page_index: usize,
115}
116
117#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
119pub struct SystemAddress {
120 pub system_index: usize,
121 pub page_index: usize,
122 pub index_on_page: usize,
123}
124
125#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
127pub enum BreakReason {
128 MeasureCapacity,
129 ExplicitSystemBreak,
130 ExplicitPageBreak,
131 PageCapacity,
132 EndOfScore,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137pub struct PageLayout {
138 pub address: PageAddress,
139 pub page_index: usize,
140 pub page_number: Option<usize>,
141 pub width_mm: f32,
142 pub height_mm: f32,
143 pub content_width_mm: f32,
144 pub content_height_mm: f32,
145 pub bleed_top_mm: f32,
146 pub bleed_right_mm: f32,
147 pub bleed_bottom_mm: f32,
148 pub bleed_left_mm: f32,
149 pub systems: Vec<SystemLayout>,
150 pub break_reason: BreakReason,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155pub struct PrintLayoutResult {
156 pub contract_version: u16,
157 pub pages: Vec<PageLayout>,
158}
159
160#[derive(Debug, thiserror::Error, PartialEq)]
161pub enum PrintLayoutError {
162 #[error("paper dimensions must be finite and greater than zero")]
163 InvalidPaperDimensions,
164 #[error("margins must be finite and non-negative")]
165 InvalidMargins,
166 #[error("system height must be finite and greater than zero")]
167 InvalidSystemHeight,
168 #[error("print scale must be finite and greater than zero")]
169 InvalidScale,
170 #[error("margins leave no usable page area")]
171 NoUsablePageArea,
172}
173
174pub fn compute_print_layout(
176 score: &Score,
177 config: &PrintConfig,
178) -> Result<PrintLayoutResult, PrintLayoutError> {
179 let (mut width_mm, mut height_mm) = config.paper_size.dimensions_mm();
180 if !width_mm.is_finite() || !height_mm.is_finite() || width_mm <= 0.0 || height_mm <= 0.0 {
181 return Err(PrintLayoutError::InvalidPaperDimensions);
182 }
183 if matches!(config.orientation, PageOrientation::Landscape) {
184 std::mem::swap(&mut width_mm, &mut height_mm);
185 }
186
187 let margins = [
188 config.margin_top_mm,
189 config.margin_right_mm,
190 config.margin_bottom_mm,
191 config.margin_left_mm,
192 config.bleed_top_mm,
193 config.bleed_right_mm,
194 config.bleed_bottom_mm,
195 config.bleed_left_mm,
196 config.safe_top_mm,
197 config.safe_right_mm,
198 config.safe_bottom_mm,
199 config.safe_left_mm,
200 ];
201 if margins
202 .iter()
203 .any(|value| !value.is_finite() || *value < 0.0)
204 {
205 return Err(PrintLayoutError::InvalidMargins);
206 }
207 if !config.system_height_mm.is_finite() || config.system_height_mm <= 0.0 {
208 return Err(PrintLayoutError::InvalidSystemHeight);
209 }
210 if !config.scale.is_finite() || config.scale <= 0.0 {
211 return Err(PrintLayoutError::InvalidScale);
212 }
213 let scaled_system_height_mm = config.system_height_mm * config.scale;
214 if !scaled_system_height_mm.is_finite() || scaled_system_height_mm <= 0.0 {
215 return Err(PrintLayoutError::InvalidScale);
216 }
217
218 let content_width_mm = width_mm
219 - config.margin_left_mm
220 - config.margin_right_mm
221 - config.safe_left_mm
222 - config.safe_right_mm;
223 let content_height_mm = height_mm
224 - config.margin_top_mm
225 - config.margin_bottom_mm
226 - config.safe_top_mm
227 - config.safe_bottom_mm;
228 if content_width_mm <= 0.0 || content_height_mm <= 0.0 {
229 return Err(PrintLayoutError::NoUsablePageArea);
230 }
231
232 let systems_per_page = config
233 .systems_per_page
234 .unwrap_or_else(|| {
235 (content_height_mm / scaled_system_height_mm)
236 .floor()
237 .max(1.0) as usize
238 })
239 .max(1);
240 let layout = compute_layout(
241 score,
242 &LayoutConfig {
243 measures_per_row: config.measures_per_system.max(1),
244 ..LayoutConfig::default()
245 },
246 );
247
248 let mut pages = Vec::new();
249 let mut page_systems = Vec::new();
250 let mut page_index = 0;
251 for (system_index, row) in layout.rows.iter().enumerate() {
252 let explicit_page_break = row.measure_indices.last().is_some_and(|&measure_index| {
253 score
254 .parts
255 .iter()
256 .flat_map(|part| part.staves.iter())
257 .filter_map(|staff| staff.measures.get(measure_index))
258 .any(|measure| measure.page_break)
259 });
260 let explicit_system_break = row.measure_indices.last().is_some_and(|&measure_index| {
261 score
262 .parts
263 .iter()
264 .flat_map(|part| part.staves.iter())
265 .filter_map(|staff| staff.measures.get(measure_index))
266 .any(|measure| measure.system_break)
267 });
268 let is_last_system = system_index + 1 == layout.rows.len();
269 let break_reason = if explicit_page_break {
270 BreakReason::ExplicitPageBreak
271 } else if explicit_system_break {
272 BreakReason::ExplicitSystemBreak
273 } else if is_last_system {
274 BreakReason::EndOfScore
275 } else {
276 BreakReason::MeasureCapacity
277 };
278 let system = SystemLayout {
279 address: SystemAddress {
280 system_index,
281 page_index,
282 index_on_page: page_systems.len(),
283 },
284 system_index,
285 page_index,
286 measure_indices: row.measure_indices.clone(),
287 top_mm: config.margin_top_mm
288 + config.safe_top_mm
289 + page_systems.len() as f32 * scaled_system_height_mm,
290 height_mm: scaled_system_height_mm,
291 break_reason,
292 };
293 page_systems.push(system);
294
295 let page_is_full = page_systems.len() >= systems_per_page;
296 if page_is_full || explicit_page_break {
297 let page_break_reason = if explicit_page_break {
298 BreakReason::ExplicitPageBreak
299 } else if is_last_system {
300 BreakReason::EndOfScore
301 } else {
302 BreakReason::PageCapacity
303 };
304 pages.push(PageLayout {
305 address: PageAddress { page_index },
306 page_index,
307 page_number: match config.page_numbering {
308 PageNumbering::None => None,
309 PageNumbering::OneBased => Some(page_index + 1),
310 },
311 width_mm,
312 height_mm,
313 content_width_mm,
314 content_height_mm,
315 bleed_top_mm: config.bleed_top_mm,
316 bleed_right_mm: config.bleed_right_mm,
317 bleed_bottom_mm: config.bleed_bottom_mm,
318 bleed_left_mm: config.bleed_left_mm,
319 systems: std::mem::take(&mut page_systems),
320 break_reason: page_break_reason,
321 });
322 page_index += 1;
323 }
324 }
325 if !page_systems.is_empty() || pages.is_empty() {
326 pages.push(PageLayout {
327 address: PageAddress { page_index },
328 page_index,
329 page_number: match config.page_numbering {
330 PageNumbering::None => None,
331 PageNumbering::OneBased => Some(page_index + 1),
332 },
333 width_mm,
334 height_mm,
335 content_width_mm,
336 content_height_mm,
337 bleed_top_mm: config.bleed_top_mm,
338 bleed_right_mm: config.bleed_right_mm,
339 bleed_bottom_mm: config.bleed_bottom_mm,
340 bleed_left_mm: config.bleed_left_mm,
341 systems: page_systems,
342 break_reason: BreakReason::EndOfScore,
343 });
344 }
345
346 Ok(PrintLayoutResult {
347 contract_version: 5,
348 pages,
349 })
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355 use acorde_core::{Clef, Measure, Part, Score, Staff};
356
357 fn score_with_measures(count: usize) -> Score {
358 let mut score = Score::default();
359 let mut part = Part::new("Piano", "Pno.");
360 let mut staff = Staff::new(Clef::Treble);
361 staff.measures = (0..count).map(|_| Measure::empty(4, 4)).collect();
362 part.staves = vec![staff];
363 score.parts = vec![part];
364 score
365 }
366
367 #[test]
368 fn paginates_rows_and_preserves_measure_indices() {
369 let score = score_with_measures(5);
370 let result = compute_print_layout(
371 &score,
372 &PrintConfig {
373 measures_per_system: 2,
374 systems_per_page: Some(2),
375 ..PrintConfig::default()
376 },
377 )
378 .expect("valid print config");
379 assert_eq!(result.pages.len(), 2);
380 assert_eq!(
381 result.pages[0]
382 .systems
383 .iter()
384 .map(|s| s.measure_indices.clone())
385 .collect::<Vec<_>>(),
386 vec![vec![0, 1], vec![2, 3]]
387 );
388 assert_eq!(result.pages[1].systems[0].measure_indices, vec![4]);
389 assert_eq!(result.pages[1].systems[0].page_index, 1);
390 assert_eq!(result.pages[1].systems[0].address.index_on_page, 0);
391 assert_eq!(
392 result.pages[1].systems[0].break_reason,
393 BreakReason::EndOfScore
394 );
395 assert_eq!(result.pages[0].break_reason, BreakReason::PageCapacity);
396 }
397
398 #[test]
399 fn forced_page_break_starts_next_system_on_next_page() {
400 let mut score = score_with_measures(3);
401 score.parts[0].staves[0].measures[0].page_break = true;
402 let result = compute_print_layout(
403 &score,
404 &PrintConfig {
405 measures_per_system: 3,
406 systems_per_page: Some(8),
407 ..PrintConfig::default()
408 },
409 )
410 .expect("valid print config");
411 assert_eq!(result.pages.len(), 2);
412 assert_eq!(result.pages[0].systems[0].measure_indices, vec![0]);
413 assert_eq!(result.pages[1].systems[0].measure_indices, vec![1, 2]);
414 assert_eq!(result.pages[0].break_reason, BreakReason::ExplicitPageBreak);
415 assert_eq!(
416 result.pages[0].systems[0].break_reason,
417 BreakReason::ExplicitPageBreak
418 );
419 }
420
421 #[test]
422 fn rejects_margins_that_leave_no_page_area() {
423 let score = score_with_measures(1);
424 let error = compute_print_layout(
425 &score,
426 &PrintConfig {
427 margin_left_mm: 200.0,
428 ..PrintConfig::default()
429 },
430 )
431 .expect_err("invalid page area");
432 assert_eq!(error, PrintLayoutError::NoUsablePageArea);
433 }
434
435 #[test]
436 fn safe_area_reduces_content_and_bleed_is_exposed() {
437 let score = score_with_measures(1);
438 let result = compute_print_layout(
439 &score,
440 &PrintConfig {
441 bleed_top_mm: 3.0,
442 bleed_right_mm: 3.0,
443 bleed_bottom_mm: 3.0,
444 bleed_left_mm: 3.0,
445 safe_top_mm: 5.0,
446 safe_right_mm: 6.0,
447 safe_bottom_mm: 7.0,
448 safe_left_mm: 8.0,
449 ..PrintConfig::default()
450 },
451 )
452 .expect("valid print config");
453 let page = &result.pages[0];
454 assert_eq!(result.contract_version, 5);
455 assert_eq!(page.bleed_left_mm, 3.0);
456 assert_eq!(page.content_width_mm, 210.0 - 14.0 - 14.0 - 8.0 - 6.0);
457 assert_eq!(page.content_height_mm, 297.0 - 16.0 - 16.0 - 5.0 - 7.0);
458 assert_eq!(page.systems[0].top_mm, 21.0);
459 }
460
461 #[test]
462 fn scale_changes_system_height_and_page_capacity() {
463 let score = score_with_measures(10);
464 let result = compute_print_layout(
465 &score,
466 &PrintConfig {
467 scale: 2.0,
468 measures_per_system: 1,
469 systems_per_page: None,
470 ..PrintConfig::default()
471 },
472 )
473 .expect("valid print config");
474 assert_eq!(result.pages[0].systems[0].height_mm, 48.0);
475 assert_eq!(result.pages[0].systems[1].top_mm, 64.0);
476 assert_eq!(result.pages.len(), 2);
477 }
478
479 #[test]
480 fn rejects_non_positive_scale() {
481 let score = score_with_measures(1);
482 let error = compute_print_layout(
483 &score,
484 &PrintConfig {
485 scale: 0.0,
486 ..PrintConfig::default()
487 },
488 )
489 .expect_err("invalid scale");
490 assert_eq!(error, PrintLayoutError::InvalidScale);
491 }
492
493 #[test]
494 fn page_numbering_is_configurable() {
495 let score = score_with_measures(5);
496 let numbered = compute_print_layout(
497 &score,
498 &PrintConfig {
499 measures_per_system: 1,
500 systems_per_page: Some(2),
501 ..PrintConfig::default()
502 },
503 )
504 .expect("valid print config");
505 assert_eq!(numbered.pages[0].page_number, Some(1));
506 assert_eq!(numbered.pages[1].page_number, Some(2));
507
508 let unnumbered = compute_print_layout(
509 &score,
510 &PrintConfig {
511 page_numbering: PageNumbering::None,
512 measures_per_system: 1,
513 systems_per_page: Some(2),
514 ..PrintConfig::default()
515 },
516 )
517 .expect("valid print config");
518 assert!(
519 unnumbered
520 .pages
521 .iter()
522 .all(|page| page.page_number.is_none())
523 );
524 }
525}