Merge pull request #782 from dotCooCoo/feat/package-font-resolution
Package: fonts found by any of their names are copied, and skipped fonts are named
This commit is contained in:
commit
116573ba86
|
|
@ -7,6 +7,7 @@ use serde_json::{Value, json};
|
|||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{ColorMode, Document, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::{Affine, Rect};
|
||||
use vectorcraft_text::embed::Embedding;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -77,13 +78,17 @@ pub struct Section {
|
|||
pub rows: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// What a font's `fsType` lets a copy of it do.
|
||||
/// What a font's `fsType` lets a copy of it do, read as exports read it ([`Embedding`]): the
|
||||
/// most permissive usage bit wins, and bitmap-only fonts may not be embedded.
|
||||
pub fn embedding_label(fs_type: u16) -> &'static str {
|
||||
match fs_type & 0x000f {
|
||||
0x0002 => "embedding not allowed",
|
||||
0x0004 => "embedding for preview and print",
|
||||
0x0008 => "embedding for editing",
|
||||
_ => "embedding allowed",
|
||||
if Embedding::from_fs_type(fs_type) == Embedding::Forbidden {
|
||||
"embedding not allowed"
|
||||
} else if fs_type & 0x0008 != 0 {
|
||||
"embedding for editing"
|
||||
} else if fs_type & 0x0004 != 0 {
|
||||
"embedding for preview and print"
|
||||
} else {
|
||||
"embedding allowed"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,13 +141,19 @@ fn sections(d: &Document, info: &Value, fonts: &BTreeSet<(String, String)>, imag
|
|||
fonts
|
||||
.iter()
|
||||
.map(|(family, style)| {
|
||||
let detail = match db.face(family, style) {
|
||||
Some(f) if f.family.eq_ignore_ascii_case(family) => {
|
||||
// Found by any of its names; a style the family lacks shows in another.
|
||||
let detail = match db.resolve(family, style) {
|
||||
Some((f, m)) if m != vectorcraft_text::FontMatch::Missing => {
|
||||
let file =
|
||||
f.path().and_then(|p| p.file_name()).map_or_else(|| "built in".into(), |n| n.to_string_lossy().into_owned());
|
||||
format!("{file}; {}", embedding_label(f.fs_type()))
|
||||
let found = format!("{file}; {}", embedding_label(f.fs_type()));
|
||||
if m == vectorcraft_text::FontMatch::Style {
|
||||
format!("substituted: shown in {} {} ({found})", f.family, f.style)
|
||||
} else {
|
||||
found
|
||||
}
|
||||
}
|
||||
Some(f) => format!("missing: shown in {} {}", f.family, f.style),
|
||||
Some((f, _)) => format!("missing: shown in {} {}", f.family, f.style),
|
||||
None => "missing".into(),
|
||||
};
|
||||
(format!("{family} {style}"), detail)
|
||||
|
|
@ -396,6 +407,12 @@ mod tests {
|
|||
assert_eq!(embedding_label(0x0002), "embedding not allowed");
|
||||
assert_eq!(embedding_label(0x0004 | 0x0100), "embedding for preview and print");
|
||||
assert_eq!(embedding_label(0x0008), "embedding for editing");
|
||||
// As exports read it: bitmap embedding only, the restricted bit with reserved bit 0, the
|
||||
// most permissive of several usage bits.
|
||||
assert_eq!(embedding_label(0x0200), "embedding not allowed");
|
||||
assert_eq!(embedding_label(0x0003), "embedding not allowed");
|
||||
assert_eq!(embedding_label(0x0006), "embedding for preview and print");
|
||||
assert_eq!(embedding_label(0x000c), "embedding for editing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -109,8 +109,15 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
lines.push("FONTS".to_string());
|
||||
for (family, style) in super::fonts::used_fonts(&st.doc) {
|
||||
let font = format!("{family} {style}");
|
||||
let reason = match db.face(&family, &style) {
|
||||
Some(f) if f.family.eq_ignore_ascii_case(&family) && f.embeddable() => {
|
||||
// Found by any of its names, as the canvas draws it.
|
||||
let resolved = db.resolve(&family, &style).filter(|(_, m)| *m != vectorcraft_text::FontMatch::Missing);
|
||||
// A style the family lacks is shown in its closest style, whose file is copied.
|
||||
let shown = match &resolved {
|
||||
Some((f, vectorcraft_text::FontMatch::Style)) => format!(" (shown in {} {})", f.family, f.style),
|
||||
_ => String::new(),
|
||||
};
|
||||
let reason = match resolved {
|
||||
Some((f, _)) if f.embeddable() => {
|
||||
let data = f.file_data();
|
||||
let file = f.path().and_then(Path::file_name).map_or_else(
|
||||
|| format!("{}-{}.{}", f.family, f.style, font_ext(data)).replace(|c: char| !(c.is_alphanumeric() || "-_.".contains(c)), ""),
|
||||
|
|
@ -121,13 +128,13 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
entries.push((format!("Fonts/{file}"), data.to_vec()));
|
||||
fonts += 1;
|
||||
}
|
||||
lines.push(format!("{font} → Fonts/{file}"));
|
||||
lines.push(format!("{font}{shown} → Fonts/{file}"));
|
||||
continue;
|
||||
}
|
||||
Some(f) if f.family.eq_ignore_ascii_case(&family) => "its licence doesn't allow embedding",
|
||||
_ => "not available on this computer",
|
||||
Some(_) => "its licence doesn't allow embedding",
|
||||
None => "not available on this computer",
|
||||
};
|
||||
lines.push(format!("{font}: {reason}, not copied"));
|
||||
lines.push(format!("{font}{shown}: {reason}, not copied"));
|
||||
skipped.push(json!({ "font": font, "reason": reason }));
|
||||
}
|
||||
lines.push(String::new());
|
||||
|
|
|
|||
|
|
@ -157,6 +157,36 @@ fn a_missing_link_is_reported_not_copied() {
|
|||
assert!(report.contains("blue.png: not found"), "{report}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_font_named_by_another_of_its_names_is_copied() {
|
||||
let dir = Folder::new("package-alias");
|
||||
let mut s = poster(&dir);
|
||||
// The bundled Semibold by its PostScript name, as an SVG or a PDF can name it.
|
||||
run(&mut s, "text.create", json!({"x": 10, "y": 80, "text": "Alias", "font": "SourceSans3-Semibold"}));
|
||||
let r = run(&mut s, "file.package", json!({}));
|
||||
assert_eq!((r["fonts"].as_u64(), r["skippedFonts"].clone()), (Some(2), json!([])), "{}", r["skippedFonts"]);
|
||||
let zip = unzip(&vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap());
|
||||
let report = String::from_utf8(zip["poster Folder/poster Report.txt"].clone()).unwrap();
|
||||
assert!(report.contains("SourceSans3-Semibold Regular → Fonts/"), "{report}");
|
||||
let details = report.lines().find(|l| l.starts_with("SourceSans3-Semibold Regular: ")).unwrap_or_default();
|
||||
assert!(details.contains("embedding allowed") && !details.contains("missing"), "{report}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_substituted_style_names_the_style_shown() {
|
||||
let dir = Folder::new("package-style");
|
||||
let mut s = poster(&dir);
|
||||
// No Source Sans 3 has a Black Wide, bundled or installed: its closest style stands in, and
|
||||
// that style's file is copied.
|
||||
run(&mut s, "text.create", json!({"x": 10, "y": 80, "text": "Heavy", "font": "Source Sans 3", "style": "Black Wide"}));
|
||||
let r = run(&mut s, "file.package", json!({}));
|
||||
assert_eq!(r["skippedFonts"], json!([]));
|
||||
let zip = unzip(&vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap());
|
||||
let report = String::from_utf8(zip["poster Folder/poster Report.txt"].clone()).unwrap();
|
||||
assert!(report.contains("Source Sans 3 Black Wide (shown in Source Sans 3 "), "{report}");
|
||||
assert!(report.contains("Source Sans 3 Black Wide: substituted: shown in Source Sans 3 "), "{report}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_document_info_report_has_every_category() {
|
||||
let dir = Folder::new("docinfo-report");
|
||||
|
|
|
|||
|
|
@ -270,9 +270,10 @@ impl FontFace {
|
|||
use skrifa::raw::TableProvider;
|
||||
self.skrifa().and_then(|f| f.os2().ok()).map_or(0, |t| t.fs_type())
|
||||
}
|
||||
/// May the font file be copied along with a document (its licence doesn't restrict embedding)?
|
||||
/// May the font file be copied along with a document? Its license allows embedding its
|
||||
/// outlines, as exports check it ([`Self::embedding`]).
|
||||
pub fn embeddable(&self) -> bool {
|
||||
self.fs_type() & 0x000f != 0x0002
|
||||
self.embedding() != crate::embed::Embedding::Forbidden
|
||||
}
|
||||
/// The face's index in its font file ([`Self::file_data`]; collections hold several).
|
||||
pub fn face_index(&self) -> u32 {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,42 @@ fn fs_type_flags_decide_the_embedding() {
|
|||
assert_eq!(Embedding::from_fs_type(0x0108), Embedding::Whole, "no subsetting");
|
||||
}
|
||||
|
||||
/// The bundled Source Sans 3 Regular renamed `family` (13 characters, as long as the original
|
||||
/// name) with OS/2 `fsType` set to `fs_type`.
|
||||
fn licensed_font(family: &str, fs_type: u16) -> Vec<u8> {
|
||||
let mut data = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf").to_vec();
|
||||
let table = |data: &[u8], tag: &[u8; 4]| {
|
||||
let f = skrifa::FontRef::new(data).unwrap();
|
||||
let r = f.table_directory.table_records().iter().find(|r| r.tag().to_be_bytes() == *tag).unwrap();
|
||||
(r.offset() as usize, r.length() as usize)
|
||||
};
|
||||
let (os2, _) = table(&data, b"OS/2");
|
||||
data[os2 + 8..os2 + 10].copy_from_slice(&fs_type.to_be_bytes());
|
||||
let (name, len) = table(&data, b"name");
|
||||
assert_eq!(family.len(), "Source Sans 3".len());
|
||||
let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::<Vec<u8>>();
|
||||
for (from, to) in [(utf16("Source Sans 3"), utf16(family)), (b"Source Sans 3".to_vec(), family.as_bytes().to_vec())] {
|
||||
let mut i = name;
|
||||
while let Some(at) = data[i..name + len].windows(from.len()).position(|w| w == from) {
|
||||
data[i + at..i + at + to.len()].copy_from_slice(&to);
|
||||
i += at + to.len();
|
||||
}
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
/// A font file goes with a document (File → Package) only when exports may embed it: not when
|
||||
/// its license is restricted or allows bitmaps only.
|
||||
#[test]
|
||||
fn fonts_exports_may_not_embed_are_not_embeddable() {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
for (family, fs_type, embeddable) in [("Bitmap Sans 3", 0x0200, false), ("Lowbit Sans 3", 0x0003, false), ("Wholed Sans 3", 0x0100, true)] {
|
||||
assert!(db.add_font(licensed_font(family, fs_type)) > 0, "{family}");
|
||||
let face = db.face(family, "Regular").unwrap();
|
||||
assert_eq!((face.family.as_str(), face.embeddable()), (family, embeddable), "fsType {fs_type:#06x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_subset_holds_only_the_glyphs_used_and_maps_their_characters() {
|
||||
let face = FontDb::global().face("Source Sans 3", "Regular").unwrap();
|
||||
|
|
|
|||
|
|
@ -114,7 +114,12 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
|||
app.ui.dialog = None;
|
||||
let n = r["files"].as_array().map_or(0, Vec::len);
|
||||
let missing = r["missingLinks"].as_array().map_or(0, Vec::len);
|
||||
let note = if missing > 0 { format!(" ({missing} linked file(s) not found)") } else { String::new() };
|
||||
// The fonts left out: their license doesn't allow it, or they aren't on this computer.
|
||||
let fonts = r["skippedFonts"].as_array().into_iter().flatten().filter_map(|f| f["font"].as_str()).collect::<Vec<_>>().join(", ");
|
||||
let mut note = if missing > 0 { format!(" ({missing} linked file(s) not found)") } else { String::new() };
|
||||
if !fonts.is_empty() {
|
||||
note.push_str(&format!(" (fonts not copied: {fonts})"));
|
||||
}
|
||||
if web {
|
||||
let (name, data) = (r["name"].as_str().unwrap_or("package.zip"), r["dataBase64"].as_str().unwrap_or_default());
|
||||
let bytes = vectorcraft_format::base64_decode(data).ok_or("the package came back damaged")?;
|
||||
|
|
@ -124,11 +129,14 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
|||
}
|
||||
let folder = r["folder"].as_str().unwrap_or_default().to_string();
|
||||
app.status(format!("Packaged {n} file(s) in {folder}{note}"));
|
||||
let shown_note = if missing > 0 {
|
||||
let mut shown_note = if missing > 0 {
|
||||
format!(" ({})", crate::i18n::fmt(tl!("{count} linked file(s) not found"), &[("count", &missing.to_string())]))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if !fonts.is_empty() {
|
||||
shown_note.push_str(&format!(" ({})", crate::i18n::fmt(tl!("fonts not copied: {fonts}"), &[("fonts", &fonts)])));
|
||||
}
|
||||
let detail = crate::i18n::fmt(
|
||||
tl!("{count} file(s) in {folder}{note}. Show the package folder?"),
|
||||
&[("count", &n.to_string()), ("folder", &folder), ("note", &shown_note)],
|
||||
|
|
|
|||
|
|
@ -106,6 +106,30 @@ fn the_web_downloads_a_zip() {
|
|||
assert!(got[0].1 > 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fonts_left_out_of_the_package_are_named() {
|
||||
let dir = Folder::new("fonts");
|
||||
let mut app = saved(&dir);
|
||||
app.run("text.create", json!({"x": 10, "y": 40, "text": "Kept"})).unwrap();
|
||||
app.run("text.create", json!({"x": 10, "y": 80, "text": "Gone", "font": "No Such Font Family"})).unwrap();
|
||||
app.run("ui.packageDialog", json!({})).unwrap();
|
||||
let out = dir.path("out");
|
||||
std::fs::create_dir_all(&out).unwrap();
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("folder".into(), json!(out));
|
||||
let r = confirm(&mut app).unwrap();
|
||||
assert_eq!(r["skippedFonts"], json!([{"font": "No Such Font Family Regular", "reason": "not available on this computer"}]));
|
||||
let gone = "fonts not copied: No Such Font Family Regular";
|
||||
assert!(app.ui.status.contains(gone), "{}", app.ui.status);
|
||||
let detail = app.ui.dialog.as_ref().unwrap().str("detail");
|
||||
assert!(detail.contains(gone) && !detail.contains("Source Sans 3"), "{detail}");
|
||||
// The web's download says so too.
|
||||
app.ui.dialog = None;
|
||||
app.services.download = Some(Box::new(|_: &str, _: &[u8]| {}));
|
||||
app.run("ui.packageDialog", json!({})).unwrap();
|
||||
confirm(&mut app).unwrap();
|
||||
assert!(app.ui.status.contains(gone), "{}", app.ui.status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_without_a_location_says_so() {
|
||||
let dir = Folder::new("empty");
|
||||
|
|
|
|||
|
|
@ -2990,6 +2990,7 @@
|
|||
Scrub Numeric Fields by Dragging Ajustar campos numéricos arrastrando
|
||||
{count} file(s) in {folder}{note}. Show the package folder? {count} archivo(s) en {folder}{note}. ¿Mostrar la carpeta del paquete?
|
||||
{count} linked file(s) not found No se encontraron {count} archivo(s) vinculado(s)
|
||||
fonts not copied: {fonts} fuentes no copiadas: {fonts}
|
||||
{count} linked image(s) changed since this document was saved. Show the new versions? {count} imagen(es) vinculada(s) cambiaron desde que se guardó este documento. ¿Mostrar las versiones nuevas?
|
||||
{count} more rules: Export writes every one. {count} reglas más: Exportar las escribe todas.
|
||||
{count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. Se omitieron {count} objetos sin nombre: ponles nombre o activa Objetos sin nombre en el menú del panel.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2987,6 +2987,7 @@
|
|||
Scrub Numeric Fields by Dragging Modifier les champs numériques en faisant glisser
|
||||
{count} file(s) in {folder}{note}. Show the package folder? {count} fichier(s) dans {folder}{note}. Afficher le dossier de l’assemblage ?
|
||||
{count} linked file(s) not found {count} fichier(s) lié(s) introuvable(s)
|
||||
fonts not copied: {fonts} polices non copiées : {fonts}
|
||||
{count} linked image(s) changed since this document was saved. Show the new versions? {count} image(s) liée(s) modifiée(s) depuis l’enregistrement de ce document. Afficher les nouvelles versions ?
|
||||
{count} more rules: Export writes every one. {count} règles de plus : Exporter les écrit toutes.
|
||||
{count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {count} objets sans nom omis : nommez-les ou activez Objets sans nom dans le menu du panneau.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2990,6 +2990,7 @@
|
|||
Scrub Numeric Fields by Dragging Regola i campi numerici trascinando
|
||||
{count} file(s) in {folder}{note}. Show the package folder? {count} file in {folder}{note}. Vuoi mostrare la cartella del pacchetto?
|
||||
{count} linked file(s) not found File collegati non trovati: {count}
|
||||
fonts not copied: {fonts} font non copiati: {fonts}
|
||||
{count} linked image(s) changed since this document was saved. Show the new versions? Immagini collegate modificate dopo il salvataggio di questo documento: {count}. Vuoi mostrare le nuove versioni?
|
||||
{count} more rules: Export writes every one. Altre {count} regole: Esporta le scrive tutte.
|
||||
{count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {count} oggetti senza nome esclusi: assegna loro un nome o attiva Oggetti senza nome nel menu del pannello.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2668,6 +2668,7 @@
|
|||
to →
|
||||
{count} file(s) in {folder}{note}. Show the package folder? {folder} に {count} 個のファイル{note}。パッケージフォルダーを表示しますか?
|
||||
{count} linked file(s) not found リンクファイルが {count} 個見つかりません
|
||||
fonts not copied: {fonts} コピーされなかったフォント:{fonts}
|
||||
{count} linked image(s) changed since this document was saved. Show the new versions? 保存後に {count} 個のリンク画像が変更されています。新しいバージョンを表示しますか?
|
||||
{count} more rules: Export writes every one. ほかに {count} 個のルールがあります。書き出しにはすべて含まれます。
|
||||
{count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. 名前のないオブジェクト {count} 個を除外しました。名前を付けるか、パネルメニューで「名前のないオブジェクト」をオンにしてください。
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2590,6 +2590,7 @@
|
|||
to para
|
||||
{count} file(s) in {folder}{note}. Show the package folder? {count} arquivo(s) em {folder}{note}. Mostrar a pasta do pacote?
|
||||
{count} linked file(s) not found {count} arquivo(s) vinculado(s) não encontrado(s)
|
||||
fonts not copied: {fonts} fontes não copiadas: {fonts}
|
||||
{count} linked image(s) changed since this document was saved. Show the new versions? {count} imagem(ns) vinculada(s) alterada(s) desde que este documento foi salvo. Mostrar as novas versões?
|
||||
{count} more rules: Export writes every one. {count} regras a mais: a Exportação escreve cada uma.
|
||||
{count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {count} objetos sem nome deixados de fora: nomeie-os, ou ative Objetos Sem Nome no menu do painel.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2986,6 +2986,7 @@
|
|||
Scrub Numeric Fields by Dragging Изменять числовые поля перетаскиванием
|
||||
{count} file(s) in {folder}{note}. Show the package folder? Файлов: {count} в {folder}{note}. Показать папку пакета?
|
||||
{count} linked file(s) not found Не найдено связанных файлов: {count}
|
||||
fonts not copied: {fonts} шрифты не скопированы: {fonts}
|
||||
{count} linked image(s) changed since this document was saved. Show the new versions? Связанных изображений ({count}) изменились с момента сохранения этого документа. Показать новые версии?
|
||||
{count} more rules: Export writes every one. Ещё правил: {count}: экспорт записывает все.
|
||||
{count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. Пропущено объектов без имени: {count}: назовите их или включите «Объекты без имени» в меню панели.
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2982,6 +2982,7 @@
|
|||
Scrub Numeric Fields by Dragging Змінювати числові поля перетягуванням
|
||||
{count} file(s) in {folder}{note}. Show the package folder? файлів: {count} {folder}{note}. Показати папку пакета?
|
||||
{count} linked file(s) not found не знайдено пов’язаних файлів: {count}
|
||||
fonts not copied: {fonts} шрифти не скопійовано: {fonts}
|
||||
{count} linked image(s) changed since this document was saved. Show the new versions? пов’язані зображення ({count}) змінилися з моменту збереження цього документа. Показати нові версії?
|
||||
{count} more rules: Export writes every one. ще правил: {count}: експорт записує все.
|
||||
{count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. пропущено об’єкти без імені: {count}: назвіть їх або увімкніть «Об’єкти без імені» у меню панелі.
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -2910,6 +2910,7 @@
|
|||
Scrub Numeric Fields by Dragging 拖动以调整数值字段
|
||||
{count} file(s) in {folder}{note}. Show the package folder? {folder} 中有 {count} 个文件{note}。要显示打包文件夹吗?
|
||||
{count} linked file(s) not found 找不到 {count} 个链接文件
|
||||
fonts not copied: {fonts} 未复制的字体:{fonts}
|
||||
{count} linked image(s) changed since this document was saved. Show the new versions? 自存储此文档后,有 {count} 个链接图像已更改。要显示新版本吗?
|
||||
{count} more rules: Export writes every one. 另有 {count} 条规则:导出时会全部写出。
|
||||
{count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. 已略过 {count} 个未命名对象:请为它们命名,或在面板菜单中开启“未命名对象”。
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -2998,6 +2998,7 @@
|
|||
Scrub Numeric Fields by Dragging 拖曳以調整數值欄位
|
||||
{count} file(s) in {folder}{note}. Show the package folder? {folder} 中有 {count} 個檔案{note}。要顯示封裝資料夾嗎?
|
||||
{count} linked file(s) not found 找不到 {count} 個連結檔案
|
||||
fonts not copied: {fonts} 未複製的字體:{fonts}
|
||||
{count} linked image(s) changed since this document was saved. Show the new versions? 自儲存此文件後,有 {count} 個連結影像已變更。要顯示新版本嗎?
|
||||
{count} more rules: Export writes every one. 另有 {count} 條規則:匯出時會全部寫出。
|
||||
{count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. 已略過 {count} 個未命名物件:請為它們命名,或在面板選單中開啟「未命名物件」。
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
Loading…
Reference in New Issue