mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-08 14:14:47 +00:00
fix(text): persist OpenType numeral tags through PSD EngineData
Map lnum/pnum/tnum as EngineData bools and the width×height combos as FigureStyle (Krita/Illustrator), so Numerals choices survive PSD round-trip.
This commit is contained in:
@@ -667,9 +667,7 @@ fn toggle_opentype(s: &mut Session, p: &Value, tag: &'static str) -> Result<Valu
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let cur = representative
|
||||
.and_then(|id| st.doc.layer(id))
|
||||
.is_some_and(|l| matches!(&l.content, LayerContent::Text(t) if feature_on_at(t, start_char, tag)));
|
||||
let cur = representative.and_then(|id| st.doc.layer(id)).is_some_and(|l| matches!(&l.content, LayerContent::Text(t) if feature_on_at(t, start_char, tag)));
|
||||
let on = p.get("on").and_then(Value::as_bool).unwrap_or(!cur);
|
||||
with_text_targets(s, p, "Change OpenType Feature", |t, (a, b)| {
|
||||
style_range(t, a.min(b), a.max(b), &|st| set_feature(st, tag, on));
|
||||
@@ -909,63 +907,41 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!("type.openType.ornaments", "Ornaments", &["Type", "OpenType"], OT, has_format_target, |s, p| toggle_opentype(s, p, "ornm")),
|
||||
spec!("type.openType.ordinals", "Ordinals", &["Type", "OpenType"], OT, has_format_target, |s, p| toggle_opentype(s, p, "ordn")),
|
||||
spec!("type.openType.fractions", "Fractions", &["Type", "OpenType"], OT, has_format_target, |s, p| toggle_opentype(s, p, "frac")),
|
||||
spec!(
|
||||
"type.openType.numerals.default",
|
||||
"Default",
|
||||
&["Type", "OpenType", "Numerals"],
|
||||
FP,
|
||||
has_format_target,
|
||||
|s, p| set_numerals(s, p, NumeralsStyle::Default)
|
||||
),
|
||||
spec!("type.openType.numerals.default", "Default", &["Type", "OpenType", "Numerals"], FP, has_format_target, |s, p| set_numerals(
|
||||
s,
|
||||
p,
|
||||
NumeralsStyle::Default
|
||||
)),
|
||||
spec!("type.openType.numerals.lining", "Lining", &["Type", "OpenType", "Numerals"], FP, has_format_target, |s, p| {
|
||||
set_numerals(s, p, NumeralsStyle::Lining)
|
||||
}),
|
||||
spec!("type.openType.numerals.oldstyle", "Oldstyle", &["Type", "OpenType", "Numerals"], FP, has_format_target, |s, p| {
|
||||
set_numerals(s, p, NumeralsStyle::Oldstyle)
|
||||
}),
|
||||
spec!(
|
||||
"type.openType.numerals.proportional",
|
||||
"Proportional",
|
||||
&["Type", "OpenType", "Numerals"],
|
||||
FP,
|
||||
has_format_target,
|
||||
|s, p| set_numerals(s, p, NumeralsStyle::Proportional)
|
||||
),
|
||||
spec!("type.openType.numerals.proportional", "Proportional", &["Type", "OpenType", "Numerals"], FP, has_format_target, |s, p| set_numerals(
|
||||
s,
|
||||
p,
|
||||
NumeralsStyle::Proportional
|
||||
)),
|
||||
spec!("type.openType.numerals.tabular", "Tabular", &["Type", "OpenType", "Numerals"], FP, has_format_target, |s, p| {
|
||||
set_numerals(s, p, NumeralsStyle::Tabular)
|
||||
}),
|
||||
spec!(
|
||||
"type.openType.numerals.proportionalLining",
|
||||
"Proportional Lining",
|
||||
&["Type", "OpenType", "Numerals"],
|
||||
FP,
|
||||
has_format_target,
|
||||
|s, p| set_numerals(s, p, NumeralsStyle::ProportionalLining)
|
||||
),
|
||||
spec!(
|
||||
"type.openType.numerals.tabularLining",
|
||||
"Tabular Lining",
|
||||
&["Type", "OpenType", "Numerals"],
|
||||
FP,
|
||||
has_format_target,
|
||||
|s, p| set_numerals(s, p, NumeralsStyle::TabularLining)
|
||||
),
|
||||
spec!(
|
||||
"type.openType.numerals.proportionalOldstyle",
|
||||
"Proportional Oldstyle",
|
||||
&["Type", "OpenType", "Numerals"],
|
||||
FP,
|
||||
has_format_target,
|
||||
|s, p| set_numerals(s, p, NumeralsStyle::ProportionalOldstyle)
|
||||
),
|
||||
spec!(
|
||||
"type.openType.numerals.tabularOldstyle",
|
||||
"Tabular Oldstyle",
|
||||
&["Type", "OpenType", "Numerals"],
|
||||
FP,
|
||||
has_format_target,
|
||||
|s, p| set_numerals(s, p, NumeralsStyle::TabularOldstyle)
|
||||
),
|
||||
spec!("type.openType.numerals.proportionalLining", "Proportional Lining", &["Type", "OpenType", "Numerals"], FP, has_format_target, |s, p| {
|
||||
set_numerals(s, p, NumeralsStyle::ProportionalLining)
|
||||
}),
|
||||
spec!("type.openType.numerals.tabularLining", "Tabular Lining", &["Type", "OpenType", "Numerals"], FP, has_format_target, |s, p| set_numerals(
|
||||
s,
|
||||
p,
|
||||
NumeralsStyle::TabularLining
|
||||
)),
|
||||
spec!("type.openType.numerals.proportionalOldstyle", "Proportional Oldstyle", &["Type", "OpenType", "Numerals"], FP, has_format_target, |s, p| {
|
||||
set_numerals(s, p, NumeralsStyle::ProportionalOldstyle)
|
||||
}),
|
||||
spec!("type.openType.numerals.tabularOldstyle", "Tabular Oldstyle", &["Type", "OpenType", "Numerals"], FP, has_format_target, |s, p| set_numerals(
|
||||
s,
|
||||
p,
|
||||
NumeralsStyle::TabularOldstyle
|
||||
)),
|
||||
spec!("type.createWorkPath", "Create Work Path", &["Type"], LP, has_text, create_work_path),
|
||||
spec!("type.convertToShape", "Convert to Shape", &["Type"], LP, has_text, convert_to_shape),
|
||||
spec!("type.rasterizeTypeLayer", "Rasterize Type Layer", &["Type"], LP, has_text, |s, p| s.execute("type.rasterize", p.clone())),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::edit::{TEMPLATE_NODES, edited_object, fits, is_stub, names_one_frame, new_object, pad_frames, td_edited_object, with_modes};
|
||||
use super::import::{OPENTYPE_KEYS, TXT2_FEATURES, TXT2_STYLISTIC_SETS, kern_units, manual, object_frame, pair_runs, parse_txt2, postscript_for, real};
|
||||
use super::import::{
|
||||
OPENTYPE_KEYS, TXT2_FEATURES, TXT2_STYLISTIC_SETS, figure_style_from_features, kern_units, manual, object_frame, pair_runs, parse_txt2, postscript_for,
|
||||
real,
|
||||
};
|
||||
use super::{
|
||||
AntiAlias, Caps, CharStyle, Color, ColorMode, D, Descriptor, E, FontBaseline, Id, Kerning, Orientation, ParagraphStyle, TextAlign, TextLayer, TextShape,
|
||||
TySh, UnicodeString, ed, engine_data, enum_value, enumv, parse_tysh, write_tysh,
|
||||
@@ -25,6 +28,11 @@ pub(crate) fn style_sheet_data(s: &CharStyle, font: usize, k: f32) -> E {
|
||||
.filter(|(_, tag)| s.features.iter().any(|f| f.tag == *tag && f.value > 0))
|
||||
.map(|(key, _)| (key.to_string(), E::Bool(true)))
|
||||
.collect();
|
||||
// Exact width×height numeral combos also go out as `/FigureStyle` for Krita / Illustrator.
|
||||
let figure = figure_style_from_features(&s.features);
|
||||
if figure != 0 {
|
||||
opentype.push(("FigureStyle".into(), E::Int(figure)));
|
||||
}
|
||||
let mut dict = vec![
|
||||
("Font".into(), E::Int(font as i64)),
|
||||
("FontSize".into(), real(s.size_pt / k)),
|
||||
|
||||
@@ -588,7 +588,15 @@ pub(crate) fn char_style(base: &[Option<&E>], d: &E, fonts: &[String], k: f32) -
|
||||
s.discretionary_ligatures = flag("DLigatures").unwrap_or(false);
|
||||
for (key, tag) in OPENTYPE_KEYS {
|
||||
if flag(key) == Some(true) {
|
||||
s.features.push(photocraft_doc::text::FontFeature { tag: tag.to_string(), value: 1 });
|
||||
push_feature(&mut s, tag);
|
||||
}
|
||||
}
|
||||
// Krita / Illustrator write `/FigureStyle` as an int for the four width×height numeral combos
|
||||
// (Photoshop's UI historically only toggled OldStyle). Expand it after the bool keys so a
|
||||
// file that carries both does not duplicate tags.
|
||||
if let Some(code) = g("FigureStyle").and_then(E::as_i64) {
|
||||
for tag in figure_style_tags(code) {
|
||||
push_feature(&mut s, tag);
|
||||
}
|
||||
}
|
||||
s.kerning = if flag("AutoKerning") == Some(false) { Kerning::Off } else { Kerning::Metrics };
|
||||
@@ -789,13 +797,71 @@ pub(crate) fn postscript_for(s: &CharStyle) -> String {
|
||||
|
||||
/// EngineData style keys for the Type › OpenType toggles and the OpenType feature each one turns
|
||||
/// on (the standard and discretionary ligature keys map to dedicated [`CharStyle`] fields).
|
||||
pub const OPENTYPE_KEYS: [(&str, &str); 8] = [
|
||||
///
|
||||
/// `lnum` / `pnum` / `tnum` are written as bools so PhotoCraft round-trips every Numerals menu
|
||||
/// choice; Photoshop's EngineData historically only had `OldStyle` (`onum`), with the four
|
||||
/// width×height combos in the integer [`figure_style_from_features`] / `FigureStyle` field.
|
||||
pub const OPENTYPE_KEYS: [(&str, &str); 11] = [
|
||||
("ContextualLigatures", "calt"),
|
||||
("Swash", "swsh"),
|
||||
("OldStyle", "onum"),
|
||||
("LiningFigures", "lnum"),
|
||||
("ProportionalFigures", "pnum"),
|
||||
("TabularFigures", "tnum"),
|
||||
("StylisticAlternates", "salt"),
|
||||
("Titling", "titl"),
|
||||
("Ornaments", "ornm"),
|
||||
("Ordinals", "ordn"),
|
||||
("Fractions", "frac"),
|
||||
];
|
||||
|
||||
/// Tags enabled by EngineData `/FigureStyle` (Krita / Illustrator): 0 none, 1 tabular+lining,
|
||||
/// 2 proportional+oldstyle, 3 proportional+lining, 4 tabular+oldstyle.
|
||||
pub fn figure_style_tags(code: i64) -> &'static [&'static str] {
|
||||
match code {
|
||||
1 => &["tnum", "lnum"],
|
||||
2 => &["pnum", "onum"],
|
||||
3 => &["pnum", "lnum"],
|
||||
4 => &["tnum", "onum"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverse of [`figure_style_tags`]: exact Numerals combos only (singles stay on the bool keys).
|
||||
pub fn figure_style_from_features(features: &[photocraft_doc::text::FontFeature]) -> i64 {
|
||||
let on = |tag: &str| features.iter().any(|f| f.tag == tag && f.value > 0);
|
||||
match (on("tnum"), on("lnum"), on("pnum"), on("onum")) {
|
||||
(true, true, false, false) => 1,
|
||||
(false, false, true, true) => 2,
|
||||
(false, true, true, false) => 3,
|
||||
(true, false, false, true) => 4,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn push_feature(s: &mut CharStyle, tag: &str) {
|
||||
if !s.features.iter().any(|f| f.tag == tag) {
|
||||
s.features.push(photocraft_doc::text::FontFeature { tag: tag.to_string(), value: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod figure_style_tests {
|
||||
use super::{figure_style_from_features, figure_style_tags};
|
||||
use photocraft_doc::text::FontFeature;
|
||||
|
||||
fn feats(tags: &[&str]) -> Vec<FontFeature> {
|
||||
tags.iter().map(|t| FontFeature { tag: (*t).into(), value: 1 }).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn figure_style_round_trips_exact_combos() {
|
||||
for code in 1..=4 {
|
||||
let tags = figure_style_tags(code);
|
||||
assert_eq!(figure_style_from_features(&feats(tags)), code);
|
||||
}
|
||||
assert_eq!(figure_style_from_features(&feats(&["lnum"])), 0);
|
||||
assert_eq!(figure_style_from_features(&feats(&["onum"])), 0);
|
||||
assert_eq!(figure_style_tags(0), &[] as &[&str]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1732,6 +1732,29 @@ fn psd_round_trips_antialias_opentype_and_warp() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Numerals menu tags (`lnum` / `onum` / `pnum` / `tnum`) survive TySh EngineData — bool keys for
|
||||
/// singles, plus `/FigureStyle` for the four width×height combos (Krita / Illustrator).
|
||||
#[test]
|
||||
fn psd_round_trips_opentype_numeral_features() {
|
||||
let cases: &[&[&str]] = &[&["lnum"], &["onum"], &["pnum"], &["tnum"], &["lnum", "tnum"], &["onum", "pnum"], &["lnum", "pnum"], &["onum", "tnum"]];
|
||||
for tags in cases {
|
||||
let style = CharStyle {
|
||||
font_family: "Inter".into(),
|
||||
size_pt: 20.0,
|
||||
features: tags.iter().map(|t| FontFeature { tag: (*t).into(), value: 1 }).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let t = styled("0123456789", style);
|
||||
let bytes = crate::psd::build_tysh(&t, 72.0, None);
|
||||
let back = crate::psd::text_layer_from_tysh(&bytes, 72.0).expect("tysh");
|
||||
let runs = back.char_runs();
|
||||
let mut got: Vec<String> = runs[0].style.features.iter().map(|f| f.tag.clone()).collect();
|
||||
got.sort_unstable();
|
||||
let want: Vec<String> = tags.iter().map(|t| (*t).to_string()).collect();
|
||||
assert_eq!(got, want, "numeral tags {tags:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A legacy-signature `TySh` (#1469): zero PointBase, `bounds` (the layout box) and `boundingBox`
|
||||
/// (the ink box, as Photoshop writes it) both around `anchor` in a remote local space, and a
|
||||
/// `transform` that cancels the remote origin.
|
||||
|
||||
Reference in New Issue
Block a user