use crate::document::PdfDocument;
use crate::geometry::Rect;
pub(crate) const MUSIC_FONT_NEEDLES: &[&str] = &[
"maestro", "bravura", "petrucci", "opus", "sonata", "emmentaler", "musicalsymbols", "engravertext", "noteheadgroup", ];
pub(crate) fn is_music_font_name(font_name: &str) -> bool {
let lower = font_name.to_ascii_lowercase();
let core = lower.split_once('+').map(|(_, r)| r).unwrap_or(&lower);
MUSIC_FONT_NEEDLES.iter().any(|n| core.contains(n))
}
pub(crate) fn find_music_regions(doc: &PdfDocument, page_idx: usize) -> Vec<Rect> {
let spans = match doc.extract_spans(page_idx) {
Ok(s) => s,
Err(_) => return Vec::new(),
};
let lookups = doc.page_font_face_lookups().unwrap_or_default();
let page_lookup = lookups.get(page_idx);
let resolve = |resource_id: &str| -> String {
page_lookup
.and_then(|m| m.get(resource_id).cloned())
.unwrap_or_else(|| resource_id.to_string())
};
let has_music_font = spans
.iter()
.any(|s| is_music_font_name(&resolve(&s.font_name)));
if !has_music_font {
return Vec::new();
}
let paths = match doc.extract_paths(page_idx) {
Ok(p) => p,
Err(_) => return Vec::new(),
};
#[derive(Clone, Copy)]
struct HLine {
x_min: f32,
x_max: f32,
y: f32,
}
let mut hlines: Vec<HLine> = Vec::new();
for p in &paths {
if p.bbox.height <= 1.0 && p.bbox.width > 50.0 {
hlines.push(HLine {
x_min: p.bbox.x,
x_max: p.bbox.x + p.bbox.width,
y: p.bbox.y + p.bbox.height * 0.5,
});
}
}
if hlines.len() < 5 {
return Vec::new();
}
hlines.sort_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal));
struct Cluster {
y_min: f32,
y_max: f32,
x_min: f32,
x_max: f32,
n: usize,
}
let mut clusters: Vec<Cluster> = Vec::new();
for hl in &hlines {
let extend = clusters.last_mut().is_some_and(|c| (hl.y - c.y_max) <= 6.0);
if extend {
let c = clusters.last_mut().unwrap();
c.y_max = hl.y;
c.x_min = c.x_min.max(hl.x_min);
c.x_max = c.x_max.min(hl.x_max);
c.n += 1;
} else {
clusters.push(Cluster {
y_min: hl.y,
y_max: hl.y,
x_min: hl.x_min,
x_max: hl.x_max,
n: 1,
});
}
}
let staves: Vec<Cluster> = clusters
.into_iter()
.filter(|c| c.n >= 5 && (c.y_max - c.y_min) <= 25.0 && c.x_max > c.x_min)
.collect();
if staves.is_empty() {
return Vec::new();
}
let mut regions: Vec<Rect> = staves
.into_iter()
.map(|c| Rect::new(c.x_min, c.y_min - 25.0, c.x_max - c.x_min, (c.y_max - c.y_min) + 50.0))
.collect();
regions.sort_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal));
let mut merged: Vec<Rect> = Vec::new();
for r in regions {
let unioned = merged.last_mut().and_then(|m| {
let m_top = m.y + m.height;
let r_top = r.y + r.height;
if r.y <= m_top + 5.0 {
let new_y = m.y.min(r.y);
let new_top = m_top.max(r_top);
let new_x = m.x.min(r.x);
let new_right = (m.x + m.width).max(r.x + r.width);
*m = Rect::new(new_x, new_y, new_right - new_x, new_top - new_y);
Some(())
} else {
None
}
});
if unioned.is_none() {
merged.push(r);
}
}
merged
}
#[cfg(feature = "rendering")]
pub(crate) fn rasterize_music_regions(
doc: &PdfDocument,
page_idx: usize,
page_h_pt: f32,
) -> Vec<((f32, f32, f32, f32), Vec<u8>)> {
use crate::rendering::{render_page, ImageFormat as RFmt, RenderOptions};
let regions = find_music_regions(doc, page_idx);
if regions.is_empty() {
return Vec::new();
}
let bytes = doc.source_bytes.clone();
if bytes.is_empty() {
return Vec::new();
}
let doc_mut = match crate::document::PdfDocument::from_bytes(bytes) {
Ok(d) => d,
Err(_) => return Vec::new(),
};
let dpi: u32 = 150;
let opts = RenderOptions {
dpi,
format: RFmt::Png,
..Default::default()
};
let full = match render_page(&doc_mut, page_idx, &opts) {
Ok(i) => i,
Err(_) => return Vec::new(),
};
let full_img = match image::load_from_memory(&full.data) {
Ok(i) => i,
Err(_) => return Vec::new(),
};
let scale = dpi as f32 / 72.0;
let img_w = full_img.width();
let img_h = full_img.height();
let mut out = Vec::with_capacity(regions.len());
for r in regions {
let (x_pdf, y_pdf, w, h) = (r.x, r.y, r.width, r.height);
let top_y_pt = page_h_pt - (y_pdf + h);
let cx = (x_pdf * scale).round().max(0.0) as u32;
let cy = (top_y_pt * scale).round().max(0.0) as u32;
let cw = (w * scale).round().max(1.0) as u32;
let ch = (h * scale).round().max(1.0) as u32;
let x = cx.min(img_w.saturating_sub(1));
let y = cy.min(img_h.saturating_sub(1));
let cw = cw.min(img_w - x);
let ch = ch.min(img_h - y);
if cw == 0 || ch == 0 {
continue;
}
let cropped = full_img.crop_imm(x, y, cw, ch);
let mut buf = Vec::new();
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::ImageEncoder;
if PngEncoder::new_with_quality(&mut buf, CompressionType::Fast, FilterType::Sub)
.write_image(cropped.as_bytes(), cw, ch, cropped.color().into())
.is_err()
{
continue;
}
if buf.is_empty() {
continue;
}
out.push(((x_pdf, y_pdf, w, h), buf));
}
out
}
pub(crate) fn rect_contains_point(region: &Rect, cx: f32, cy: f32) -> bool {
cx >= region.x
&& cx <= region.x + region.width
&& cy >= region.y
&& cy <= region.y + region.height
}
pub(crate) fn rect_contains_bbox(region: &Rect, bbox: &Rect) -> bool {
let cx = bbox.x + bbox.width * 0.5;
let cy = bbox.y + bbox.height * 0.5;
cx >= region.x
&& cx <= region.x + region.width
&& cy >= region.y
&& cy <= region.y + region.height
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn music_font_needles_match_case_insensitive() {
assert!(is_music_font_name("Maestro"));
assert!(is_music_font_name("maestro"));
assert!(is_music_font_name("MAESTRO"));
assert!(is_music_font_name("XVSURQ+Maestro"));
assert!(is_music_font_name("ABCDEF+Bravura-Regular"));
assert!(is_music_font_name("Sonata"));
assert!(is_music_font_name("Emmentaler-20"));
}
#[test]
fn music_font_needles_reject_general_fonts() {
assert!(!is_music_font_name("Times New Roman"));
assert!(!is_music_font_name("Calibri"));
assert!(!is_music_font_name("Helvetica"));
assert!(!is_music_font_name("ABCDEF+TeXGyreTermes-Regular"));
assert!(!is_music_font_name("Arial"));
assert!(!is_music_font_name(""));
}
}