fontmerge 0.2.1

Merge two font source files, copying glyphs and OpenType layout features from one font to another
Documentation
use babelfont::{Font, Glyph, LayerType};
use fontdrasil::types::Axes;
use indexmap::IndexSet;

use crate::{
    designspace::{
        Strategy, convert_between_designspaces, fontdrasil_axes, nearest_master_id, within_bounds,
    },
    error::FontmergeError,
};

pub(crate) fn merge_glyph(
    font1: &mut Font,
    font1_nonsparse_master_ids: &[String],
    font2_glyph: &Glyph,
    font2_axes: &Axes,
    font2: &Font,
    strategies: &[Strategy],
) -> Result<(), FontmergeError> {
    let font1_axes = fontdrasil_axes(&font1.axes)?;
    // If this glyph already exists in font1, we only get here because the caller has decided
    // to replace it (existing_handling == Skip is filtered out before merge_glyph is called),
    // so it should be overwritten wholesale with font2's glyph, exactly as if it were new.
    if let Some(existing) = font1.glyphs.get_mut(&font2_glyph.name) {
        *existing = font2_glyph.clone();
    } else {
        font1.glyphs.push(font2_glyph.clone());
    }
    #[allow(clippy::unwrap_used)] // We just inserted or replaced this glyph above
    let glyph = font1.glyphs.get_mut(&font2_glyph.name).unwrap();
    // Move layers out
    let mut layers = std::mem::take(&mut glyph.layers);
    // Ensure all layers have an ID
    for (i, layer) in layers.iter_mut().enumerate() {
        if layer.id.is_none() {
            layer.id = Some(format!("{}_layer_{}", glyph.name, i));
        }
    }
    log::debug!(
        "Existing locations for this glyph: {:?}",
        layers
            .iter()
            .filter_map(|l| l.location.as_ref())
            .collect::<Vec<_>>()
    );
    let mut drop_layers = IndexSet::new();
    for (master_id, strategy) in font1_nonsparse_master_ids.iter().zip(strategies.iter()) {
        let master = font1
            .masters
            .iter()
            .find(|m| &m.id == master_id)
            .expect("Master ID from non-sparse list not found in font1 masters");
        let new_layer = match strategy {
            Strategy::Exact {
                layer: id,
                master_name: _,
                clamped,
            } => {
                // Find a layer in layers with this master ID, either in master_id or layer id
                let layer = layers.iter().find(|l| {
                    l.master == LayerType::DefaultForMaster(id.to_string())
                        || l.master == LayerType::AssociatedWithMaster(id.to_string())
                        || l.id.as_ref().map(|lid| lid == id).unwrap_or(false)
                });
                if let Some(l) = layer {
                    // We can unwrap here because we set IDs above
                    if !clamped {
                        #[allow(clippy::unwrap_used)]
                        drop_layers.insert(l.id.clone().unwrap());
                    }
                    Some(l.clone())
                } else {
                    log::warn!(
                        "No layer found for glyph '{}' matching master ID '{}'",
                        glyph.name,
                        id
                    );
                    None
                }
            }
            Strategy::InterpolateOrIntermediate {
                location,
                clamped: _,
            } => {
                log::info!(
                    "Interpolating glyph '{}' at location {:?}",
                    glyph.name,
                    location
                );
                Some(
                    font2
                        .interpolate_glyph(&glyph.name, location)
                        .map_err(|e| {
                            FontmergeError::Interpolation(format!(
                                "Failed to interpolate glyph '{}' at location {:?}: {}",
                                glyph.name, location, e
                            ))
                        })?,
                )
            }
            Strategy::Failed(reason) => {
                log::warn!(
                    "Skipping layer for glyph '{}' for master '{}' due to failed strategy: {}",
                    glyph.name,
                    master.id,
                    reason
                );
                None
            }
        };
        let mut new_layer = match new_layer {
            Some(l) => l,
            None => continue,
        };
        // Set its to have the correct master ID
        new_layer.id = Some(master.id.clone());
        new_layer.master = LayerType::DefaultForMaster(master.id.clone());
        new_layer.location = None; // Not needed now we have exact match
        glyph.layers.push(new_layer);
    }
    // Remove used layers, let's see what's left
    let remaining_layers = layers
        .into_iter()
        .filter(|l| l.id.as_ref().is_some_and(|id| !drop_layers.contains(id)))
        .collect::<Vec<_>>();
    log::debug!(
        "After merging, glyph still '{}' has {} layers remaining (dropped {})",
        font2_glyph.name,
        remaining_layers.len(),
        drop_layers.len()
    );
    // These layers all have locations. If, once translated into font1's design space, they
    // lie within its bounds, shove them on as intermediate (non-master) layers.
    let mut remaining_layers = remaining_layers
        .into_iter()
        .filter(|l| l.location.is_some())
        .collect::<Vec<_>>();
    #[allow(clippy::unwrap_used)] // We filtered out layers without a location above
    for layer in remaining_layers.iter_mut() {
        // These locations are in the design space of font2. We're going to move them into font1, so what
        // we need to do is:
        // * Convert them to user space,
        // * Then fill in any missing axes with defaults from font1 and remove any axes not in font1,
        // * Then convert to design space of font1.
        // We don't clamp here, because we want to drop layers which fall outside font 1's designspace.
        let (loc_in_font1, _) = convert_between_designspaces(
            layer.location.as_ref().unwrap(),
            font2_axes,
            &font1_axes,
            false,
        )?;
        if !within_bounds(&font1_axes, &loc_in_font1) {
            log::debug!(
                "Not adding remaining layer at location {:?} to glyph '{}' because it is outside font 1's designspace",
                layer.location.as_ref().unwrap(),
                glyph.name
            );
            continue;
        }

        // This is an intermediate layer, not a master, so associate it with the nearest
        // real master in font1.
        layer.master = match nearest_master_id(&font1.masters, &font1_axes, &loc_in_font1) {
            Some(id) => LayerType::AssociatedWithMaster(id),
            None => LayerType::FreeFloating,
        };

        // Make double double sure there isn't a font master at this location already in font1
        let user_loc_in_font1 = loc_in_font1.to_user(&font1_axes)?;
        if font1
            .masters
            .iter()
            .any(|m| m.location.to_user(&font1_axes).unwrap_or_default() == user_loc_in_font1)
        {
            log::debug!(
                "Not adding remaining layer at location {:?} to glyph '{}' because a master already exists there",
                layer.location.as_ref().unwrap(),
                glyph.name
            );
            continue;
        }
        layer.location = Some(loc_in_font1);

        // Check we haven't already added a layer at this location.
        let loc = layer.location.as_ref().unwrap();
        let already_exists = glyph
            .layers
            .iter()
            .any(|l| l.location.as_ref().is_some_and(|l2| l2 == loc));
        if already_exists {
            continue;
        }
        log::debug!(
            "Adding remaining layer at location {:?} to glyph '{}' as intermediate layer",
            layer.location.as_ref().unwrap(),
            glyph.name
        );
        glyph.layers.push(layer.clone());
    }

    Ok(())
}