Find Font: Change and Change All leave hidden and locked text alone

Squashed from #4852 onto main.
This commit is contained in:
dotCooCoo
2026-10-31 19:43:15 +07:00
committed by bflatastic
parent 875d3301f5
commit 1f31b6ed4a
28 changed files with 124 additions and 3 deletions
+82 -3
View File
@@ -29,7 +29,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Replace Font",
[],
None,
"{from: {family, style?}, to: {family (any of its names; stored as the font's own family name), style?}, selectionOnly?} (style omitted: every style of the family / keep the closest style) → {runs}",
"{from: {family, style?}, to: {family (any of its names; stored as the font's own family name), style?}, selectionOnly?} (style omitted: every style of the family / keep the closest style); text that is hidden or locked, in a hidden or locked group or layer, or on a locked artboard keeps its font → {runs, skipped?: the text objects left in the font, warning?}",
has_doc,
replace
),
@@ -414,10 +414,18 @@ fn replace(s: &mut Session, p: &Value) -> Result<Value> {
Some((f, m)) if m != vectorcraft_text::FontMatch::Missing => f.family.clone(),
_ => return Err(bad(C, format!("font family `{tf}` is not available"))),
};
let ids = scope(s, bool_or(p, "selectionOnly", false))?;
let matches = |st: &vectorcraft_doc::CharStyle| {
st.font_family.eq_ignore_ascii_case(&ff) && fs.as_ref().is_none_or(|x| st.font_style.eq_ignore_ascii_case(x))
};
// Only text the Selection tool can reach changes: not hidden or locked, in a hidden or locked
// group or layer, or on a locked artboard. The text left in the font is counted.
let (ids, skipped) = {
let d = &s.doc()?.doc;
let editable = d.editable();
let (ids, other): (Vec<NodeId>, Vec<NodeId>) = scope(s, bool_or(p, "selectionOnly", false))?.into_iter().partition(|id| editable(*id));
let skipped = other.into_iter().filter(|id| text(d, *id).is_some_and(|t| t.runs.iter().any(|r| matches(&r.style)))).count();
(ids, skipped)
};
let n = s.edit("Replace Font", |d, _| {
let mut n = 0;
for id in &ids {
@@ -446,9 +454,17 @@ fn replace(s: &mut Session, p: &Value) -> Result<Value> {
}
Ok(n)
})?;
Ok(json!({ "runs": n }))
let mut out = json!({ "runs": n });
if skipped > 0 {
out["skipped"] = json!(skipped);
out["warning"] = json!(REPLACE_SKIPPED);
}
Ok(out)
}
/// What Replace Font says when it left text in the font that it can't reach.
const REPLACE_SKIPPED: &str = "some text is hidden or locked, so its font wasn't replaced";
fn select_font(s: &mut Session, p: &Value) -> Result<Value> {
let family = str_param(p, "family").ok_or_else(|| bad("select.font", "missing `family`"))?.to_string();
let style = str_param(p, "style").map(str::to_string);
@@ -621,6 +637,69 @@ mod tests {
assert_eq!(s.execute("select.font", &json!({"family": "Inter"})).unwrap()["count"], 0);
assert!(s.doc().unwrap().selection.objects.is_empty());
}
/// Replace Font leaves hidden and locked text in its font, and text inside a hidden or locked
/// group or layer, and says so. Text on a visible, unlocked template layer is changed.
#[test]
fn replace_font_skips_hidden_and_locked_text() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
let text = |s: &mut Session, y: i32| {
s.execute("text.create", &json!({"x": 10, "y": y, "text": "Serif", "font": "Source Serif 4"})).unwrap()["id"].as_u64().unwrap()
};
let mut ids = vec![];
for y in [20, 40, 60, 80, 100] {
ids.push(text(&mut s, y));
}
for (id, group, cmd) in
[(ids[1], false, "object.hide"), (ids[2], false, "object.lock"), (ids[3], true, "object.hide"), (ids[4], true, "object.lock")]
{
s.execute("select.set", &json!({"ids": [id]})).unwrap();
if group {
s.execute("object.group", &json!({})).unwrap();
}
s.execute(cmd, &json!({})).unwrap();
}
let mut layer_text = |props: &[Value]| {
let layer = s.execute("layer.new", &json!({})).unwrap()["id"].as_u64().unwrap();
let id = text(&mut s, 140);
for p in props {
let mut p = p.clone();
p["id"] = json!(layer);
s.execute("layer.setProps", &p).unwrap();
}
id
};
ids.push(layer_text(&[json!({"visible": false})]));
ids.push(layer_text(&[json!({"locked": true})]));
// Template on locks the layer; unlocking it makes its text editable again.
let template = layer_text(&[json!({"template": true}), json!({"locked": false})]);
let family = |s: &Session, id: u64| text_family(s, id);
let r = s.execute("text.replaceFont", &json!({"from": {"family": "Source Serif 4"}, "to": {"family": "Source Sans 3"}})).unwrap();
assert_eq!(r["runs"], 2, "{r}");
assert_eq!(r["skipped"], 6, "{r}");
assert_eq!(r["warning"], REPLACE_SKIPPED, "{r}");
assert_eq!(family(&s, ids[0]), "Source Sans 3");
assert_eq!(family(&s, template), "Source Sans 3");
for id in &ids[1..] {
assert_eq!(family(&s, *id), "Source Serif 4", "object {id}");
}
// The font stays in the document's list while the skipped text uses it.
let fams: Vec<String> =
s.execute("text.fonts", &json!({})).unwrap().as_array().unwrap().iter().map(|f| f["family"].as_str().unwrap().to_string()).collect();
assert_eq!(fams, ["Source Sans 3", "Source Serif 4"]);
// Only skipped text left: nothing changes, and the warning says why.
let r = s.execute("text.replaceFont", &json!({"from": {"family": "Source Serif 4"}, "to": {"family": "Source Sans 3"}})).unwrap();
assert_eq!((r["runs"].clone(), r["skipped"].clone(), r["warning"].clone()), (json!(0), json!(6), json!(REPLACE_SKIPPED)), "{r}");
// A plain replacement carries no warning.
let r = s.execute("text.replaceFont", &json!({"from": {"family": "Source Sans 3"}, "to": {"family": "Inter"}})).unwrap();
assert_eq!(r, json!({"runs": 2}));
}
/// The font family of text object `id`'s first run.
fn text_family(s: &Session, id: u64) -> String {
text(&s.doc().unwrap().doc, NodeId(id)).unwrap().runs[0].style.font_family.clone()
}
}
#[cfg(test)]
+16
View File
@@ -182,6 +182,22 @@ mod tests {
assert_eq!(fonts(&mut app)[0]["family"], "Source Sans 3");
}
/// Change All leaves locked text in its font, and the status bar says so.
#[test]
fn change_all_says_when_locked_text_keeps_its_font() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
app.session.execute("text.create", &json!({"x": 10, "y": 40, "text": "Hi", "font": "Missing Family"})).unwrap();
app.session.execute("object.lock", &json!({})).unwrap();
open(&mut app);
let d = app.ui.dialog.clone().unwrap();
let from = fonts(&mut app)[0].clone();
let r = change(&mut app, &d, &from, true).unwrap();
assert_eq!(r["runs"], 0, "{r}");
assert_eq!(fonts(&mut app)[0]["family"], "Missing Family");
assert_eq!(app.ui.status, "some text is hidden or locked, so its font wasn't replaced");
}
#[test]
fn find_in_folder_puts_the_missing_fonts_dialog_in_find_fonts_place() {
let dir = std::fs::canonicalize(vectorcraft_testkit::temp_dir("find-font-folder")).unwrap();
+1
View File
@@ -4091,6 +4091,7 @@ axis Both И двете
@msg no such perspective grid preset няма такава предварителна настройка за перспективна мрежа
@msg no such recent file няма такъв скорошен файл
@msg no such recent font няма такъв скорошен шрифт
@msg some text is hidden or locked, so its font wasn't replaced част от текста е скрит или заключен, затова шрифтът му не беше заменен
@msg no such saved selection няма такава запазена селекция
@msg no such segment няма такъв сегмент
@msg no such spine point няма такава точка от оста
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4480,6 +4480,7 @@ imageTrace Outline Obris
@msg no such perspective grid preset nema takvog predloška mreže perspektive
@msg no such recent file nema takve nedavne datoteke
@msg no such recent font nema takvog nedavnog fonta
@msg some text is hidden or locked, so its font wasn't replaced dio teksta je skriven ili zaključan, pa njegov font nije zamijenjen
@msg no such saved selection nema takvog sačuvanog odabira
@msg no such segment nema takvog segmenta
@msg no such spine point nema takve tačke kičme
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5170,6 +5170,7 @@
@msg no such perspective grid preset no existeix aquest preajust de quadrícula de perspectiva
@msg no such recent file no existeix aquest fitxer recent
@msg no such recent font no existeix aquest tipus de lletra recent
@msg some text is hidden or locked, so its font wasn't replaced part del text està amagat o bloquejat, així que el seu tipus de lletra no s'ha substituït
@msg no such saved selection no existeix aquesta selecció desada
@msg no such segment no existeix aquest segment
@msg no such spine point no existeix aquest punt de l’espina
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4517,6 +4517,7 @@ imageTrace Outline Obrys
@msg no such perspective grid preset takové přednastavení perspektivní mřížky neexistuje
@msg no such recent file takový nedávný soubor neexistuje
@msg no such recent font takové nedávné písmo neexistuje
@msg some text is hidden or locked, so its font wasn't replaced některý text je skrytý nebo zamčený, a proto jeho písmo nebylo nahrazeno
@msg no such saved selection takový uložený výběr neexistuje
@msg no such segment takový segment neexistuje
@msg no such spine point takový bod páteře neexistuje
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4199,6 +4199,7 @@ imageTrace Outline Kontur
@msg no such perspective grid preset ingen sådan perspektivgitterforudindstilling
@msg no such recent file ingen sådan seneste fil
@msg no such recent font ingen sådan seneste skrifttype
@msg some text is hidden or locked, so its font wasn't replaced noget tekst er skjult eller låst, så dens skrifttype blev ikke erstattet
@msg no such saved selection ingen sådan gemt markering
@msg no such segment intet sådant segment
@msg no such subpath ingen sådan delsti
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4131,6 +4131,7 @@ theme Appearance Erscheinungsbild
@msg no such perspective grid preset keine solche Vorgabe für Perspektivenraster
@msg no such recent file keine solche zuletzt geöffnete Datei
@msg no such recent font keine solche zuletzt verwendete Schrift
@msg some text is hidden or locked, so its font wasn't replaced einige Texte sind ausgeblendet oder gesperrt, daher wurde ihre Schrift nicht ersetzt
@msg no such saved selection keine solche gespeicherte Auswahl
@msg no such segment kein solches Segment
@msg no such spine point kein solcher Achsenpunkt
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4170,6 +4170,7 @@ theme Appearance Εμφάνιση
@msg no such perspective grid preset δεν υπάρχει τέτοια προεπιλογή πλέγματος προοπτικής
@msg no such recent file δεν υπάρχει τέτοιο πρόσφατο αρχείο
@msg no such recent font δεν υπάρχει τέτοια πρόσφατη γραμματοσειρά
@msg some text is hidden or locked, so its font wasn't replaced κάποιο κείμενο είναι κρυφό ή κλειδωμένο, οπότε η γραμματοσειρά του δεν αντικαταστάθηκε
@msg no such saved selection δεν υπάρχει τέτοια αποθηκευμένη επιλογή
@msg no such segment δεν υπάρχει τέτοιο τμήμα
@msg no such spine point δεν υπάρχει τέτοιο σημείο κεντρικής καμπύλης
Can't render this file because it contains an unexpected character in line 164 and column 5.
+1
View File
@@ -4210,6 +4210,7 @@ axis Both Ambos
@msg no such perspective grid preset no existe ese preajuste de cuadrícula de perspectiva
@msg no such recent file no existe ese archivo reciente
@msg no such recent font no existe esa fuente reciente
@msg some text is hidden or locked, so its font wasn't replaced parte del texto está oculto o bloqueado, así que su fuente no se ha reemplazado
@msg no such saved selection no existe esa selección guardada
@msg no such segment no existe ese segmento
@msg no such spine point no existe ese punto del eje
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4426,6 +4426,7 @@ imageTrace Outline Ääriviiva
@msg no such perspective grid preset perspektiiviruudukon esiasetusta ei ole
@msg no such recent file viimeisintä tiedostoa ei ole
@msg no such recent font viimeisintä fonttia ei ole
@msg some text is hidden or locked, so its font wasn't replaced osa tekstistä on piilotettu tai lukittu, joten sen fonttia ei korvattu
@msg no such saved selection tallennettua valintaa ei ole
@msg no such segment segmenttiä ei ole
@msg no such spine point selkärangan pistettä ei ole
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4207,6 +4207,7 @@ axis Both Les deux
@msg no such perspective grid preset paramètre prédéfini de grille de perspective inexistant
@msg no such recent file fichier récent inexistant
@msg no such recent font police récente inexistante
@msg some text is hidden or locked, so its font wasn't replaced une partie du texte est masquée ou verrouillée : sa police n'a pas été remplacée
@msg no such saved selection sélection enregistrée inexistante
@msg no such segment segment inexistant
@msg no such spine point point de la dorsale inexistant
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4874,6 +4874,7 @@ imageTrace Outline Körvonal
@msg no such perspective grid preset nincs ilyen perspektivikusrács-előbeállítás
@msg no such recent file nincs ilyen legutóbbi fájl
@msg no such recent font nincs ilyen legutóbbi betűtípus
@msg some text is hidden or locked, so its font wasn't replaced néhány szöveg rejtett vagy zárolt, ezért a betűtípusa nem lett lecserélve
@msg no such saved selection nincs ilyen mentett kijelölés
@msg no such segment nincs ilyen szakasz
@msg no such spine point nincs ilyen gerincpont
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4199,6 +4199,7 @@ imageTrace Outline Kontur
@msg no such perspective grid preset tidak ada preset kisi perspektif seperti itu
@msg no such recent file tidak ada berkas terbaru seperti itu
@msg no such recent font tidak ada font terbaru seperti itu
@msg some text is hidden or locked, so its font wasn't replaced sebagian teks tersembunyi atau terkunci, sehingga fontnya tidak diganti
@msg no such saved selection tidak ada seleksi tersimpan seperti itu
@msg no such segment tidak ada segmen seperti itu
@msg no such subpath tidak ada subjalur seperti itu
Can't render this file because it contains an unexpected character in line 2168 and column 5.
+1
View File
@@ -4250,6 +4250,7 @@ axis Both Entrambi
@msg no such perspective grid preset preset di griglia prospettica inesistente
@msg no such recent file file recente inesistente
@msg no such recent font font recente inesistente
@msg some text is hidden or locked, so its font wasn't replaced parte del testo è nascosta o bloccata, quindi il suo font non è stato sostituito
@msg no such saved selection selezione salvata inesistente
@msg no such segment segmento inesistente
@msg no such spine point punto della dorsale inesistente
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4544,6 +4544,7 @@ swatch Bright Violet ビビッドバイオレット
@msg no such perspective grid preset 指定された遠近グリッドプリセットが見つかりません
@msg no such recent file 指定された最近使ったファイルが見つかりません
@msg no such recent font 指定された最近使ったフォントが見つかりません
@msg some text is hidden or locked, so its font wasn't replaced 一部のテキストが非表示またはロックされているため、そのフォントは置換されませんでした
@msg no such saved selection 指定された選択範囲は保存されていません
@msg no such user library 指定されたユーザーライブラリが見つかりません
@msg no such view 指定された表示が見つかりません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3986,6 +3986,7 @@ font-technology Variable ცვლადი
@msg no such perspective grid preset პერსპექტივის ბადის ასეთი პრესეტი არ არის
@msg no such recent file ასეთი ბოლოს გახსნილი ფაილი არ არის
@msg no such recent font ასეთი ბოლოდროინდელი შრიფტი არ არის
@msg some text is hidden or locked, so its font wasn't replaced ზოგიერთი ტექსტი დამალული ან ჩაკეტილია, ამიტომ მისი შრიფტი არ შეიცვალა
@msg no such saved selection ასეთი შენახული მონიშვნა არ არის
@msg no such segment ასეთი სეგმენტი არ არის
@msg no such subpath ასეთი ქვეტრაექტორია არ არის
Can't render this file because it is too large.
+1
View File
@@ -4472,6 +4472,7 @@ theme Appearance 외관
@msg no such perspective grid preset 해당 원근 격자 프리셋이 없습니다
@msg no such recent file 해당 최근 파일이 없습니다
@msg no such recent font 해당 최근 글꼴이 없습니다
@msg some text is hidden or locked, so its font wasn't replaced 일부 텍스트가 숨겨져 있거나 잠겨 있어 해당 글꼴을 바꾸지 않았습니다
@msg no such saved selection 해당 저장된 선택 영역이 없습니다
@msg no such user library 해당 사용자 라이브러리가 없습니다
@msg no such view 해당 보기가 없습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4211,6 +4211,7 @@ imageTrace Outline Contour
@msg no such perspective grid preset perspectiefrastervoorinstelling bestaat niet
@msg no such recent file recent bestand bestaat niet
@msg no such recent font recent lettertype bestaat niet
@msg some text is hidden or locked, so its font wasn't replaced een deel van de tekst is verborgen of vergrendeld, dus het lettertype ervan is niet vervangen
@msg no such saved selection opgeslagen selectie bestaat niet
@msg no such segment segment bestaat niet
@msg no such subpath subpad bestaat niet
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4341,6 +4341,7 @@ imageTrace Outline Kontur
@msg no such perspective grid preset brak takiego ustawienia siatki perspektywy
@msg no such recent file brak takiego ostatniego pliku
@msg no such recent font brak takiej ostatniej czcionki
@msg some text is hidden or locked, so its font wasn't replaced część tekstu jest ukryta lub zablokowana, więc jego czcionka nie została zastąpiona
@msg no such saved selection brak takiego zapisanego zaznaczenia
@msg no such segment brak takiego segmentu
@msg no such spine point brak takiego punktu grzbietu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3787,6 +3787,7 @@ axis Both Ambos
@msg no such perspective grid preset predefinição de grade de perspectiva inexistente
@msg no such recent file arquivo recente inexistente
@msg no such recent font fonte recente inexistente
@msg some text is hidden or locked, so its font wasn't replaced parte do texto está oculta ou bloqueada, então sua fonte não foi substituída
@msg no such saved selection seleção salva inexistente
@msg no such segment segmento inexistente
@msg no such spine point ponto da coluna inexistente
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -4246,6 +4246,7 @@ axis Both Оба
@msg no such perspective grid preset такого набора сетки перспективы нет
@msg no such recent file такого последнего файла нет
@msg no such recent font такого последнего шрифта нет
@msg some text is hidden or locked, so its font wasn't replaced часть текста скрыта или заблокирована, поэтому её шрифт не заменён
@msg no such saved selection такого сохранённого выделения нет
@msg no such segment такого сегмента нет
@msg no such spine point такой точки остова нет
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4121,6 +4121,7 @@ axis Both Båda
@msg no such perspective grid preset förinställningen för perspektivrutnät finns inte
@msg no such recent file den senaste filen finns inte
@msg no such recent font det senaste typsnittet finns inte
@msg some text is hidden or locked, so its font wasn't replaced en del text är dold eller låst, så dess typsnitt ersattes inte
@msg no such saved selection den sparade markeringen finns inte
@msg no such segment segmentet finns inte
@msg no such spine point ryggradspunkten finns inte
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -3983,6 +3983,7 @@ axis Both Her İkisi
@msg no such perspective grid preset böyle bir perspektif ızgarası ön ayarı yok
@msg no such recent file böyle yeni bir dosya yok
@msg no such recent font böyle yeni bir yazı tipi yok
@msg some text is hidden or locked, so its font wasn't replaced bazı metinler gizli veya kilitli olduğundan yazı tipleri değiştirilmedi
@msg no such saved selection böyle kayıtlı bir seçim yok
@msg no such segment öyle bir bölüm yok
@msg no such subpath böyle bir alt yol yok
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -4234,6 +4234,7 @@ axis Both Обидві
@msg no such perspective grid preset такого набору сітки перспективи немає
@msg no such recent file такого останнього файлу немає
@msg no such recent font такого останнього шрифту немає
@msg some text is hidden or locked, so its font wasn't replaced частина тексту прихована або заблокована, тому її шрифт не замінено
@msg no such saved selection такого збереженого виділення немає
@msg no such segment такого сегменту немає
@msg no such spine point такої точки осі немає
Can't render this file because it contains an unexpected character in line 178 and column 5.
+1
View File
@@ -4523,6 +4523,7 @@ theme Appearance Giao diện
@msg no such perspective grid preset không có thiết lập sẵn lưới phối cảnh như vậy
@msg no such recent file không có tệp gần đây như vậy
@msg no such recent font không có phông chữ gần đây như vậy
@msg some text is hidden or locked, so its font wasn't replaced một số văn bản bị ẩn hoặc bị khóa nên phông chữ của chúng không được thay thế
@msg no such saved selection không có vùng chọn đã lưu như vậy
@msg no such segment không có đoạn như vậy
@msg no such spine point không có điểm trục như vậy
Can't render this file because it contains an unexpected character in line 200 and column 5.
+1
View File
@@ -4261,6 +4261,7 @@ imageTrace Outline 轮廓
@msg no such perspective grid preset 没有此透视网格预设
@msg no such recent file 没有此最近使用的文件
@msg no such recent font 没有此最近使用的字体
@msg some text is hidden or locked, so its font wasn't replaced 部分文字已隐藏或锁定,因此其字体未被替换
@msg no such saved selection 没有此已保存的选区
@msg no such user library 没有此用户库
@msg no such view 没有此视图
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2201,6 +2201,7 @@ file-filter Templates 範本
@msg no such perspective grid preset 沒有這個透視格點預設集
@msg no such recent file 沒有這個最近使用的檔案
@msg no such recent font 沒有這個最近使用的字體
@msg some text is hidden or locked, so its font wasn't replaced 部分文字已隱藏或鎖定,因此其字體未被取代
@msg no such saved selection 沒有這個已儲存的選取範圍
@msg no such user library 沒有這個使用者資料庫
@msg no such view 沒有這個檢視
Can't render this file because it contains an unexpected character in line 6 and column 47.