M7: support shaped Thai text in Add Text and UI fallback fonts (#767)

* M7: support shaped Thai text in Add Text and UI fallback fonts

Share the bounded shaping and Type 3 writer with Thai, preserving vowel and tone clusters and original Unicode through save/reopen. Keep the Thai face optional and update UI notices and their translations.

Constraint: Thai font assets remain in the companion craft-fonts repository.

Confidence: high

Scope-risk: moderate

Co-authored-by: Codex <noreply@openai.com>

* M7: show Thai Add Text before and after the fix

Document the rejected input on the base commit and the saved Thai text on the fixed build using genuine UI captures.

Constraint: Synthetic content and licensed fonts only; system font fallback disabled.

Co-authored-by: Codex <noreply@openai.com>

* M7: demonstrate Thai stress cases in the comparison screenshot

Show nine lines of stacked marks, both Sara Am encodings, Thai digits and mixed Latin text. Verify all 198 Unicode code points survive saving and extraction with both PdfCraft and Poppler.

Co-authored-by: Codex <noreply@openai.com>

* CI and releases: craft-fonts pin to 3d9ab3a (adds Noto Sans Thai)

ci.yml, freebsd.yml and the craft-fonts-ref release.yml passes to craft-common-actions move
from 8dcdacd to storytold/craft-fonts 3d9ab3a, the revision with Noto Sans Thai (#18), so
Thai Add Text is built in and tested.

* ATTRIBUTION: drop the screenshot entry again (the image isn't committed)

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Tanapat Eiam-arj 2026-10-12 05:07:36 +07:00 committed by GitHub
parent b91d38c073
commit 8ee3673bd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 437 additions and 144 deletions

View File

@ -36,7 +36,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: storytold/craft-fonts
ref: 8dcdacd5153e64560d109541a47d806f26f048c0
ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
path: craft-fonts
- name: Install Linux system dependencies (egui/winit/rfd)

View File

@ -26,4 +26,4 @@ jobs:
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
uses: storytold/craft-common-actions/.github/workflows/freebsd.yml@main
with:
craft-fonts-ref: 8dcdacd5153e64560d109541a47d806f26f048c0
craft-fonts-ref: 3d9ab3a132ed0eff457e424226a435a901da30d3

View File

@ -24,7 +24,7 @@ jobs:
with:
app-name: pdfcraft
app-title: PdfCraft
craft-fonts-ref: 8dcdacd5153e64560d109541a47d806f26f048c0
craft-fonts-ref: 3d9ab3a132ed0eff457e424226a435a901da30d3
version: ${{ inputs.version }}
secrets:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}

View File

@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "bf6e968d781894c6641df2b52224a2b273b33ab4110b05d5b92ea67d80753d19"
sha256 = "7ed6a7c482dab19fe1b54a7704902610e79e9bc60f19115ce8af7b1ebca504ff"
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "eebd627422ee48d52e35655c41e8a587e4d86c38fcb56d5465d8446d5971dabf"
sha256 = "e5141febe07d89c4c4c7b2d2f87abf71ad1ab8dc4a7880d03805db6f0022118b"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "2f11d8535473dfdefc0aea011d3079eaada0286a58982a7084e20bda8477de54"
sha256 = "6dff4fded1106659f7c0b1c7e2c671c02e59313ab58875fabf591a9356098767"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "7d3f5d26df0596f96f5ff0e7cccabbdf1f0c2278062cfa0a1046b364b04c7ba7"
sha256 = "1978cfd55b08fe9905e00ddd16a10d3d0f5371143e2f00a58a77affcc1d874c3"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "d1a7742f43f01253d40f339c525ad0e2c15c18be1a60d5cc4c8a7bebbebcf3de"
sha256 = "928435778bd8c703083087d401851ab01dc038db3ff568e572b6cf646f805b03"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "819ab9c8426bf3d1fc2c7021f4b5a19d41f8b8cc65301da625ecb12a2297fc8c"
sha256 = "4f95f2dc524177a5a6261f2f6df6def2cd061596bddac20b264cf6051978b3e3"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "258518681eb774c7b90335f1d2089bf1bb37831084cf8121f3bbea75931e3f6b"
sha256 = "7c0df34f12030b1dc266413839dbb507bc890cc8847f6b2eac324ed54ff3fc19"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "404766adf3b492801346390914884c1b75d97e2b4e9e3dcd9239fc8062e7332c"
sha256 = "77f4ec8eb9cba7e2f2aad0883a91a62ca871140135c586f32c725f66774a0632"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "656940b456a6c2401f8bd3107a71cedcae5e52cd446e86e806285b6322d8ed8e"
sha256 = "4e4d4e4e2f9d4d4be91ed2767a07640b626d83733093d12bff99d0a3986aa7d7"
[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "b5ad0282d7a1ddb4e818c7f8a76c2182bd3c9343b8a555b0dae7b78b831b594a"
sha256 = "3d1af7e703706bf4c814062871b47308b70f358356c3b17782dde3eaaa66abb7"
[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "71dfed7bedeff0755c7377b7244ccfe57d9958cc803202c52b04da58bce9b1dd"
sha256 = "d03048fe760c7daf08c0d8ed8470633c0eaca8f68c41d1c8ce889813c7f6fab1"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "5d0bfe97ef2ef2d1577c538793fc3267483f5c9fe9de4df97924d04cf18f7292"
sha256 = "69e07b82ae409de7544f538fedc8e56cdb9f4092be7f45cc7366bddbc03ce93f"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "b69b54cbeee8c9a876db5ed7d49e056e794742bd0a001a2c6383d464e24c31e4"
sha256 = "0a71d58a88ea532358d3b133eba3aae217ad4bcbb763b6235b137615ce226d2b"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "db5ba8dc27fb272e2a76e017c5bb38bad200bb7299b9cce40d94d837704507fb"
sha256 = "1e5172befbe995c800cd69e9d6abc02c92544b62d8e6dac0f4a0c26cd0b1efbb"
[[asset]]
path = "crates/ui-egui/src/i18n/kk.tsv"
@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "3c94bc6bb9950d8172c3f43c32d09496a382eea32717fcee3855d338881f35fa"
sha256 = "dd76d58abda6153e5c0b9fcc761d652cf2ed96522fab18c9ce87e9cec594043a"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "10ff47a04951e99f902efeb9832d12b78f5f632d54503843283adfd2ad9e8186"
sha256 = "6c1ca7f6d53454c69b3e362a2a09c5c28d04b615c53936c639e3b43c880552d3"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "983c0cc2efa98d2d3f0449f45c7f7ebdd39c93578b77a07631ee5613b0989a98"
sha256 = "9151c1049289e318409965c5ad061448db9f35a7e8f14a754ba83616e3d87930"
[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "439ad5e6ae9c3e2484b1a5f9f11868ff33ddb150308d32d8c32691f2817c15a6"
sha256 = "096be489498c16dbf85fcd6d2d0d96590736a8b0bbe3b3a5b557e2627c89f9c1"
[[asset]]
path = "crates/ui-egui/src/i18n/tr.tsv"
@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "b4e90e5929a95a8c85cf88099003cb772e45591759e192f89026cd883a8149c7"
sha256 = "e72215bc4bf21f598bf82a6ac55551bdef47d62cceedea239d89c0febd9cb1b7"
[[asset]]
path = "assets/icons/accessibility.svg"
@ -3339,4 +3339,4 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "be33bd6c0f2952eafb04d22cbc71f4e43594bceecc0acaca9cfd2b602100c29c"
sha256 = "7cc8bf51e042d07a397dbebf0747b496879e721a6644f1af197cfb3cbcbe7904"

1
Cargo.lock generated
View File

@ -3611,6 +3611,7 @@ dependencies = [
"image",
"pdfcraft-cos",
"pdfcraft-engine",
"pdfcraft-fonts",
"pdfcraft-model",
"pdfcraft-organize",
"pdfcraft-platform",

7
NOTICE
View File

@ -20,12 +20,15 @@ list (author, source, license) is in ATTRIBUTION.md.
- Inter typeface (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt.
- JetBrains Mono typeface (assets/fonts/JetBrainsMono-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-JetBrainsMono.txt.
- Dancing Script typeface (assets/fonts/DancingScript.ttf), Copyright 2016 The Dancing Script Project Authors — SIL Open Font License 1.1 (Reserved Font Name "Dancing Script"). See assets/fonts/OFL-DancingScript.txt.
- Japanese fonts from craft-fonts (https://github.com/storytold/craft-fonts), an optional build input
- Fonts from craft-fonts (https://github.com/storytold/craft-fonts), an optional build input
(CRAFT_FONTS_DIR) that is not part of this repository. Builds made with it, which includes every
official release, embed the fonts listed in
https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (BIZ UDPGothic, Shippori Mincho,
BIZ UDMincho; the web build only BIZ UDPGothic Regular) under the SIL Open Font License 1.1. Those
packages carry each font's licence as OFL-<family>.txt.
packages carry each font's licence as OFL-<family>.txt. Builds made with the craft-fonts revision
pinned in .github/workflows/ also include Noto Sans Thai (in the web build too), Copyright 2022
The Noto Project Authors (https://github.com/notofonts/thai), under SIL OFL 1.1; its licence is
fonts/noto-sans-thai/OFL.txt in the craft-fonts build input.
- hayro (rendering, via crates.io) — Apache-2.0 OR MIT. Its `embed-fonts` feature bundles the Foxit
standard-14 Type 1 fonts under BSD-3-Clause ("Copyright 2014 PDFium Authors"), and hayro-cmap bundles
Adobe CMap resources under BSD-3-Clause.

View File

@ -139,6 +139,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
Newest first: the date, what moved, and the new overall estimate.
- **2026-10-10 (community, M7, Thai Add Text):** Optional craft-fonts Thai face (Noto Sans Thai) for the UI and shaped Add Text through the engine, CLI and MCP. Stacked marks and Sara Am are kept through Type 3 clusters with ToUnicode; regular style, and wrapping at cluster boundaries (no dictionary word segmentation). The craft-fonts pin for CI and releases moves to the revision with the Thai face. Forms and Fill & Sign are outside this change. Estimates unchanged.
- **2026-10-11 (#995, #1077, permissions password after opening):** A PDF with only a permissions password opens without asking, restricted, and nothing let the user enter that password afterwards: the notice and the hint pointed at reopening with it. Now the security notice and Properties ▸ Security offer Unlock…, and Change settings / Remove security on a restricted document ask for the permissions password first and then carry on (as Acrobat does). The open password is refused there. `Session::unlock` upgrades the open document in place (owner and user passwords unlock the same file key, so nothing is re-read), unlocks the undo and redo states and reopens saves as owner; it is not an edit and doesn't mark the document unsaved. New command `protect.unlock`; the new strings are in all 15 full catalogs. Tests cover crypt, cos, engine (undo, redo, save, separate open password) and the UI. Still missing: an automation tool for it. Estimates unchanged.
- **2026-10-11 (M14, #307 Plan 3):** Engine open and full refresh use one page/content traversal to populate marks and added content. The independent public scans remain available; the combined scanner borrows direct objects and holds referenced objects through `Arc<Object>` instead of cloning scan-only `/Contents` arrays or `/PCAdded` dictionaries. Differential tests cover an indirect `/PCAdded` dictionary as well as ordering, malformed/direct/indirect/repeated entries and `/UserUnit`. Exact `bbbe600`→final release CLI measurements used 10 alternating pairs: wall-time medians were 183.645→174.440 ms on 10,000 marker-free pages (10/10 wins), 33.587→32.752 ms on 1,500 mixed pages (8/10), and 24.331→20.133 ms on the 100,000-entry array stress fixture (10/10). The stress fixture's maximum-RSS median fell 28,655,616→19,038,208 B; peak-footprint medians were 14,747,136→14,673,408 B. From P0, the follow-up stress run was 20.886→20.201 ms (10/10), with RSS 23,953,408→19,087,360 B and peak footprint 14,706,144→14,697,952 B. `doc_open` stdout hashes matched every pair. The ancillary `organize` binding preserves the prior `prune_legacy` update condition. Estimates unchanged.
- **2026-10-11 (M4, Organize overview):** Opening Organize pages from All tools, Home, the tool palette or `ui.set tool=organize` opens the thumbnail grid directly. Checkboxes toggle individual pages without modifier keys; page controls rotate either way or delete one page, while the toolbar acts on the selection. Portrait and landscape previews retain their aspect ratio. UI and control regressions cover entry, selection, single-page versus batch actions, undo, permissions and retaining the last page. Milestone and effort estimates unchanged.

View File

@ -24,6 +24,7 @@ default = ["mcp"]
mcp = ["dep:base64"]
[dev-dependencies]
pdfcraft-fonts = { workspace = true }
pdfcraft-cos = { workspace = true }
pdfcraft-model = { workspace = true }
pdfcraft-xfa = { workspace = true }

View File

@ -4107,6 +4107,30 @@ fn command_batch_refuses_more_than_a_thousand_steps() {
assert!(err.to_string().contains("at most 1000 steps"), "{err}");
}
#[test]
fn thai_text_through_tools_saves_reopens_extracts_and_renders() {
let dir = workdir("thai-content");
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({"path": "a.pdf"}))["doc"].as_u64().unwrap();
let text = "สวัสดี น้ำ ผู้ใช้ ปู่ ๑๒๓ Hello 123";
let args = json!({"doc": doc, "page": 1, "text": text, "at": [10, 20], "size": 9});
if pdfcraft_fonts::document_thai_font().is_none() {
assert!(a.call("page_add_text", &args).unwrap_err().to_string().contains("Thai"));
assert_eq!(ok(&mut a, "content_list", json!({"doc": doc}))["count"], 0);
return;
}
ok(&mut a, "page_add_text", args);
assert!(page_text(&mut a, doc)[0].contains(text), "{:?}", page_text(&mut a, doc));
ok(&mut a, "doc_save", json!({"doc": doc, "path": "thai.pdf"}));
let reopened = ok(&mut a, "doc_open", json!({"path": "thai.pdf"}))["doc"].as_u64().unwrap();
assert_eq!(ok(&mut a, "content_list", json!({"doc": reopened}))["items"][0]["text"], text);
assert!(page_text(&mut a, reopened)[0].contains(text));
let rendered = a.call("page_render", &json!({"doc": reopened, "page": 1, "dpi": 144})).unwrap();
assert!(rendered.iter().any(|c| matches!(c, Content::Png { data, width, height } if !data.is_empty() && *width > 0 && *height > 0)));
ok(&mut a, "content_update", json!({"doc": reopened, "page": 1, "index": 1, "text": "แก้ไข น้ำ"}));
assert!(page_text(&mut a, reopened)[0].contains("แก้ไข น้ำ"));
}
#[test]
fn mixed_objects_move_through_tools_with_generation_and_one_undo() {
let dir = workdir("mixed-object-move");

View File

@ -11,7 +11,9 @@
use std::collections::{HashMap, HashSet};
use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString, Stream};
use pdfcraft_fonts::{GlyphError, GlyphOutline, ShapedCluster, arabic_glyph, helvetica_width, literal, shape_arabic, win_ansi};
use pdfcraft_fonts::{
GlyphError, GlyphOutline, ShapedCluster, ShapedScript, helvetica_width, literal, shape_arabic, shape_thai, shaped_glyph, win_ansi,
};
use unicode_bidi::{Level, ParagraphBidiInfo};
use crate::{EditError, MarkKind, check, contents, n, page_list, place_tagged};
@ -225,17 +227,26 @@ fn wrapped(t: &AddedText) -> Vec<(String, bool)> {
let mut out = Vec::new();
for para in t.text.split('\n') {
let arabic = para.chars().any(is_arabic);
let thai = para.chars().any(is_thai);
if thai
&& !arabic
&& let Ok(lines) = thai_lines(t, para, width)
{
out.extend(lines.into_iter().map(|line| (line, false)));
continue;
}
let rtl = arabic && unicode_bidi::get_base_direction(para) == unicode_bidi::Direction::Rtl;
// Arabic words are shaped one by one (joining stops at spaces), so a line's width is the
// sum of its words' and shaping stays linear in the paragraph's length. The space is the
// paragraph's; a space between Latin words is drawn in the item's font, a few hundredths
// of an em apart.
let measure = |s: &str| if arabic { arabic_width(t, s, rtl) } else { t.family.width(s, t.size, t.bold) };
let measure = |s: &str| if arabic || thai { shaped_width(t, s, rtl) } else { t.family.width(s, t.size, t.bold) };
let space = measure(" ");
let (mut line, mut line_w) = (String::new(), 0.0);
for word in para.split(' ') {
let candidate = if line.is_empty() { word.to_string() } else { format!("{line} {word}") };
let candidate_w = if arabic { if line.is_empty() { measure(word) } else { line_w + space + measure(word) } } else { measure(&candidate) };
let candidate_w =
if arabic || thai { if line.is_empty() { measure(word) } else { line_w + space + measure(word) } } else { measure(&candidate) };
if !line.is_empty() && candidate_w > width {
out.push((std::mem::take(&mut line), rtl));
line = word.to_string();
@ -250,6 +261,39 @@ fn wrapped(t: &AddedText) -> Vec<(String, bool)> {
out
}
/// Thai wrapping at shaped cluster boundaries never detaches a tone/vowel mark from its base.
/// This is glyph-safe wrapping, not dictionary-based Thai word segmentation.
fn thai_lines(t: &AddedText, para: &str, width: f64) -> Result<Vec<String>, EditError> {
let (pieces, _) = shaped_layout(t, para, false)?;
let mut atoms = Vec::new();
for piece in pieces {
match piece {
Piece::Shaped(clusters) => atoms.extend(clusters.into_iter().map(|c| (c.text, c.advance * t.size))),
Piece::Latin(s) => atoms.extend(s.chars().map(|c| {
let text = c.to_string();
let w = t.family.width(&text, t.size, t.bold);
(text, w)
})),
}
}
let mut lines = Vec::new();
let (mut line, mut used) = (String::new(), 0.0);
for (text, advance) in atoms {
if !line.is_empty() && used + advance > width {
lines.push(std::mem::take(&mut line));
used = 0.0;
}
line.push_str(&text);
used += advance;
}
lines.push(line);
Ok(lines)
}
fn is_thai(c: char) -> bool {
matches!(u32::from(c), 0x0E00..=0x0E7F)
}
/// Arabic letters, marks, digits and punctuation: an item with any of them is shaped with the
/// craft-fonts Arabic face.
fn is_arabic(c: char) -> bool {
@ -260,6 +304,13 @@ fn is_arabic(c: char) -> bool {
/// with the craft-fonts Arabic face (#125), or Arabic that the face can't draw (or that has no
/// face to draw it). Page-content tools refuse such text; the Add-text editor keeps it open.
pub fn first_undrawable(t: &AddedText) -> Option<char> {
if t.text.chars().any(is_thai) {
if let Some(c) = t.text.chars().find(|c| is_thai(*c) && !pdfcraft_fonts::thai_has(*c)) {
return Some(c);
}
let rest = AddedText { text: t.text.chars().filter(|c| !is_thai(*c)).collect(), ..t.clone() };
return first_undrawable(&rest);
}
// Arabic that nothing can draw: there's no face, or the face lacks it.
if let Some(c) = t.text.chars().find(|c| shaped_arabic(*c) && !pdfcraft_fonts::arabic_has(*c)) {
return Some(c);
@ -269,12 +320,12 @@ pub fn first_undrawable(t: &AddedText) -> Option<char> {
// that is also how `draw` redraws an existing item, so it's checked that way.
return t.text.split('\n').find_map(pdfcraft_fonts::first_non_win_ansi);
}
// What `arabic_layout` leaves to the standard font, exactly as `draw_arabic` splits it.
// What `shaped_layout` leaves to the standard font, exactly as `draw_shaped` splits it.
wrapped(t).into_iter().find_map(|(line, rtl)| {
let (pieces, _) = arabic_layout(t, &line, rtl).ok()?;
let (pieces, _) = shaped_layout(t, &line, rtl).ok()?;
pieces.iter().find_map(|p| match p {
Piece::Latin(s) => pdfcraft_fonts::first_non_win_ansi(s),
Piece::Arabic(_) => None,
Piece::Shaped(_) => None,
})
})
}
@ -305,16 +356,16 @@ fn mirrored(c: char) -> char {
}
}
/// A piece of a line with Arabic text, in drawing order.
/// A piece of a line with shaped text, in drawing order.
enum Piece {
/// Shown in the item's own standard font.
Latin(String),
Arabic(Vec<ShapedCluster>),
Shaped(Vec<ShapedCluster>),
}
/// A line of an Arabic item in drawing order (the Unicode bidirectional algorithm, UAX #9), and
/// A line of a shaped-text item in drawing order (the Unicode bidirectional algorithm, UAX #9), and
/// its width at the item's size. `rtl` is its paragraph's direction.
fn arabic_layout(t: &AddedText, line: &str, rtl: bool) -> Result<(Vec<Piece>, f64), EditError> {
fn shaped_layout(t: &AddedText, line: &str, rtl: bool) -> Result<(Vec<Piece>, f64), EditError> {
// unicode-bidi indexes the first level of a line, so an empty one would panic.
if line.is_empty() {
return Ok((Vec::new(), 0.0));
@ -329,20 +380,30 @@ fn arabic_layout(t: &AddedText, line: &str, rtl: bool) -> Result<(Vec<Piece>, f6
let Some(text) = line.get(run) else { continue };
// Arabic letters take the Arabic face; so do spaces and punctuation between them, which the
// shaper then mirrors. Latin text (and what the face lacks) keeps the item's font.
let mut segments: Vec<(bool, String)> = Vec::new();
let mut segments: Vec<(Option<ShapedScript>, String)> = Vec::new();
for c in text.chars().filter(|c| !is_bidi_control(*c)) {
let arabic = is_arabic(c) || (run_rtl && !c.is_alphanumeric() && pdfcraft_fonts::arabic_has(c));
let script = if is_thai(c) {
Some(ShapedScript::Thai)
} else if is_arabic(c) || (run_rtl && !c.is_alphanumeric() && pdfcraft_fonts::arabic_has(c)) {
Some(ShapedScript::Arabic)
} else {
None
};
match segments.last_mut() {
Some((kind, s)) if *kind == arabic => s.push(c),
_ => segments.push((arabic, c.to_string())),
Some((kind, s)) if *kind == script => s.push(c),
_ => segments.push((script, c.to_string())),
}
}
if run_rtl {
segments.reverse();
}
for (arabic, s) in segments {
pieces.push(if arabic {
Piece::Arabic(shape_arabic(&s, run_rtl).map_err(|e| arabic_error(e, &s))?)
for (script, s) in segments {
pieces.push(if let Some(script) = script {
let shaped = match script {
ShapedScript::Arabic => shape_arabic(&s, run_rtl).map_err(|e| arabic_error(e, &s))?,
ShapedScript::Thai => shape_thai(&s).map_err(|e| thai_error(e, &s))?,
};
Piece::Shaped(shaped)
} else if run_rtl {
Piece::Latin(s.chars().rev().map(mirrored).collect())
} else {
@ -354,16 +415,16 @@ fn arabic_layout(t: &AddedText, line: &str, rtl: bool) -> Result<(Vec<Piece>, f6
.iter()
.map(|p| match p {
Piece::Latin(s) => t.family.width(s, t.size, t.bold),
Piece::Arabic(clusters) => clusters.iter().map(|c| c.advance).sum::<f64>() * t.size,
Piece::Shaped(clusters) => clusters.iter().map(|c| c.advance).sum::<f64>() * t.size,
})
.sum();
Ok((pieces, width))
}
/// The width of `s` in an Arabic item; the item's font's estimate if it can't be shaped (drawing
/// The width of `s` in a shaped-text item; the item's font's estimate if it can't be shaped (drawing
/// then reports why).
fn arabic_width(t: &AddedText, s: &str, rtl: bool) -> f64 {
arabic_layout(t, s, rtl).map(|(_, w)| w).unwrap_or_else(|_| t.family.width(s, t.size, t.bold))
fn shaped_width(t: &AddedText, s: &str, rtl: bool) -> f64 {
shaped_layout(t, s, rtl).map(|(_, w)| w).unwrap_or_else(|_| t.family.width(s, t.size, t.bold))
}
fn arabic_error(e: GlyphError, text: &str) -> EditError {
@ -376,6 +437,16 @@ fn arabic_error(e: GlyphError, text: &str) -> EditError {
})
}
fn thai_error(e: GlyphError, text: &str) -> EditError {
EditError::Invalid(match e {
GlyphError::NoFont => {
"Thai text needs a Thai font in this build; rebuild with CRAFT_FONTS_DIR pointing to craft-fonts with a Thai face".into()
}
GlyphError::Missing => format!("the Thai font can't show \"{text}\""),
GlyphError::TooComplex => "the Thai glyphs are too complex; use a shorter text box".into(),
})
}
/// The box a text item occupies (height from its lines).
pub fn text_rect(t: &AddedText) -> [f64; 4] {
let r = norm(t.rect);
@ -391,31 +462,31 @@ fn font_name(base: &str) -> String {
/// per 240 distinct glyphs it needs.
const GLYPHS_PER_FONT: usize = 240;
/// A line of an Arabic item after layout: its text, whether its paragraph runs right to left, its
/// pieces in drawing order and its width at the item's size.
/// A line of a shaped-text item after layout: its text, whether its paragraph runs right to left,
/// its pieces in drawing order and its width at the item's size.
type LaidLine = (String, bool, Vec<Piece>, f64);
/// Draw a text item that has Arabic in it. Each line is laid out in display order; Arabic glyphs
/// Draw a text item that has Arabic or Thai in it. Each line is laid out in display order; Arabic glyphs
/// come from Type 3 fonts drawn from the craft-fonts face's outlines (no font file is embedded),
/// with a ToUnicode map for copy and search. Other text uses the item's standard font.
fn draw_arabic(doc: &mut Document, t: &AddedText, taken: &Dict, fonts: &mut Dict, out: &mut Vec<u8>) -> Result<(), EditError> {
fn draw_shaped(doc: &mut Document, t: &AddedText, taken: &Dict, fonts: &mut Dict, out: &mut Vec<u8>) -> Result<(), EditError> {
let laid = wrapped(t)
.into_iter()
.map(|(line, rtl)| arabic_layout(t, &line, rtl).map(|(pieces, w)| (line, rtl, pieces, w)))
.map(|(line, rtl)| shaped_layout(t, &line, rtl).map(|(pieces, w)| (line, rtl, pieces, w)))
.collect::<Result<Vec<_>, _>>()?;
draw_laid_arabic(doc, t, &laid, taken, fonts, out, arabic_glyph)
draw_laid_shaped(doc, t, &laid, taken, fonts, out, shaped_glyph)
}
/// [`draw_arabic`] for lines that are already laid out. `outline` is the source of glyph outlines
/// (the craft-fonts face, or a stand-in in tests).
fn draw_laid_arabic(
/// [`draw_shaped`] for lines that are already laid out. `outline` is the source of glyph outlines
/// for a script's face (the craft-fonts faces, or a stand-in in tests).
fn draw_laid_shaped(
doc: &mut Document,
t: &AddedText,
laid: &[LaidLine],
taken: &Dict,
fonts: &mut Dict,
out: &mut Vec<u8>,
outline: impl Fn(u32) -> Result<GlyphOutline, GlyphError>,
outline: impl Fn(ShapedScript, u32) -> Result<GlyphOutline, GlyphError>,
) -> Result<(), EditError> {
let r = text_rect(t);
let latin = font_name(t.family.base_font(t.bold, t.italic));
@ -424,7 +495,7 @@ fn draw_laid_arabic(
let mut keys: Vec<&ShapedCluster> = Vec::new();
let mut index: HashMap<ClusterKey, usize> = HashMap::new();
for c in laid.iter().flat_map(|(_, _, pieces, _)| pieces).flat_map(|p| match p {
Piece::Arabic(clusters) => clusters.as_slice(),
Piece::Shaped(clusters) => clusters.as_slice(),
Piece::Latin(_) => &[],
}) {
index.entry(cluster_key(c)).or_insert_with(|| {
@ -432,12 +503,16 @@ fn draw_laid_arabic(
keys.len() - 1
});
}
// Every outline is read before anything is written, so a failure leaves the document as it was.
let mut outlines = HashMap::new();
for c in &keys {
for (id, _) in &c.glyphs {
if !outlines.contains_key(id) {
outlines.insert(*id, outline(*id).map_err(|e| arabic_error(e, &c.text))?);
if let std::collections::hash_map::Entry::Vacant(entry) = outlines.entry((c.script, *id)) {
entry.insert(outline(c.script, *id).map_err(|e| match c.script {
ShapedScript::Arabic => arabic_error(e, &c.text),
ShapedScript::Thai => thai_error(e, &c.text),
})?);
}
}
}
@ -445,7 +520,7 @@ fn draw_laid_arabic(
let mut taken_names: HashSet<Vec<u8>> = taken.iter().chain(fonts.iter()).map(|(name, _)| name.clone()).collect();
let mut names = Vec::new();
for chunk in keys.chunks(GLYPHS_PER_FONT) {
let font = type3_arabic(doc, chunk, &outlines);
let font = type3_shaped(doc, chunk, &outlines);
// A name no font on the page has, so items never replace each other's fonts (object
// numbers alone don't do: a full save renumbers objects but keeps resource names).
let mut name = format!("PCAr{}", font.num);
@ -483,7 +558,7 @@ fn draw_laid_arabic(
current = Some(latin.as_str());
pen += t.family.width(s, t.size, t.bold) + tw * s.matches(' ').count() as f64;
}
Piece::Arabic(clusters) => {
Piece::Shaped(clusters) => {
for c in clusters {
let Some(&k) = index.get(&cluster_key(c)) else { continue };
let Some(name) = names.get(k / GLYPHS_PER_FONT) else { continue };
@ -508,14 +583,14 @@ fn draw_laid_arabic(
/// What makes two clusters the same Type 3 glyph: text, glyphs, offsets and advance (bit for bit;
/// equal clusters come from the same font units).
type ClusterKey<'a> = (&'a str, Vec<(u32, u64, u64)>, u64);
type ClusterKey<'a> = (ShapedScript, &'a str, Vec<(u32, u64, u64)>, u64);
fn cluster_key(c: &ShapedCluster) -> ClusterKey<'_> {
(c.text.as_str(), c.glyphs.iter().map(|(id, [x, y])| (*id, x.to_bits(), y.to_bits())).collect(), c.advance.to_bits())
(c.script, c.text.as_str(), c.glyphs.iter().map(|(id, [x, y])| (*id, x.to_bits(), y.to_bits())).collect(), c.advance.to_bits())
}
/// A Type 3 font with `clusters` as codes 1…, each drawn from the Arabic face's glyph `outlines`.
fn type3_arabic(doc: &mut Document, clusters: &[&ShapedCluster], outlines: &HashMap<u32, GlyphOutline>) -> ObjRef {
/// A Type 3 font with `clusters` as codes 1…, each drawn from its script face's glyph `outlines`.
fn type3_shaped(doc: &mut Document, clusters: &[&ShapedCluster], outlines: &HashMap<(ShapedScript, u32), GlyphOutline>) -> ObjRef {
let mut charprocs = Dict::new();
let mut differences = vec![Object::Int(1)];
let mut widths = Vec::with_capacity(clusters.len());
@ -527,7 +602,7 @@ fn type3_arabic(doc: &mut Document, clusters: &[&ShapedCluster], outlines: &Hash
let mut paths = Vec::new();
let mut ink: Option<[f64; 4]> = None;
for (id, [dx, dy]) in &c.glyphs {
let Some(g) = outlines.get(id) else { continue };
let Some(g) = outlines.get(&(c.script, *id)) else { continue };
for contour in &g.contours {
for (j, p) in contour.iter().enumerate() {
paths.extend(format!("{} {} {}\n", n((p[0] + dx) * 1000.0), n((p[1] + dy) * 1000.0), if j == 0 { "m" } else { "l" }).bytes());
@ -564,7 +639,18 @@ fn type3_arabic(doc: &mut Document, clusters: &[&ShapedCluster], outlines: &Hash
}
cmap.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
let cmap = doc.add(Object::Stream(Stream::flate(Dict::new(), cmap.as_bytes())));
let family = pdfcraft_fonts::document_arabic_font().map_or("Arabic", |f| f.family);
// The face's family when every glyph comes from one script; without the face, the script's
// name (as Arabic items were always named).
let family = match clusters.first().map(|c| c.script).filter(|s| clusters.iter().all(|c| c.script == *s)) {
Some(script) => script.font().map_or(
match script {
ShapedScript::Arabic => "Arabic",
ShapedScript::Thai => "Thai",
},
|f| f.family,
),
None => "PdfCraft Shaped Text",
};
let mut descriptor = Dict::new();
descriptor.set(b"Type".to_vec(), Object::name("FontDescriptor"));
descriptor.set(b"FontName".to_vec(), Object::name(&family.replace(' ', "")));
@ -611,8 +697,8 @@ fn draw(doc: &mut Document, c: &Content, view: [f64; 6], taken: &Dict, existing:
}
let mut fonts = Dict::new();
fonts.set(name.clone().into_bytes(), Object::Dict(font));
if t.text.chars().any(is_arabic) && (pdfcraft_fonts::document_arabic_font().is_some() || !existing) {
draw_arabic(doc, t, taken, &mut fonts, &mut out)?;
if t.text.chars().any(is_thai) || (t.text.chars().any(is_arabic) && (pdfcraft_fonts::document_arabic_font().is_some() || !existing)) {
draw_shaped(doc, t, taken, &mut fonts, &mut out)?;
res.set(b"Font".to_vec(), Object::Dict(fonts));
out.extend_from_slice(b"Q\n");
return Ok((out, res));
@ -840,11 +926,17 @@ fn validate(c: &Content) -> Result<(), EditError> {
if (r[2] - r[0]).abs() < 1.0 {
return Err(EditError::Invalid("the text box is too narrow".into()));
}
if t.text.chars().any(is_thai) && pdfcraft_fonts::document_thai_font().is_none() {
return Err(thai_error(GlyphError::NoFont, &t.text));
}
// Without the Arabic face, `draw` decides for the Arabic letters themselves (#403): a new
// item is refused with what's missing, and one that already holds Arabic stays movable.
// Everything else in it is drawn in the standard font, so it still has to fit.
let undrawable = if t.text.chars().any(shaped_arabic) && pdfcraft_fonts::document_arabic_font().is_none() {
t.text.split('\n').find_map(|line| line.chars().find(|c| !shaped_arabic(*c) && !pdfcraft_fonts::win_ansi_encodable(*c)))
t.text.split('\n').find_map(|line| {
line.chars()
.find(|c| !shaped_arabic(*c) && !(is_thai(*c) && pdfcraft_fonts::thai_has(*c)) && !pdfcraft_fonts::win_ansi_encodable(*c))
})
} else {
first_undrawable(t)
};
@ -963,7 +1055,7 @@ pub fn delete_content(doc: &mut Document, page: usize, index: usize) -> Result<(
Ok(())
}
/// The Type 3 fonts an Arabic item's stream shows text with. Each item has its own (`draw_arabic`
/// The Type 3 fonts an Arabic item's stream shows text with. Each item has its own (`draw_shaped`
/// names them so), so they go when the item is replaced or deleted.
fn own_fonts(doc: &Document, obj: ObjRef) -> Vec<Vec<u8>> {
let Object::Stream(s) = &*doc.get(obj) else { return Vec::new() };

View File

@ -12,14 +12,14 @@ const SMALL_ITEM_FINGERPRINT: u64 = 14_135_892_979_695_233_112;
/// A cluster that shows one character with the single glyph `id`, at `advance` em.
fn cluster(id: u32, advance: f64) -> ShapedCluster {
ShapedCluster { glyphs: vec![(id, [0.0, 0.0])], advance, text: "ب".into() }
ShapedCluster { script: ShapedScript::Arabic, glyphs: vec![(id, [0.0, 0.0])], advance, text: "ب".into() }
}
/// One run of `clusters` as a line of an item at `item()`'s size.
fn line(clusters: Vec<ShapedCluster>, rtl: bool) -> LaidLine {
let text = clusters.iter().map(|c| c.text.as_str()).collect();
let width = clusters.iter().map(|c| c.advance).sum::<f64>() * item().size;
(text, rtl, vec![Piece::Arabic(clusters)], width)
(text, rtl, vec![Piece::Shaped(clusters)], width)
}
/// A glyph outline: a box on the baseline, `advance` em wide.
@ -85,7 +85,7 @@ fn an_item_past_the_old_glyph_limit_gets_a_font_per_240_glyphs() {
let laid = vec![line((0..n).map(|i| cluster(i as u32 + 1, 0.5)).collect(), false)];
let mut doc = Document::new_empty();
let (mut fonts, mut out) = (Dict::new(), Vec::new());
let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_| Ok(boxed(0.5)));
let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, _| Ok(boxed(0.5)));
assert!(drawn.is_ok(), "{drawn:?}");
let made = fonts_of(&doc, &fonts);
let mut expected = vec![GLYPHS_PER_FONT; 16];
@ -108,7 +108,7 @@ fn an_outline_missing_from_a_later_font_leaves_the_document_unchanged() {
let before = snapshot(&doc);
let missing = n as u32;
let (mut fonts, mut out) = (Dict::new(), Vec::new());
let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |id| {
let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, id| {
if id == missing { Err(GlyphError::Missing) } else { Ok(boxed(0.5)) }
});
assert!(matches!(&drawn, Err(EditError::Invalid(msg)) if msg.contains("can't show")), "{drawn:?}");
@ -120,15 +120,15 @@ fn an_outline_missing_from_a_later_font_leaves_the_document_unchanged() {
fn an_item_below_the_old_limit_is_drawn_exactly_as_before() {
// A left-to-right line with a Latin run, a space and a two-glyph cluster, then a right-to-left
// line. Glyphs repeat across the two lines and share one font.
let space = ShapedCluster { glyphs: vec![(6, [0.0, 0.0])], advance: 0.25, text: " ".into() };
let ligature = ShapedCluster { glyphs: vec![(4, [0.0, 0.0]), (5, [-0.1, 0.2])], advance: 0.6, text: "لا".into() };
let space = ShapedCluster { script: ShapedScript::Arabic, glyphs: vec![(6, [0.0, 0.0])], advance: 0.25, text: " ".into() };
let ligature = ShapedCluster { script: ShapedScript::Arabic, glyphs: vec![(4, [0.0, 0.0]), (5, [-0.1, 0.2])], advance: 0.6, text: "لا".into() };
let mixed: LaidLine =
("Hi ب".into(), false, vec![Piece::Latin("Hi ".into()), Piece::Arabic(vec![cluster(1, 0.5), cluster(2, 0.5), space, ligature])], 4.0);
("Hi ب".into(), false, vec![Piece::Latin("Hi ".into()), Piece::Shaped(vec![cluster(1, 0.5), cluster(2, 0.5), space, ligature])], 4.0);
let rtl = line(vec![cluster(2, 0.5), cluster(9, 0.75)], true);
let laid = vec![mixed, rtl];
let mut doc = Document::new_empty();
let (mut fonts, mut out) = (Dict::new(), Vec::new());
let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |id| Ok(boxed(0.5 + f64::from(id) * 0.01)));
let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, id| Ok(boxed(0.5 + f64::from(id) * 0.01)));
assert!(drawn.is_ok(), "{drawn:?}");
assert_eq!(fonts.len(), 1);
let mut bytes = out.clone();
@ -150,7 +150,7 @@ fn long_text_with_few_distinct_glyphs_needs_one_font() {
let reads = Cell::new(0usize);
let mut doc = Document::new_empty();
let (mut fonts, mut out) = (Dict::new(), Vec::new());
let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_| {
let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, _| {
reads.set(reads.get() + 1);
Ok(boxed(0.5))
});
@ -164,13 +164,18 @@ fn long_text_with_few_distinct_glyphs_needs_one_font() {
fn distinct_clusters_past_the_old_limit_share_their_glyph_outlines() {
// 3,841 different clusters (their advances differ) built from four glyphs: four outlines are read.
let clusters: Vec<ShapedCluster> = (0..3_841u32)
.map(|i| ShapedCluster { glyphs: vec![(1 + i % 4, [0.0, 0.0])], advance: 0.5 + f64::from(i) * 1e-6, text: "ب".into() })
.map(|i| ShapedCluster {
script: ShapedScript::Arabic,
glyphs: vec![(1 + i % 4, [0.0, 0.0])],
advance: 0.5 + f64::from(i) * 1e-6,
text: "ب".into(),
})
.collect();
let laid = vec![line(clusters, false)];
let reads = Cell::new(0usize);
let mut doc = Document::new_empty();
let (mut fonts, mut out) = (Dict::new(), Vec::new());
let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_| {
let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, _| {
reads.set(reads.get() + 1);
Ok(boxed(0.5))
});
@ -183,18 +188,18 @@ fn distinct_clusters_past_the_old_limit_share_their_glyph_outlines() {
#[test]
fn hostile_clusters_draw_empty_or_fail_without_changing_the_document() {
// A cluster with no glyphs and no text draws nothing, but it is still one glyph of a font.
let empty = ShapedCluster { glyphs: Vec::new(), advance: 0.0, text: String::new() };
let empty = ShapedCluster { script: ShapedScript::Arabic, glyphs: Vec::new(), advance: 0.0, text: String::new() };
let laid = vec![line(vec![empty, cluster(1, 0.5)], false)];
let mut doc = Document::new_empty();
let (mut fonts, mut out) = (Dict::new(), Vec::new());
let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_| Ok(boxed(0.5)));
let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, _| Ok(boxed(0.5)));
assert!(drawn.is_ok(), "{drawn:?}");
assert_eq!(glyphs_shown(&out).len(), 2);
// A glyph id the face doesn't have fails the item before anything is written.
let before = snapshot(&doc);
let laid = vec![line(vec![cluster(1, 0.5), cluster(u32::MAX, 0.5)], false)];
let (mut fonts, mut out) = (Dict::new(), Vec::new());
let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |id| {
let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, id| {
if id == u32::MAX { Err(GlyphError::Missing) } else { Ok(boxed(0.5)) }
});
assert!(drawn.is_err());
@ -207,7 +212,7 @@ fn a_font_name_the_page_already_uses_is_not_reused() {
let laid = vec![line(vec![cluster(1, 0.5)], false)];
let mut doc = Document::new_empty();
let (mut fonts, mut out) = (Dict::new(), Vec::new());
let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_| Ok(boxed(0.5)));
let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &Dict::new(), &mut fonts, &mut out, |_, _| Ok(boxed(0.5)));
assert!(drawn.is_ok(), "{drawn:?}");
let first = fonts_of(&doc, &fonts)[0].0.clone();
// The same item on a page whose resources already use that name.
@ -215,7 +220,7 @@ fn a_font_name_the_page_already_uses_is_not_reused() {
taken.set(first.clone(), Object::Null);
let mut doc = Document::new_empty();
let (mut fonts, mut out) = (Dict::new(), Vec::new());
let drawn = draw_laid_arabic(&mut doc, &item(), &laid, &taken, &mut fonts, &mut out, |_| Ok(boxed(0.5)));
let drawn = draw_laid_shaped(&mut doc, &item(), &laid, &taken, &mut fonts, &mut out, |_, _| Ok(boxed(0.5)));
assert!(drawn.is_ok(), "{drawn:?}");
let second = fonts_of(&doc, &fonts)[0].0.clone();
assert_ne!(second, first);

View File

@ -2245,6 +2245,37 @@ fn hostile_form_xobjects_are_read_once() {
assert!(images::reading_images(&doc, 0).unwrap().is_empty());
}
#[test]
fn added_thai_text_wraps_clusters_and_survives_reopen() {
let mut doc = fixture();
let t = AddedText {
rect: [20.0, 100.0, 250.0, 300.0], text: "สวัสดี น้ำ ผู้ใช้ ปู่ ๑๒๓ Hello 123".into(), size: 16.0, ..AddedText::default()
};
if pdfcraft_fonts::document_thai_font().is_none() {
assert_eq!(first_undrawable(&t), Some('ส'));
let before = list_added(&doc);
let err = add_content(&mut doc, 0, &Content::Text(t)).unwrap_err().to_string();
assert!(err.contains("Thai") && err.contains("CRAFT_FONTS_DIR"), "{err}");
assert_eq!(list_added(&doc), before);
return;
}
assert_eq!(first_undrawable(&t), None);
add_content(&mut doc, 0, &Content::Text(t.clone())).unwrap();
let reopened = reopen(&doc);
assert_eq!(list_added(&reopened)[0].content, Content::Text(AddedText { rect: super::added::text_rect(&t), ..t.clone() }));
let narrow = AddedText { rect: [0.0, 0.0, 16.0, 200.0], text: "กิ่กิ่กิ่".into(), ..t.clone() };
assert_eq!(super::added::lines(&narrow), ["กิ่", "กิ่", "กิ่"]);
update_content(&mut doc, 0, 0, &Content::Text(AddedText { text: "แก้ไข น้ำ".into(), ..t.clone() })).unwrap();
assert!(!own(&doc, 0).is_empty());
let bad = Content::Text(AddedText { text: "ไทย 日".into(), ..t });
let before = list_added(&doc);
assert!(update_content(&mut doc, 0, 0, &bad).is_err());
assert_eq!(list_added(&doc), before);
delete_content(&mut doc, 0, 0).unwrap();
assert!(list_added(&doc).is_empty());
assert!(arabic_font_names(&doc, 0).is_empty());
}
#[test]
fn group_moves_preserve_glyph_bytes_and_unselected_text_state() {
for content in [

View File

@ -50,6 +50,10 @@ set). Without that variable `CRAFT_FONTS` is empty and everything below copes:
- `document_japanese_fonts_for_style`: every `Jpan` face in that order of preference, so the
editor can use the first one that has all of the replacement's glyphs (the faces differ in
coverage, e.g. of Cyrillic).
- `ui_thai_fonts` / `document_thai_font` / `shape_thai`: the `Thai` faces for the interface
and shaped Thai Add Text content. Harfrust positions stacked vowel/tone marks and decomposes
Sara Am; cluster ToUnicode entries preserve the original text. The shared Type 3 writer
embeds outlines, so reopening does not depend on installed fonts.
- `SHIPPORI_MINCHO`: Shippori Mincho's bytes, or `None`.
Generated Japanese Type 3 fonts record the fallback face's family and style in an indirect
@ -60,7 +64,7 @@ The descriptor does not change glyph programs or replace the Type 3 `FontBBox` m
only Gothic Regular, so it cannot preserve bold. No new font assets or changes to that input are
needed.
wasm32 builds embed only BIZ UDPGothic Regular and any `Arab`, `Telu` or `Hebr` face, to keep the web build small: the web build
wasm32 builds embed only BIZ UDPGothic Regular and any `Arab`, `Telu`, `Hebr` or `Thai` face, to keep the web build small: the web build
currently has no Chinese face, so Chinese there still shows the replacement glyph.
Font files are never
committed here (`AGENTS.md` §1.4; team members: [craftrules `standards/fonts.md`](https://github.com/storytold/craftrules/blob/main/standards/fonts.md), internal).

View File

@ -1,7 +1,7 @@
//! Embeds the fonts of the optional craft-fonts build input (`CRAFT_FONTS_DIR`) as `CRAFT_FONTS`.
//! The recipe is craft-fonts' `docs/integration.md`; unset, `CRAFT_FONTS` is empty. It only reads
//! the local checkout (no network). On wasm32 it embeds only the UI faces (BIZ UDPGothic Regular
//! and any `Arab`, `Telu` or `Hebr` face), to keep the web build within hosting limits (Cloudflare Pages:
//! and any `Arab`, `Telu`, `Hebr` or `Thai` face), to keep the web build within hosting limits (Cloudflare Pages:
//! 25 MiB per file).
use std::fmt::Write as _;
@ -55,7 +55,8 @@ fn craft_fonts(dir: &std::path::Path) -> Result<String, String> {
let telugu = scripts.split(',').any(|s| s.trim() == "Telu");
// And the Hebrew ones (small), for Hebrew text and Edit text on the web.
let hebrew = scripts.split(',').any(|s| s.trim() == "Hebr");
let web_face = (*family == "BIZ UDPGothic" && *style == "Regular") || arabic || telugu || hebrew;
let web_face =
(*family == "BIZ UDPGothic" && *style == "Regular") || arabic || telugu || hebrew || scripts.split(',').any(|s| s.trim() == "Thai");
if wasm && !web_face {
continue;
}

View File

@ -66,6 +66,15 @@ pub fn ui_telugu_fonts() -> Vec<&'static CraftFont> {
CRAFT_FONTS.iter().filter(|f| f.covers("Telu")).collect()
}
/// Thai faces shared by the interface and PDF text writer.
pub fn ui_thai_fonts() -> Vec<&'static CraftFont> {
CRAFT_FONTS.iter().filter(|f| f.covers("Thai")).collect()
}
pub fn document_thai_font() -> Option<&'static CraftFont> {
ui_thai_fonts().into_iter().next()
}
/// The `Hebr` craft-fonts faces for Hebrew interface text (file names, document titles, text
/// being edited), in manifest order. Empty when built without craft-fonts or when it has no
/// Hebrew face.

View File

@ -5,19 +5,20 @@
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod arabic;
mod craft;
mod encodings;
pub mod pdf;
mod script;
pub use arabic::{ShapedCluster, arabic_glyph, arabic_has, shape_arabic};
mod shaping;
pub use craft::{
CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_hebrew_font, document_japanese_font, document_japanese_font_for_style,
document_japanese_fonts_for_style, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_hebrew_fonts, ui_japanese_fonts, ui_telugu_fonts,
document_japanese_fonts_for_style, document_thai_font, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_hebrew_fonts, ui_japanese_fonts,
ui_telugu_fonts, ui_thai_fonts,
};
pub use script::{
GlyphError, GlyphOutline, INTER_REGULAR, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_missing, script_outline,
};
pub use shaping::{ShapedCluster, ShapedScript, arabic_glyph, arabic_has, shape_arabic, shape_thai, shaped_glyph, thai_has};
/// Approximate advance of `s` in Helvetica (or Arial) at `size` points.
pub fn helvetica_width(s: &str, size: f64) -> f64 {

View File

@ -1,5 +1,4 @@
//! Arabic text written into PDFs: shaping (joining forms, lam-alef ligatures, mark placement) with
//! the craft-fonts `Arab` face, and that face's glyph outlines.
//! Arabic and Thai text shaping with the matching craft-fonts face and bounded glyph outlines.
use std::sync::OnceLock;
@ -10,10 +9,12 @@ use skrifa::{FontRef, GlyphId, MetadataProvider};
use crate::{GlyphError, GlyphOutline};
/// One cluster of shaped Arabic text: the glyphs that show one character (a letter's body and
/// One cluster of shaped text: the glyphs that show one character (a letter's body and
/// dots, say), or several characters for a ligature. Em units.
#[derive(Clone, Debug, PartialEq)]
pub struct ShapedCluster {
/// Face that owns the glyph ids.
pub script: ShapedScript,
/// Glyph ids, each with its offset from the cluster's pen position (marks sit above or below).
pub glyphs: Vec<(u32, [f64; 2])>,
pub advance: f64,
@ -21,34 +22,76 @@ pub struct ShapedCluster {
pub text: String,
}
fn face() -> Option<&'static (FontRef<'static>, ShaperData)> {
/// The face owning a shaped glyph id. IDs from different faces must never share a cache key.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ShapedScript {
Arabic,
Thai,
}
impl ShapedScript {
pub fn font(self) -> Option<&'static crate::CraftFont> {
match self {
Self::Arabic => crate::document_arabic_font(),
Self::Thai => crate::document_thai_font(),
}
}
}
fn face(script: ShapedScript) -> Option<&'static (FontRef<'static>, ShaperData)> {
static FACE: OnceLock<Option<(FontRef<'static>, ShaperData)>> = OnceLock::new();
FACE.get_or_init(|| {
let font = FontRef::new(crate::document_arabic_font()?.bytes).ok()?;
let data = ShaperData::new(&font);
Some((font, data))
})
.as_ref()
static THAI: OnceLock<Option<(FontRef<'static>, ShaperData)>> = OnceLock::new();
let cache = match script {
ShapedScript::Arabic => &FACE,
ShapedScript::Thai => &THAI,
};
cache
.get_or_init(|| {
let font = FontRef::new(script.font()?.bytes).ok()?;
let data = ShaperData::new(&font);
Some((font, data))
})
.as_ref()
}
/// Whether the Arabic document face has a glyph for `c` (false without the face).
pub fn arabic_has(c: char) -> bool {
face().is_some_and(|(font, _)| font.charmap().map(c).is_some())
face(ShapedScript::Arabic).is_some_and(|(font, _)| font.charmap().map(c).is_some())
}
/// Shape one directional run of `text` with the Arabic document face. The clusters come in
/// drawing order, left to right. [`GlyphError::NoFont`] without the face;
/// [`GlyphError::Missing`] when it lacks a character.
pub fn shape_arabic(text: &str, rtl: bool) -> Result<Vec<ShapedCluster>, GlyphError> {
let (font, data) = face().ok_or(GlyphError::NoFont)?;
shape_script(text, rtl, ShapedScript::Arabic)
}
pub fn thai_has(c: char) -> bool {
face(ShapedScript::Thai).is_some_and(|(font, _)| font.charmap().map(c).is_some())
}
/// Shape Thai with grapheme clusters so Sara Am decomposition and stacked marks keep their
/// original Unicode text exactly once, including when wrapped or copied from the saved PDF.
pub fn shape_thai(text: &str) -> Result<Vec<ShapedCluster>, GlyphError> {
shape_script(text, false, ShapedScript::Thai)
}
fn shape_script(text: &str, rtl: bool, kind: ShapedScript) -> Result<Vec<ShapedCluster>, GlyphError> {
let (font, data) = face(kind).ok_or(GlyphError::NoFont)?;
let shaper = data.shaper(font).build();
let scale = 1.0 / f64::from(shaper.units_per_em().max(1));
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.set_direction(if rtl { Direction::RightToLeft } else { Direction::LeftToRight });
buffer.set_script(script::ARABIC);
// One cluster per character, so marks keep their own text for copy and search.
buffer.set_cluster_level(BufferClusterLevel::Characters);
buffer.set_script(match kind {
ShapedScript::Arabic => script::ARABIC,
ShapedScript::Thai => script::THAI,
});
// Preserve the historical Arabic per-character mapping; Thai marks stay with their base.
buffer.set_cluster_level(match kind {
ShapedScript::Arabic => BufferClusterLevel::Characters,
ShapedScript::Thai => BufferClusterLevel::MonotoneGraphemes,
});
let shaped = shaper.shape(buffer, ShapeOptions::new());
// Clusters are byte offsets into `text`; a cluster shows the characters up to the next one.
let mut starts: Vec<usize> = shaped.glyph_infos().iter().map(|g| g.cluster as usize).collect();
@ -66,7 +109,7 @@ pub fn shape_arabic(text: &str, rtl: bool) -> Result<Vec<ShapedCluster>, GlyphEr
_ => {
let end = starts.get(starts.partition_point(|s| *s <= start)).copied().unwrap_or(text.len());
let text = text.get(start..end).unwrap_or_default().to_string();
out.push((start, ShapedCluster { glyphs: Vec::new(), advance: 0.0, text }));
out.push((start, ShapedCluster { script: kind, glyphs: Vec::new(), advance: 0.0, text }));
// Just pushed.
let Some((_, c)) = out.last_mut() else { continue };
c
@ -82,7 +125,12 @@ pub fn shape_arabic(text: &str, rtl: bool) -> Result<Vec<ShapedCluster>, GlyphEr
/// Glyph `id` of the Arabic document face, bounded like [`crate::japanese_glyph`]. Glyphs without
/// outlines (spaces) come back empty.
pub fn arabic_glyph(id: u32) -> Result<GlyphOutline, GlyphError> {
let (font, _) = face().ok_or(GlyphError::NoFont)?;
shaped_glyph(ShapedScript::Arabic, id)
}
/// A bounded outline from the same face that produced the shaped glyph id.
pub fn shaped_glyph(script: ShapedScript, id: u32) -> Result<GlyphOutline, GlyphError> {
let (font, _) = face(script).ok_or(GlyphError::NoFont)?;
if font.maxp().map_or(true, |m| id >= u32::from(m.num_glyphs())) {
return Err(GlyphError::Missing);
}
@ -169,3 +217,36 @@ mod tests {
assert!(shape_arabic("", true).unwrap().is_empty());
}
}
#[cfg(test)]
mod thai_tests {
use super::*;
#[test]
fn thai_clusters_preserve_sara_am_and_stacked_marks() {
if crate::document_thai_font().is_none() {
assert_eq!(shape_thai("น้ำ"), Err(GlyphError::NoFont));
assert!(!thai_has('ก'));
return;
}
for text in ["น้ำ", "กิ่", "ผู้ใช้", "ปู่", "เก้า", "สวัสดีภาษาไทย", "๑๒๓", ""]
{
let clusters = shape_thai(text).unwrap();
assert_eq!(clusters.iter().map(|c| c.text.as_str()).collect::<String>(), text);
for c in clusters {
assert_eq!(c.script, ShapedScript::Thai);
assert!(c.advance.is_finite() && c.advance >= 0.0);
for (id, pos) in c.glyphs {
assert!(pos.iter().all(|v| v.is_finite()));
shaped_glyph(c.script, id).unwrap();
}
}
}
let stacked = shape_thai("กิ่").unwrap();
assert_eq!(stacked.len(), 1, "base, vowel and tone must wrap together");
assert!(stacked[0].glyphs.len() >= 3);
assert!(stacked[0].glyphs.iter().any(|(_, p)| p[0] != 0.0 || p[1] != 0.0));
assert_eq!(shape_thai("日"), Err(GlyphError::Missing));
assert_eq!(shaped_glyph(ShapedScript::Thai, u32::MAX).map(|_| ()), Err(GlyphError::Missing));
}
}

View File

@ -37,7 +37,7 @@ pub(crate) type UndrawableMemo = std::cell::RefCell<Option<(AddedText, Option<ch
/// The warning shown for text with `c` in it.
pub(crate) fn undrawable_message(c: char) -> String {
crate::i18n::fmt(
tl!("The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now."),
tl!("The PDF text fonts can't draw “{c}” ({code}). Choose supported text."),
&[("c", &c.to_string()), ("code", &format!("U+{:04X}", u32::from(c)))],
)
}
@ -466,7 +466,7 @@ pub(crate) fn format_panel(ui: &mut egui::Ui, t: &Tokens, style: &AddedText) ->
if let Some(picked) = crate::comments::swatch_grid(ui, Some(c)) {
s.color = picked;
}
ui.label(egui::RichText::new(tl!("Standard fonts; text outside Windows-1252 isn't supported yet.")).small().color(t.text_faint));
ui.label(egui::RichText::new(tl!("Thai and Arabic use regular fallback fonts when available.")).small().color(t.text_faint));
(s != *style).then_some(s)
}

View File

@ -705,7 +705,7 @@
Centre توسيط
Align right محاذاة لليمين
Justify ضبط
Standard fonts; text outside Windows-1252 isn't supported yet. خطوط قياسية؛ النص خارج Windows-1252 غير مدعوم بعد.
Thai and Arabic use regular fallback fonts when available. تستخدم التايلاندية والعربية خطوطًا بديلة بالنمط العادي عند توفرها.
Edit image تحرير الصورة
Rotate counterclockwise تدوير عكس عقارب الساعة
Rotate clockwise تدوير مع عقارب الساعة
@ -2151,6 +2151,7 @@ calendar date December ديسمبر
Reverse open arrow سهم مفتوح معكوس
Reverse closed arrow سهم مغلق معكوس
Slash شرطة مائلة
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. لا تستطيع خطوط نص PDF عرض «{c}» ({code}). اختر نصًا مدعومًا.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. الخطوط القياسية لا تستطيع رسم «{c}» ({code}). حاليًا يمكن إضافة محارف أوروبا الغربية فقط كنص.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. خط هذا الحقل لا يستطيع رسم «{c}» ({code})، لذا يعرض PdfCraft علامة ? مكانه. القيمة محفوظة، وتعيد برامج عرض PDF الأخرى رسم الحقل منها.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. الخطوط التي يكتب بها PdfCraft لا تستطيع رسم «{c}» ({code})، لذا يظهر بشكل ? في كل برامج عرض PDF. النص محفوظ.

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -385,7 +385,7 @@
Centre Центрирай
Align right Подравни вдясно
Justify Двустранно подравняване
Standard fonts; text outside Windows-1252 isn't supported yet. Стандартни шрифтове; текст извън Windows-1252 все още не се поддържа.
Thai and Arabic use regular fallback fonts when available. Тайландският и арабският използват резервни шрифтове с нормален стил, когато са налични.
Edit image Редактирай изображение
Rotate counterclockwise Завърти обратно на часовниковата стрелка
Rotate clockwise Завърти по часовниковата стрелка
@ -2093,6 +2093,7 @@ action wizard Start Старт
Clear Recent Files Изчисти последно отваряните файлове
Remove from Recent Премахни от последно отваряните
Remove {name} from Recent Премахни {name} от последно отваряните
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Шрифтовете за PDF текст не могат да изобразят „{c}“ ({code}). Изберете поддържан текст.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартните шрифтове не могат да покажат „{c}“ ({code}). Засега като текст могат да се добавят само западноевропейски символи.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Шрифтът на това поле не може да покаже „{c}“ ({code}), затова PdfCraft показва ? на негово място. Стойността е запазена и другите програми за PDF преначертават полето по нея.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Шрифтовете, с които PdfCraft пише, не могат да покажат „{c}“ ({code}), затова във всяка програма за PDF то се показва като ?. Текстът е запазен.

Can't render this file because it contains an unexpected character in line 8 and column 16.

View File

@ -782,7 +782,7 @@ key Space Mezerník
Centre Na střed
Align right Zarovnat doprava
Justify Do bloku
Standard fonts; text outside Windows-1252 isn't supported yet. Standardní písma; text mimo Windows-1252 zatím není podporován.
Thai and Arabic use regular fallback fonts when available. Thajština a arabština používají běžná náhradní písma, pokud jsou k dispozici.
Edit image Upravit obrázek
Rotate counterclockwise Otočit proti směru hodinových ručiček
Rotate clockwise Otočit po směru hodinových ručiček
@ -2178,7 +2178,7 @@ calendar date December prosince
Home / End Home / End
H / Space (hold) H / mezerník (podržet)
Line ending Zakončení čáry
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standardní písma neumí vykreslit „{c}“ ({code}). Jako text lze zatím přidávat jen západoevropské znaky.
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Písma pro text v PDF neumí vykreslit „{c}“ ({code}). Zvolte podporovaný text.
Larger pages Větší stránky
Smaller pages Menší stránky
Reset page size Obnovit velikost stránek

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -678,7 +678,7 @@
Centre Zentriert
Align right Rechtsbündig
Justify Blocksatz
Standard fonts; text outside Windows-1252 isn't supported yet. Standardschriften; Text außerhalb von Windows-1252 wird noch nicht unterstützt.
Thai and Arabic use regular fallback fonts when available. Thai und Arabisch verwenden, falls verfügbar, Ersatzschriften im normalen Schriftschnitt.
Edit image Bild bearbeiten
Rotate counterclockwise Gegen den Uhrzeigersinn drehen
Rotate clockwise Im Uhrzeigersinn drehen
@ -2135,6 +2135,7 @@ action wizard Start Starten
Clear Recent Files Liste „Zuletzt geöffnet“ leeren
Remove from Recent Aus „Zuletzt geöffnet“ entfernen
Remove {name} from Recent {name} aus „Zuletzt geöffnet“ entfernen
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Die PDF-Textschriften können „{c}“ ({code}) nicht darstellen. Wählen Sie unterstützten Text.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Die Standardschriften können „{c}“ ({code}) nicht darstellen. Derzeit lassen sich nur westeuropäische Zeichen als Text hinzufügen.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Die Schrift dieses Felds kann „{c}“ ({code}) nicht darstellen, daher zeigt PdfCraft an dieser Stelle ? an. Der Wert ist gespeichert, und andere PDF-Viewer zeichnen das Feld anhand des Werts neu.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Die Schriften, mit denen PdfCraft schreibt, können „{c}“ ({code}) nicht darstellen, daher erscheint es in jedem PDF-Viewer als ?. Der Text ist gespeichert.

Can't render this file because it contains an unexpected character in line 6 and column 51.

View File

@ -1437,7 +1437,7 @@
Couldn't import the signature image: {e} Δεν ήταν δυνατή η εισαγωγή της εικόνας υπογραφής: {e}
Go to page… Μετάβαση στη σελίδα…
Reopen the files that were open when PdfCraft last closed Εκ νέου άνοιγμα των αρχείων που ήταν ανοιχτά κατά τον τελευταίο τερματισμό του PdfCraft
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Οι τυπικές γραμματοσειρές δεν μπορούν να εμφανίσουν τον χαρακτήρα «{c}» ({code}). Προς το παρόν μπορούν να προστεθούν ως κείμενο μόνο δυτικοευρωπαϊκοί χαρακτήρες.
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Οι γραμματοσειρές κειμένου PDF δεν μπορούν να εμφανίσουν τον χαρακτήρα «{c}» ({code}). Επιλέξτε κείμενο που υποστηρίζεται.
Open arrow Ανοιχτή αιχμή βέλους
Closed arrow Κλειστή αιχμή βέλους
Butt Επίπεδο άκρο
@ -1984,7 +1984,7 @@
Field names with + - * / and parentheses; put \\ before spaces in names. Ονόματα πεδίων με + - * / και παρενθέσεις· προσθέστε \\ πριν από τα κενά των ονομάτων.
Format text Μορφοποίηση κειμένου
Centre Κεντράρισμα
Standard fonts; text outside Windows-1252 isn't supported yet. Τυπικές γραμματοσειρές· κείμενο εκτός της κωδικοποίησης Windows-1252 δεν υποστηρίζεται ακόμη.
Thai and Arabic use regular fallback fonts when available. Τα ταϊλανδικά και τα αραβικά χρησιμοποιούν κανονικές εναλλακτικές γραμματοσειρές, όταν είναι διαθέσιμες.
Crop (% trimmed from each side) Περικοπή (% αφαίρεσης από κάθε πλευρά)
Click on the page to add text; each click starts a new box, and ✓ finishes. Drag items to move them, drag a corner to resize, double-click text to edit it. Κάντε κλικ στη σελίδα για να προσθέσετε κείμενο· κάθε κλικ δημιουργεί νέο πλαίσιο και το ✓ ολοκληρώνει την εισαγωγή. Σύρετε τα στοιχεία για μετακίνηση, μια γωνία για αλλαγή μεγέθους ή κάντε διπλό κλικ στο κείμενο για επεξεργασία.
Choose an image Επιλογή εικόνας

Can't render this file because it has a wrong number of fields in line 5.

View File

@ -699,7 +699,7 @@
Centre Centrar
Align right Alinear a la derecha
Justify Justificar
Standard fonts; text outside Windows-1252 isn't supported yet. Fuentes estándar; el texto fuera de Windows-1252 aún no es compatible.
Thai and Arabic use regular fallback fonts when available. El tailandés y el árabe usan fuentes alternativas de estilo normal cuando están disponibles.
Edit image Editar imagen
Rotate counterclockwise Girar a la izquierda
Rotate clockwise Girar a la derecha
@ -2048,6 +2048,7 @@
Couldn't import the signature image: {e} No se pudo importar la imagen de la firma: {e}
Go to page… Ir a la página…
For page sizes and margins. Para tamaños de página y márgenes.
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Las fuentes de texto PDF no pueden mostrar «{c}» ({code}). Elija texto compatible.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Las fuentes estándar no pueden mostrar «{c}» ({code}). Por ahora solo se pueden añadir como texto caracteres de Europa occidental.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. La fuente de este campo no puede mostrar «{c}» ({code}), así que PdfCraft muestra ? en su lugar. El valor se guarda y los demás visores de PDF vuelven a dibujar el campo a partir de él.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Las fuentes con las que escribe PdfCraft no pueden mostrar «{c}» ({code}), así que aparece como ? en todos los visores de PDF. El texto se guarda.

Can't render this file because it contains an unexpected character in line 6 and column 60.

View File

@ -678,7 +678,7 @@
Centre Centrer
Align right Aligner à droite
Justify Justifier
Standard fonts; text outside Windows-1252 isn't supported yet. Polices standard ; le texte hors Windows-1252 n'est pas encore pris en charge.
Thai and Arabic use regular fallback fonts when available. Le thaï et l’arabe utilisent des polices de remplacement de style normal lorsqu’elles sont disponibles.
Edit image Modifier l'image
Rotate counterclockwise Pivoter vers la gauche
Rotate clockwise Pivoter vers la droite
@ -2089,6 +2089,7 @@
No recent files Aucun fichier récent
Go to page… Aller à la page…
For page sizes and margins. Pour les tailles de page et les marges.
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Les polices de texte PDF ne peuvent pas afficher « {c} » ({code}). Choisissez un texte pris en charge.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Les polices standard ne peuvent pas afficher « {c} » ({code}). Pour l'instant, seuls les caractères d'Europe occidentale peuvent être ajoutés comme texte.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. La police de ce champ ne peut pas afficher « {c} » ({code}) : PdfCraft affiche ? à sa place. La valeur est enregistrée, et les autres lecteurs PDF redessinent le champ à partir de celle-ci.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Les polices qu'utilise PdfCraft pour écrire ne peuvent pas afficher « {c} » ({code}) : il apparaît comme ? dans tous les lecteurs PDF. Le texte est enregistré.

Can't render this file because it contains an unexpected character in line 6 and column 39.

View File

@ -675,7 +675,7 @@
Centre Középre
Align right Jobbra igazítás
Justify Sorkizárás
Standard fonts; text outside Windows-1252 isn't supported yet. Szabványos betűtípusok; a Windows-1252-n kívüli szöveg még nem támogatott.
Thai and Arabic use regular fallback fonts when available. A thai és az arab szöveg normál stílusú helyettesítő betűtípusokat használ, ha rendelkezésre állnak.
Edit image Kép szerkesztése
Rotate counterclockwise Forgatás az óramutató járásával ellentétesen
Rotate clockwise Forgatás az óramutató járásával megegyezően
@ -2127,6 +2127,7 @@ action wizard Start Indítás
Reverse open arrow Fordított nyitott nyíl
Reverse closed arrow Fordított zárt nyíl
Slash Perjel
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. A PDF szövegének betűtípusai nem tudják megjeleníteni ezt: „{c}” ({code}). Válasszon támogatott szöveget.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. A szabványos betűtípusok nem tudják megjeleníteni ezt: „{c}” ({code}). Egyelőre csak nyugat-európai karakterek adhatók hozzá szövegként.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. A mező betűtípusa nem tudja megjeleníteni ezt: „{c}” ({code}), ezért a PdfCraft ? jelet mutat a helyén. Az érték mentve van, és a többi PDF-megjelenítő ebből rajzolja újra a mezőt.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. A PdfCraft által íráshoz használt betűtípusok nem tudják megjeleníteni ezt: „{c}” ({code}), ezért minden PDF-megjelenítőben ? látszik a helyén. A szöveg mentve van.

Can't render this file because it contains an unexpected character in line 7 and column 44.

View File

@ -701,7 +701,7 @@
Centre Centra
Align right Allinea a destra
Justify Giustifica
Standard fonts; text outside Windows-1252 isn't supported yet. Caratteri standard; il testo al di fuori di Windows-1252 non è ancora supportato.
Thai and Arabic use regular fallback fonts when available. Il thailandese e l’arabo usano caratteri sostitutivi in stile normale, quando disponibili.
Edit image Modifica immagine
Rotate counterclockwise Ruota in senso antiorario
Rotate clockwise Ruota in senso orario
@ -2144,6 +2144,7 @@
The document was closed before it could be saved. Il documento è stato chiuso prima che potesse essere salvato.
The pages were extracted but not deleted, because the document changed while you were choosing a folder. Le pagine sono state estratte ma non eliminate, perché il documento è cambiato mentre sceglievi una cartella.
The printer driver's own settings for this print: paper tray, paper type, finishing and more. Le impostazioni proprie del driver della stampante per questa stampa: vassoio, tipo di carta, finitura e altro.
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. I caratteri del testo PDF non possono visualizzare «{c}» ({code}). Scegliere un testo supportato.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Il font di questo campo non può disegnare «{c}» ({code}), quindi PdfCraft mostra ? al suo posto. Il valore è salvato e gli altri visualizzatori PDF ridisegnano il campo a partire da esso.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. I font con cui PdfCraft scrive non possono disegnare «{c}» ({code}), quindi appare come ? in ogni visualizzatore PDF. Il testo è salvato.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. I caratteri standard non possono disegnare “{c}” ({code}). Per ora si possono aggiungere come testo solo caratteri dell'Europa occidentale.

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -705,7 +705,7 @@
Centre 中央揃え
Align right 右揃え
Justify 両端揃え
Standard fonts; text outside Windows-1252 isn't supported yet. 標準フォントを使用します。Windows-1252 に含まれない文字にはまだ対応していません。
Thai and Arabic use regular fallback fonts when available. タイ語とアラビア語には、利用可能な場合、標準スタイルの代替フォントを使用します。
Edit image 画像を編集
Rotate counterclockwise 反時計回りに回転
Rotate clockwise 時計回りに回転
@ -2019,6 +2019,7 @@
Go to page… ページへ移動…
For page sizes and margins. ページサイズと余白に使います。
Reopen the files that were open when PdfCraft last closed 前回 PdfCraft を閉じたときに開いていたファイルを再度開く
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF テキスト用フォントでは「{c}」({code})を表示できません。対応している文字を使用してください。
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 標準フォントでは「{c}」({code})を表示できません。現在、テキストとして追加できるのは西ヨーロッパの文字のみです。
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. このフィールドのフォントでは「{c}」({code})を表示できないため、PdfCraft では ? と表示されます。値は保存され、他の PDF ビューアーではこの値からフィールドが再描画されます。
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft が書き込みに使うフォントでは「{c}」({code})を表示できないため、どの PDF ビューアーでも ? と表示されます。テキストは保存されています。

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -665,7 +665,7 @@
Centre Ортаға туралау
Align right Оңға туралау
Justify Екі жаққа туралау
Standard fonts; text outside Windows-1252 isn't supported yet. Стандартты қаріптер; Windows-1252-ден тыс мәтін әзірге қолдау таппайды.
Thai and Arabic use regular fallback fonts when available. Тай және араб тілдері қолжетімді болса, қалыпты қосалқы қаріптерді пайдаланады.
Edit image Суретті түзету
Rotate counterclockwise Сағат тілінің бағытына қарсы бұру
Rotate clockwise Сағат тілінің бағытымен бұру
@ -2071,7 +2071,7 @@ calendar date December Желтоқсан
H / Space (hold) H / Space (ұстап тұру)
Rotation: Бұру:
Line ending Сызық ұшы
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартты қаріптер «{c}» таңбасын ({code}) сала алмайды. Әзірге мәтін ретінде тек Батыс Еуропа таңбаларын қосуға болады.
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF мәтін қаріптері «{c}» таңбасын ({code}) сала алмайды. Қолдау көрсетілетін мәтінді таңдаңыз.
Larger pages Үлкенірек беттер
Smaller pages Кішірек беттер
Reset page size Бет өлшемін қалпына келтіру

Can't render this file because it contains an unexpected character in line 813 and column 12.

View File

@ -700,7 +700,7 @@
Centre Wyśrodkuj
Align right Wyrównaj do prawej
Justify Wyjustuj
Standard fonts; text outside Windows-1252 isn't supported yet. Standardowe czcionki; tekst spoza Windows-1252 nie jest jeszcze obsługiwany.
Thai and Arabic use regular fallback fonts when available. Tajski i arabski używają zwykłych czcionek zastępczych, gdy są dostępne.
Edit image Edytuj obraz
Rotate counterclockwise Obróć w lewo
Rotate clockwise Obróć w prawo
@ -2001,6 +2001,7 @@
Couldn't import the signature image: {e} Nie można zaimportować obrazu podpisu: {e}
Go to page… Przejdź do strony…
Reopen the files that were open when PdfCraft last closed Otwórz ponownie pliki otwarte przy ostatnim zamknięciu PdfCraft
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Czcionki tekstu PDF nie obsługują znaku „{c}” ({code}). Wybierz obsługiwany tekst.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standardowe czcionki nie obsługują znaku „{c}” ({code}). Na razie można dodawać tylko tekst ze znakami zachodnioeuropejskimi.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Czcionka tego pola nie obsługuje znaku „{c}” ({code}), więc PdfCraft wyświetla w jego miejscu ?. Wartość została zapisana, a inne przeglądarki PDF odrysują pole na jej podstawie.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Czcionki, którymi pisze PdfCraft, nie obsługują znaku „{c}” ({code}), więc w każdej przeglądarce PDF w jego miejscu widać ?. Tekst został zapisany.

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -339,7 +339,7 @@
Centre Centralizar
Align right Alinhar à direita
Justify Justificar
Standard fonts; text outside Windows-1252 isn't supported yet. Fontes padrão; texto fora de Windows-1252 ainda não é suportado.
Thai and Arabic use regular fallback fonts when available. O tailandês e o árabe usam fontes alternativas de estilo normal quando disponíveis.
Edit image Editar imagem
Rotate counterclockwise Girar no sentido anti-horário
Rotate clockwise Girar no sentido horário
@ -2031,6 +2031,7 @@
This web address mixes letters from different alphabets, a common way to imitate another site's name. Este endereço da web mistura letras de alfabetos diferentes, uma forma comum de imitar o nome de outro site.
This web address uses letters from another alphabet, which can look like familiar ones. Este endereço da web usa letras de outro alfabeto, que podem parecer familiares.
Only continue if you trust this document. An address can carry information from the document, such as what you typed into its form. Continue apenas se você confiar neste documento. Um endereço pode carregar informações do documento, como o que você digitou no formulário dele.
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. As fontes de texto PDF não podem exibir “{c}” ({code}). Escolha um texto compatível.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. As fontes padrão não conseguem desenhar “{c}” ({code}). Por enquanto, apenas caracteres da Europa Ocidental podem ser adicionados como texto.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. A fonte deste campo não consegue desenhar “{c}” ({code}), então o PdfCraft mostra ? no lugar. O valor foi salvo, e outros leitores de PDF redesenham o campo a partir dele.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. As fontes que o PdfCraft usa para escrever não conseguem desenhar “{c}” ({code}), então ele aparece como ? em qualquer leitor de PDF. O texto foi salvo.

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -699,7 +699,7 @@
Centre По центру
Align right Выровнять вправо
Justify По ширине
Standard fonts; text outside Windows-1252 isn't supported yet. Стандартные шрифты; текст вне Windows-1252 пока не поддерживается.
Thai and Arabic use regular fallback fonts when available. Для тайского и арабского текста используются доступные резервные шрифты обычного начертания.
Edit image Редактировать изображение
Rotate counterclockwise Повернуть против часовой стрелки
Rotate clockwise Повернуть по часовой стрелке
@ -2049,6 +2049,7 @@
Created a PDF from the first {n} files PDF создан из первых файлов: {n}
Go to page… Перейти на страницу…
For page sizes and margins. Для размеров страниц и полей.
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Шрифты текста PDF не могут отобразить «{c}» ({code}). Выберите поддерживаемый текст.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартные шрифты не могут отобразить «{c}» ({code}). Пока в виде текста можно добавлять только западноевропейские символы.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Шрифт этого поля не может отобразить «{c}» ({code}), поэтому PdfCraft показывает на его месте ?. Значение сохранено, и другие программы просмотра PDF перерисуют поле по нему.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Шрифты, которыми пишет PdfCraft, не могут отобразить «{c}» ({code}), поэтому во всех программах просмотра PDF на его месте будет ?. Текст сохранён.

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -1685,7 +1685,7 @@
Stamp palette Paleta žigov
Stamps Žigi
Standard Business Standardno poslovno
Standard fonts; text outside Windows-1252 isn't supported yet. Standardne pisave; besedilo zunaj nabora Windows-1252 še ni podprto.
Thai and Arabic use regular fallback fonts when available. Tajščina in arabščina uporabljata običajne nadomestne pisave, kadar so na voljo.
Standards Standardi
Star Zvezda
Start Začetek
@ -2134,7 +2134,7 @@ signature pad Type Vtipkaj
The field {name} no longer exists. Polje {name} ne obstaja več.
The printer driver's own settings for this print: paper tray, paper type, finishing and more. Lastne nastavitve gonilnika tiskalnika za to tiskanje: pladenj za papir, vrsta papirja, dodelava in drugo.
The selected fields could not be found. Select them again. Izbranih polj ni bilo mogoče najti. Izberite jih znova.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standardne pisave ne morejo izrisati znaka „{c}“ ({code}). Zaenkrat je kot besedilo mogoče dodati samo zahodnoevropske znake.
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Pisave za besedilo PDF ne morejo izrisati znaka „{c}“ ({code}). Izberite podprto besedilo.
This printer offers no settings of its own. Ta tiskalnik nima lastnih nastavitev.
To change locked fields, also set Locked to No. Za spremembo zaklenjenih polj nastavite tudi Zaklenjeno na Ne.
Today: {date} Danes: {date}

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -682,7 +682,7 @@ comment menu Edit సవరించు
Centre మధ్యకు అమర్చు
Align right కుడికి అమర్చు
Justify రెండు వైపులా అమర్చు
Standard fonts; text outside Windows-1252 isn't supported yet. ప్రామాణిక ఫాంట్‌లు; Windows-1252 పరిధికి బయటి టెక్స్ట్‌కు ఇంకా మద్దతు లేదు.
Thai and Arabic use regular fallback fonts when available. థాయ్ మరియు అరబిక్ కోసం అందుబాటులో ఉన్నప్పుడు సాధారణ శైలి ప్రత్యామ్నాయ ఫాంట్‌లను ఉపయోగిస్తారు.
Edit image చిత్ర సవరణ
Rotate counterclockwise అపసవ్యదిశలో తిప్పు
Rotate clockwise సవ్యదిశలో తిప్పు

Can't render this file because it contains an unexpected character in line 13 and column 16.

View File

@ -663,7 +663,7 @@
Centre Ortala
Align right Sağa hizala
Justify İki yana yasla
Standard fonts; text outside Windows-1252 isn't supported yet. Standart yazı tipleri; Windows-1252 dışındaki metinler henüz desteklenmiyor.
Thai and Arabic use regular fallback fonts when available. Tayca ve Arapça, mevcut olduğunda normal yedek yazı tiplerini kullanır.
Edit image Resmi düzenle
Rotate counterclockwise Saat yönünün tersine döndür
Rotate clockwise Saat yönünde döndür
@ -2094,7 +2094,7 @@ action wizard Start Başlat
Clear Recent Files Son kullanılan dosyaları temizle
Remove from Recent Son kullanılanlardan kaldır
Remove {name} from Recent {name} öğesini son kullanılanlardan kaldır
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Standart yazı tipleri “{c}” ({code}) karakterini çizemez. Şimdilik metin olarak yalnızca Batı Avrupa karakterleri eklenebilir.
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF metin yazı tipleri “{c}” ({code}) karakterini çizemez. Desteklenen bir metin seçin.
Larger pages Daha büyük sayfalar
Smaller pages Daha küçük sayfalar
Reset page size Sayfa boyutunu sıfırla

Can't render this file because it has a wrong number of fields in line 7.

View File

@ -703,7 +703,7 @@
Centre По центру
Align right Вирівняти праворуч
Justify За шириною
Standard fonts; text outside Windows-1252 isn't supported yet. Стандартні шрифти; текст поза Windows-1252 ще не підтримується.
Thai and Arabic use regular fallback fonts when available. Для тайського й арабського тексту використовуються доступні резервні шрифти звичайного накреслення.
Edit image Редагувати зображення
Rotate counterclockwise Повернути проти годинникової стрілки
Rotate clockwise Повернути за годинниковою стрілкою
@ -2151,6 +2151,7 @@ calendar date December грудня
Reverse open arrow Зворотна відкрита стрілка
Reverse closed arrow Зворотна закрита стрілка
Slash Коса риска
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. Шрифти тексту PDF не можуть відобразити «{c}» ({code}). Виберіть підтримуваний текст.
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. Стандартні шрифти не можуть відобразити «{c}» ({code}). Поки що як текст можна додавати лише західноєвропейські символи.
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. Шрифт цього поля не може відобразити «{c}» ({code}), тому PdfCraft показує на його місці ?. Значення збережено, і інші програми перегляду PDF перемалюють поле за ним.
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. Шрифти, якими пише PdfCraft, не можуть відобразити «{c}» ({code}), тому в усіх програмах перегляду PDF на його місці буде ?. Текст збережено.

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -391,7 +391,7 @@
Centre 居中
Align right 右对齐
Justify 两端对齐
Standard fonts; text outside Windows-1252 isn't supported yet. 标准字体;尚不支持 Windows-1252 之外的文本。
Thai and Arabic use regular fallback fonts when available. 泰语和阿拉伯语使用可用的常规样式备用字体。
Edit image 编辑图像
Rotate counterclockwise 逆时针旋转
Rotate clockwise 顺时针旋转
@ -2100,6 +2100,7 @@
Couldn't import the signature image: {e} 无法导入签名图像:{e}
Go to page… 转到页面…
For page sizes and margins. 用于页面尺寸和边距。
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF 文本字体无法显示“{c}”({code})。请选择受支持的文本。
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 标准字体无法显示“{c}”({code})。目前只能添加西欧字符作为文本。
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. 此字段的字体无法显示“{c}”({code}),PdfCraft 会在该处显示 ?。值已保存,其他 PDF 阅读器会根据该值重新绘制此字段。
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft 写入时使用的字体无法显示“{c}”({code}),因此它在所有 PDF 阅读器中都会显示为 ?。文本已保存。

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -347,7 +347,7 @@
Centre 居中
Align right 右對齊
Justify 兩端對齊
Standard fonts; text outside Windows-1252 isn't supported yet. 標準字型;尚不支援 Windows-1252 之外的文字。
Thai and Arabic use regular fallback fonts when available. 泰語和阿拉伯語使用可用的一般樣式備用字型。
Edit image 編輯圖片
Rotate counterclockwise 逆時針旋轉
Rotate clockwise 順時針旋轉
@ -2010,6 +2010,7 @@
Go to page… 轉到頁面…
For page sizes and margins. 用於頁面尺寸和邊距。
Reopen the files that were open when PdfCraft last closed 重新開啟上次關閉 PdfCraft 時開啟的檔案
The PDF text fonts can't draw “{c}” ({code}). Choose supported text. PDF 文字字型無法顯示「{c}」({code})。請選擇支援的文字。
The standard fonts can't draw “{c}” ({code}). Only Western European characters can be added as text for now. 標準字型無法顯示「{c}」({code})。目前只能新增西歐字元作為文字。
This field's font can't draw “{c}” ({code}), so PdfCraft shows ? in its place. The value is saved, and other PDF viewers redraw the field from it. 此欄位的字型無法顯示「{c}」({code}),PdfCraft 會在該處顯示 ?。值已儲存,其他 PDF 閱讀器會依據該值重新繪製此欄位。
The fonts PdfCraft writes with can't draw “{c}” ({code}), so it shows as ? in every PDF viewer. The text is saved. PdfCraft 寫入時使用的字型無法顯示「{c}」({code}),因此它在所有 PDF 閱讀器中都會顯示為 ?。文字已儲存。

Can't render this file because it contains an unexpected character in line 9 and column 16.

View File

@ -204,9 +204,9 @@ pub fn installed_font_definitions(cjk: CjkPreference) -> FontDefinitions {
}
/// The interface fonts: Inter (and JetBrains Mono for code) first, then egui's defaults, then
/// the CJK, Arabic, Telugu and Hebrew faces of the optional craft-fonts build input as the last
/// fallback in every family. Without craft-fonts there is no Japanese, Chinese, Arabic, Telugu or
/// Hebrew face here.
/// the CJK, Arabic, Telugu, Hebrew and Thai faces of the optional craft-fonts build input as the
/// last fallback in every family. Without craft-fonts there is no Japanese, Chinese, Arabic,
/// Telugu, Hebrew or Thai face here.
pub fn font_definitions() -> FontDefinitions {
font_definitions_for(false)
}
@ -239,9 +239,14 @@ pub fn font_definitions_for(prefer_hans: bool) -> FontDefinitions {
}
}
// Arabic-script faces (file names, document titles), Telugu faces (the Telugu interface,
// file names) and Hebrew faces (file names, text being edited) after the CJK ones; the
// file names), Hebrew faces (file names, text being edited) and Thai faces after the CJK ones; the
// ranges don't overlap.
for face in pdfcraft_fonts::ui_arabic_fonts().into_iter().chain(pdfcraft_fonts::ui_telugu_fonts()).chain(pdfcraft_fonts::ui_hebrew_fonts()) {
for face in pdfcraft_fonts::ui_arabic_fonts()
.into_iter()
.chain(pdfcraft_fonts::ui_telugu_fonts())
.chain(pdfcraft_fonts::ui_hebrew_fonts())
.chain(pdfcraft_fonts::ui_thai_fonts())
{
let name = face.name();
if !fonts.font_data.contains_key(&name) {
add(&mut fonts, &name, face.bytes);

View File

@ -211,3 +211,23 @@ fn a_kept_draft_goes_with_its_deleted_page() {
h.run_steps(2);
assert!(h.state().views[0].content.draft.is_none());
}
#[test]
fn typing_thai_text_keeps_marks_and_commits_to_page() {
if pdfcraft_fonts::document_thai_font().is_none() {
return;
}
let fonts = pdfcraft_ui_egui::theme::font_definitions();
let face = pdfcraft_fonts::document_thai_font().unwrap();
assert!(fonts.families[&egui::FontFamily::Proportional].contains(&face.name()));
let mut h = harness();
assert!(h.state_mut().execute("edit.text"));
h.run_steps(2);
let p = at(&h, 40.0, 100.0);
click(&mut h, p);
h.event(egui::Event::Text("น้ำ กิ่ ผู้ใช้ Hello".into()));
h.run_steps(3);
let p = at(&h, 40.0, 200.0);
click(&mut h, p);
assert_eq!(texts(&h), ["น้ำ กิ่ ผู้ใช้ Hello"]);
}

View File

@ -1974,8 +1974,8 @@ milestone = "M7"
status = "shipped"
commands = ["edit.text"]
tools = ["page_add_text", "content_update"]
evidence = ["crates/ui-egui/tests/content.rs::typing_moving_styling_and_deleting_added_content", "crates/ui-egui/tests/content.rs::clicking_elsewhere_keeps_the_text_and_starts_a_new_box", "crates/automation/tests/automation.rs::adding_content_through_tools", "crates/edit/src/tests.rs::added_text_and_images_are_page_content_that_stays_editable", "crates/engine/src/tests.rs::added_text_and_images_render_and_stay_editable", "crates/edit/src/tests.rs::added_arabic_text_is_shaped_in_display_order_and_stays_searchable"]
notes = "page content, editable later (retype, move, restyle); standard fonts, Windows-1252 text. Arabic text is shaped with the craft-fonts Arab face (Type 3 outlines with ToUnicode) and laid out in bidirectional display order; it is drawn regular whatever the bold/italic setting, Hebrew and other right-to-left scripts are not covered, and typing in the box is still left to right (misc.rtl-ui). Each click on the page keeps the box being typed and starts a new one; Done and Discard beside the box"
evidence = ["crates/ui-egui/tests/content.rs::typing_moving_styling_and_deleting_added_content", "crates/ui-egui/tests/content.rs::clicking_elsewhere_keeps_the_text_and_starts_a_new_box", "crates/automation/tests/automation.rs::adding_content_through_tools", "crates/edit/src/tests.rs::added_text_and_images_are_page_content_that_stays_editable", "crates/engine/src/tests.rs::added_text_and_images_render_and_stay_editable", "crates/edit/src/tests.rs::added_arabic_text_is_shaped_in_display_order_and_stays_searchable", "crates/automation/tests/automation.rs::thai_text_through_tools_saves_reopens_extracts_and_renders", "crates/ui-egui/tests/content.rs::typing_thai_text_keeps_marks_and_commits_to_page"]
notes = "page content, editable later (retype, move, restyle); standard fonts, Windows-1252 text. Arabic text is shaped with the craft-fonts Arab face (Type 3 outlines with ToUnicode) and laid out in bidirectional display order; it is drawn regular whatever the bold/italic setting, Hebrew and other right-to-left scripts are not covered, and typing in the box is still left to right (misc.rtl-ui). Thai Add Text uses the craft-fonts Thai face, with shaped clusters and ToUnicode; Thai is regular style, wrapping preserves clusters but is not dictionary-based, and forms/Fill and Sign are not covered by this change. Each click on the page keeps the box being typed and starts a new one; Done and Discard beside the box"
[[feature]]
id = "edit.find-replace"