mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 13:37:35 -06:00
Print: 4 x 6 in label paper and a Label layout for shipping labels (#1583)
* Print: 4 x 6 in label paper and a Label layout for shipping labels Thermal label printers (e.g. VEVOR Y486) could not be used for eBay/carrier labels: the paper list had no 4 x 6 in size, the chosen paper was never sent to CUPS (the queue's default was used), and labels laid out sideways on a Letter/A4 sheet printed small or rotated. - PAPERS gains "4 x 6 in label (101.6 x 152.4 mm)"; cups_media() sends the chosen sheet to the spooler as `-o media=` (Letter, Legal, Tabloid, A3-A5 and w288h432 for the label), so the driver feeds the size the PDF was laid out on. - New Layout::Label: each page is cut to its printed area (a low-resolution render, as Fit Visible does), turned a quarter turn when it is sideways in the direction that makes its text upright (text_reads_up reads the text and form matrices), and fitted uniformly with a small margin. Vector content is kept, so barcodes stay sharp and undistorted. "Turn upside down" flips it 180 degrees. One label per sheet. - Choosing the label paper selects the Label layout, and vice versa. - Strings translated in every bundled catalog. Tests: label geometry (crop, quarter turns, no stretching, margins), cups_media, text direction; print, ui-egui and automation unit tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GFXSksdMaJ3YnheY7Q6sTx * Refresh catalog hashes after merging main --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Alessio Boschi <alessio.boschi@dinova.one> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Alessio Boschi
Brandon Thomas
parent
24f93e6102
commit
980bd90087
+22
-22
@@ -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 = "3476a5c81ffadc4583688aa6d3bc4456b2b13a16d8daf8cd2b650e764d89ff9d"
|
||||
sha256 = "66aecafe992d9f1bd6cbfd2152b2aa17af77e6fed93aca5042b5756fae564cb9"
|
||||
|
||||
# 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 = "c5651b392263a30db0cd6c086e132edcc3ac4fab9a64fd786161bd5917c8d7bb"
|
||||
sha256 = "407bef213cf599815142fa917e6e0126270979796f9e7a9819c1884d761a32e6"
|
||||
|
||||
[[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 = "de71e41c22a8154ea46da3ecab69f5b972c8c8f81fa4393e12e449ab598e000f"
|
||||
sha256 = "052d78606b5a03776e7bbfc277ed1ac08622b91b882e64c039c0283adfb5fb75"
|
||||
|
||||
[[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 = "cbfa1b7938f52b6d39cec95612bea3bbff5822e2d62f2e1e9871b92bab7efd34"
|
||||
sha256 = "a4e587d787cb49c8e553b136e83441f7132569011dd5e8aedcd9bab5de5f0ba2"
|
||||
|
||||
[[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 = "018fd6acbab584c8fa1b49d5950e03e4209f9d073ea83d23bd57c5a114ef35d7"
|
||||
sha256 = "3101724a510ee4b91cb3ebd44164061b789ed74dc9142c13fd316efd11353ae4"
|
||||
|
||||
[[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 = "fa5e2462162d403c2c2d341a8d8534945724215dd21f6585ff79797dbdefb47a"
|
||||
sha256 = "0b349ab77ac8679cce6c13abb7dafbb2a8f19eb523d09573255e28d6e919ec05"
|
||||
|
||||
[[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 = "d5a3a04bf5cd9ddb2e67d5e4c938508c3f6536806f8a476445c5fea90d275e4c"
|
||||
sha256 = "3548fa14b5eb68370a81e9cdf17156f40a5b7a10a4d9771b11f9a88cb63b6f3a"
|
||||
|
||||
[[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 = "1a30e7994b3dfbcca19a6a8a3c7fde8593b4bf7bb546bd2b266b274f3a224c2e"
|
||||
sha256 = "3441b5c753be2e317aed4bb0e69fdbcd995cc2069977363c55f82b791064043c"
|
||||
|
||||
[[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 = "3bd44d5996cebc76481d9dd2ded478bbbe2cf5fd24892ad8702a2ed059cbbfa9"
|
||||
sha256 = "ddb7d9af7883e417fef04e24acef23fea1736c38d362303ab951056ccb3e4473"
|
||||
|
||||
[[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 = "0dd303b34e6ccbbed8c030e876a696bf555b8d33abac55ca02e415a5a987f067"
|
||||
sha256 = "8ce577e9405d987b0b78b89c43f9e02f5c03b300c75a22a6cb1f53d84352d1b3"
|
||||
|
||||
[[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 = "7d59c4324d82ad3be522ce6cff16e5101334b6d816e8d920c529ba2151aa4233"
|
||||
sha256 = "bbbce91dc8b5ceccaa7c224560a96cf6ba473dc8774cd3a705ac766e8142c203"
|
||||
|
||||
[[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 = "7f1b964b5ac341c3d6e8b3328a319a2c9d7e6ca358255a155fb3499cb9bb9ee8"
|
||||
sha256 = "434d3c5d0cde05d62f93ee136bc1cd6e1051e316af354f235a5479fcf23c4c42"
|
||||
|
||||
[[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 = "c1404eee8a87dde7d0900eb61f945518281e17edae26cc5615dc64e7f3335ee9"
|
||||
sha256 = "267701ae34f1f0333846d2adcfdd01e2cfd2aa78a45b57cad417ee7b91adcef9"
|
||||
|
||||
[[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 = "433e5246288f0d27ed10463fe1c33461493a32f2f793684b001546678ade2195"
|
||||
sha256 = "d38888203419c3fd635a0eb8a38cb6253ee47f3e3b90b388e545661f1f3cdc65"
|
||||
|
||||
[[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 = "70cd926ae324621d24b6c75519ccab8dfa88f7b6a64a5d52bcf8b115d3ad8e27"
|
||||
sha256 = "c714b39eafc7a292a4f592255a9f1a82522921c52775ef267e283099aeae7697"
|
||||
|
||||
[[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 = "fc29daaff6f6377ab0a9e76a64995a0210f894251872d0d9a9a59e721bf542e1"
|
||||
sha256 = "ddde7f7703c2b906256cb5f58bc9b841dd6351deee6cd24d35349c6e548181de"
|
||||
|
||||
[[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 = "cfa46745d42d4d4e14f6b3d941d83006129ae331e47a87a3ef673087ad7cb292"
|
||||
sha256 = "b7bbccc4562d55cb71276ee408fe4106172c03874b4f7c106ede65bcbcd40082"
|
||||
|
||||
[[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 = "7ee8a57e49498a4a062d04d995a1129034c8e006defaf927460cf5dcf3c19e0e"
|
||||
sha256 = "89875f19640a31f0e577d68f37a7652ebf28a2a0b9d646a9be2ee6d933839eb2"
|
||||
|
||||
[[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 = "398c94f07f44ba96f803e3ac64faf606562c5c9a5dc02765c1ed10a648c7251e"
|
||||
sha256 = "9aaa392d37a93f93e354082c8b677d558df5df1d92b46951ec7a587f8fc93776"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/lt.tsv"
|
||||
@@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "7affbbad51af9b4fd54aec7fb118500a81f145eea79070a61e645f989449560b"
|
||||
sha256 = "6568a24e04e407c75840d8119e55411431f4a6f774b3565ff9ee7d7c79a75e00"
|
||||
|
||||
|
||||
[[asset]]
|
||||
@@ -240,7 +240,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "1ea1b1ebc84ad7f84efafd6777469259709dc9f4c9ed93147f2522433d42ff5c"
|
||||
sha256 = "78c0b206ca69f255d80abd18f32a8eeaac397660928256cfed568d897cd9554c"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/accessibility.svg"
|
||||
@@ -3363,7 +3363,7 @@ 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 = "7a4558dc2e1fdbfc254b823839db49d05c6113fdf3414d250895b3063d916bba"
|
||||
sha256 = "68722b7edd99436f46e786536ca11f03ce4131748e720f460bae9a621252ebcd"
|
||||
|
||||
[[asset]]
|
||||
path = "vendor/vello_common/assets/probe.png"
|
||||
|
||||
@@ -136,7 +136,7 @@ impl Automation {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let settings = print::Settings { pages, paper, orientation, layout, content, raster };
|
||||
let settings = print::Settings { pages, paper, orientation, layout, content, raster, crops: Vec::new() };
|
||||
let sizes: Vec<(f64, f64)> = self.doc(a)?.info.pages.iter().map(|p| (p.width as f64, p.height as f64)).collect();
|
||||
let sheets = print::layout(&sizes, &settings).map_err(|e| bad(e.to_string()))?.len();
|
||||
let bytes = self.session.print_pdf(id, &settings).map_err(failed)?;
|
||||
|
||||
@@ -4297,6 +4297,20 @@ impl Session {
|
||||
pdfcraft_print::impose(&cos, settings).map_err(|e| EditError::Print(e.to_string()))
|
||||
}
|
||||
|
||||
/// For each page, whether its text mostly runs bottom-to-top ([`pdfcraft_print::text_reads_up`]);
|
||||
/// only `pages` are read, the others are `false`.
|
||||
pub fn print_text_up(&self, id: DocId, pages: &[usize]) -> Vec<bool> {
|
||||
let Some(doc) = self.get(id) else { return Vec::new() };
|
||||
let cos = match doc.editor.as_ref() {
|
||||
Some(e) => e.cos.clone(),
|
||||
None => match pdfcraft_cos::Document::open_with_password(doc.bytes.clone(), doc.password.as_deref()) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Vec::new(),
|
||||
},
|
||||
};
|
||||
(0..doc.info.pages.len()).map(|i| pages.contains(&i) && pdfcraft_print::text_reads_up(&cos, i)).collect()
|
||||
}
|
||||
|
||||
/// Combine whole files, in order, into new PDF bytes (one bookmark per file).
|
||||
pub fn combine(&self, sources: &[(String, Arc<Vec<u8>>)]) -> Result<Arc<Vec<u8>>, EditError> {
|
||||
let docs = sources.iter().map(|(n, b)| open_source(n, b)).collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
+128
-1
@@ -179,6 +179,11 @@ pub enum Layout {
|
||||
overlap: f64,
|
||||
cut_marks: bool,
|
||||
},
|
||||
/// Shipping label: each page cut to its [`Settings::crops`] box, turned so its text reads
|
||||
/// upright on the sheet and fitted with [`LABEL_MARGIN`]; `flip` turns it a further 180°.
|
||||
Label {
|
||||
flip: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
@@ -228,6 +233,10 @@ pub struct Settings {
|
||||
pub orientation: Orientation,
|
||||
pub layout: Layout,
|
||||
pub content: Content,
|
||||
/// For [`Layout::Label`], per source page: the area to keep (display space) and whether its
|
||||
/// text runs bottom-to-top (so a quarter turn clockwise makes it upright). `None` or a
|
||||
/// missing entry keeps the whole page.
|
||||
pub crops: Vec<Option<Crop>>,
|
||||
/// Print as image (Acrobat's Advanced Print Setup): every page rendered to a picture at this
|
||||
/// resolution before it is placed on the paper. The engine does the rendering; `impose` is
|
||||
/// then given the image-only document.
|
||||
@@ -249,6 +258,12 @@ impl Default for Raster {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Crop {
|
||||
pub rect: [f64; 4],
|
||||
pub text_up: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Settings {
|
||||
@@ -258,12 +273,13 @@ impl Default for Settings {
|
||||
layout: Layout::default(),
|
||||
content: Content::default(),
|
||||
raster: None,
|
||||
crops: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Common paper sizes (points, portrait).
|
||||
pub const PAPERS: [(&str, (f64, f64)); 19] = [
|
||||
pub const PAPERS: [(&str, (f64, f64)); 20] = [
|
||||
("US Letter", (612.0, 792.0)),
|
||||
("US Legal", (612.0, 1008.0)),
|
||||
("Tabloid", (792.0, 1224.0)),
|
||||
@@ -283,8 +299,91 @@ pub const PAPERS: [(&str, (f64, f64)); 19] = [
|
||||
("A2", (1190.55, 1683.78)),
|
||||
("A1", (1683.78, 2383.94)),
|
||||
("A0", (2383.94, 3370.39)),
|
||||
("4 x 6 in label (101.6 x 152.4 mm)", (288.0, 432.0)),
|
||||
];
|
||||
|
||||
/// `PAPERS` index of the 4 × 6 in shipping label.
|
||||
pub const LABEL_PAPER: usize = 19;
|
||||
|
||||
/// The CUPS media name for `PAPERS[index]`, so the spooler feeds the sheet the PDF was laid out
|
||||
/// on rather than the queue's default (PPD keywords; `w288h432` is the usual 4×6 label).
|
||||
pub fn cups_media(index: usize) -> Option<&'static str> {
|
||||
match PAPERS.get(index)?.0 {
|
||||
"US Letter" => Some("Letter"),
|
||||
"US Legal" => Some("Legal"),
|
||||
"Tabloid" => Some("Tabloid"),
|
||||
"A3" => Some("A3"),
|
||||
"A4" => Some("A4"),
|
||||
"A5" => Some("A5"),
|
||||
_ if index == LABEL_PAPER => Some("w288h432"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether most of page `index`'s text runs bottom-to-top as the page displays (a label laid
|
||||
/// sideways on a letter or A4 sheet), counting each text-showing operator once; form XObjects
|
||||
/// are followed a few levels deep.
|
||||
pub fn text_reads_up(doc: &Document, index: usize) -> bool {
|
||||
let Some(page) = pdfcraft_model::pages(doc).into_iter().nth(index) else { return false };
|
||||
let to_display = Matrix(page.view_matrix(doc)).invert().unwrap_or_default();
|
||||
let res = page.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_default();
|
||||
let mut score = 0i64;
|
||||
text_direction(doc, &decoded_contents(doc, &page.dict), &res, to_display, 0, &mut score);
|
||||
score > 0
|
||||
}
|
||||
|
||||
fn text_direction(doc: &Document, data: &[u8], res: &Dict, base: Matrix, depth: usize, score: &mut i64) {
|
||||
let (mut ctm, mut tm, mut stack) = (base, Matrix::IDENTITY, Vec::new());
|
||||
for op in pdfcraft_content::parse(data).ops {
|
||||
match op.op.as_slice() {
|
||||
// Bounded like the renderer's graphics-state stack: deeper saves are ignored.
|
||||
b"q" if stack.len() < 256 => stack.push(ctm),
|
||||
b"Q" => ctm = stack.pop().unwrap_or(base),
|
||||
b"cm" => ctm = Matrix::from_operands(&op.operands).unwrap_or_default().then(&ctm),
|
||||
b"BT" => tm = Matrix::IDENTITY,
|
||||
b"Tm" => tm = Matrix::from_operands(&op.operands).unwrap_or_default(),
|
||||
b"Tj" | b"TJ" | b"'" | b"\"" => {
|
||||
let [a, b, ..] = tm.then(&ctm).0;
|
||||
*score += i64::from(b > a.abs()) - i64::from(-b > a.abs());
|
||||
}
|
||||
b"Do" if depth < 4 => {
|
||||
let Some(name) = op.name(0) else { continue };
|
||||
let Some(xobj) = res.get(b"XObject").and_then(|x| doc.resolve(x).as_dict().and_then(|x| x.get(name).cloned())) else { continue };
|
||||
let Object::Stream(form) = &*doc.resolve(&xobj) else { continue };
|
||||
if form.dict.get(b"Subtype").and_then(|s| doc.resolve(s).as_name().map(<[u8]>::to_vec)).as_deref() != Some(b"Form".as_slice()) {
|
||||
continue;
|
||||
}
|
||||
let m = form.dict.get(b"Matrix").and_then(|m| doc.resolve(m).as_array().and_then(|a| Matrix::from_operands(a))).unwrap_or_default();
|
||||
let inner = form.dict.get(b"Resources").and_then(|r| doc.resolve(r).as_dict().cloned()).unwrap_or_else(|| res.clone());
|
||||
if let Ok(body) = form.decoded() {
|
||||
text_direction(doc, &body, &inner, m.then(&ctm), depth + 1, score);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Margin kept around a label: thermal heads miss the very edge, and many 4×6 drivers feed
|
||||
/// 100 × 150 mm (283 × 425 pt), a little short of the 288 × 432 pt sheet.
|
||||
pub const LABEL_MARGIN: f64 = 8.0;
|
||||
|
||||
/// Place `crop` (display space) of `page` centred in `cell`, scaled by `s` and turned by
|
||||
/// `quarter` quarter turns counter-clockwise (0–3).
|
||||
fn place_crop(page: usize, crop: [f64; 4], cell: [f64; 4], s: f64, quarter: u8) -> Placement {
|
||||
let (w, h) = (crop[2] - crop[0], crop[3] - crop[1]);
|
||||
let (pw, ph) = if quarter % 2 == 1 { (h * s, w * s) } else { (w * s, h * s) };
|
||||
let (x, y) = (cell[0] + (cell[2] - cell[0] - pw) / 2.0, cell[1] + (cell[3] - cell[1] - ph) / 2.0);
|
||||
let [a, b, c, d, e, f] = match quarter % 4 {
|
||||
0 => [s, 0.0, 0.0, s, x, y],
|
||||
1 => [0.0, s, -s, 0.0, x + pw, y],
|
||||
2 => [-s, 0.0, 0.0, -s, x + pw, y + ph],
|
||||
_ => [0.0, -s, s, 0.0, x, y + ph],
|
||||
};
|
||||
// Move the crop's corner to the origin first.
|
||||
Placement { page, matrix: Matrix([a, b, c, d, e - a * crop[0] - c * crop[1], f - b * crop[0] - d * crop[1]]), clip: crop }
|
||||
}
|
||||
|
||||
/// Match a displayed PDF page to a standard sheet, independent of orientation.
|
||||
/// Half a point allows rounded metric PDF dimensions without confusing distinct sizes.
|
||||
pub fn matching_paper(size: (f64, f64)) -> Option<usize> {
|
||||
@@ -373,6 +472,34 @@ pub fn layout(sizes: &[(f64, f64)], settings: &Settings) -> Result<Vec<Sheet>, P
|
||||
sheets.push(Sheet { size, placed: vec![place_in(p, d, [0.0, 0.0, size.0, size.1], s, false)], ..Sheet::default() });
|
||||
}
|
||||
}
|
||||
Layout::Label { flip } => {
|
||||
let size = oriented(paper, settings.orientation, false);
|
||||
let cell = [LABEL_MARGIN, LABEL_MARGIN, size.0 - LABEL_MARGIN, size.1 - LABEL_MARGIN];
|
||||
for &p in &pages {
|
||||
let d = sizes[p];
|
||||
let crop = settings.crops.get(p).copied().flatten();
|
||||
let rect = crop
|
||||
.map(|c| [c.rect[0].max(0.0), c.rect[1].max(0.0), c.rect[2].min(d.0), c.rect[3].min(d.1)])
|
||||
.filter(|r| r[2] - r[0] > 1.0 && r[3] - r[1] > 1.0)
|
||||
.unwrap_or([0.0, 0.0, d.0, d.1]);
|
||||
let (w, h) = (rect[2] - rect[0], rect[3] - rect[1]);
|
||||
let turn = (w > h) != (cell[2] - cell[0] > cell[3] - cell[1]);
|
||||
let mut quarter = match (turn, crop.is_some_and(|c| c.text_up)) {
|
||||
(false, _) => 0,
|
||||
(true, true) => 3,
|
||||
(true, false) => 1,
|
||||
};
|
||||
if flip {
|
||||
quarter = (quarter + 2) % 4;
|
||||
}
|
||||
let s = if turn {
|
||||
fit_scale((h, w), (cell[2] - cell[0], cell[3] - cell[1]))
|
||||
} else {
|
||||
fit_scale((w, h), (cell[2] - cell[0], cell[3] - cell[1]))
|
||||
};
|
||||
sheets.push(Sheet { size, placed: vec![place_crop(p, rect, cell, s, quarter)], ..Sheet::default() });
|
||||
}
|
||||
}
|
||||
Layout::Multiple { cols, rows, per_sheet, order, border, auto_rotate } => {
|
||||
let cells = cols.checked_mul(rows).filter(|&n| n > 0 && n <= 256);
|
||||
let Some(cells) = cells else {
|
||||
|
||||
@@ -644,6 +644,57 @@ fn paper_matching_accepts_rounded_metric_sizes_but_not_custom_or_invalid_sizes()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn label_is_cut_turned_upright_and_fitted_without_distortion() {
|
||||
let label = LABEL_PAPER;
|
||||
assert_eq!(PAPERS[label].0, "4 x 6 in label (101.6 x 152.4 mm)");
|
||||
assert_eq!(PAPERS[label].1, (288.0, 432.0));
|
||||
assert_eq!(cups_media(label), Some("w288h432"));
|
||||
// A sideways label in the lower half of an A4 sheet, text running bottom-to-top.
|
||||
let rect = [40.0, 30.0, 560.0, 420.0];
|
||||
for (text_up, flip, want_text_dir) in [(true, false, (1.0, 0.0)), (false, false, (1.0, 0.0)), (true, true, (-1.0, 0.0))] {
|
||||
// Text direction on the page: up when text_up, down otherwise.
|
||||
let page_dir = if text_up { (0.0, 1.0) } else { (0.0, -1.0) };
|
||||
let s = Settings {
|
||||
pages: vec![0],
|
||||
paper: PAPERS[label].1,
|
||||
layout: Layout::Label { flip },
|
||||
crops: vec![Some(Crop { rect, text_up })],
|
||||
..Settings::default()
|
||||
};
|
||||
let sheets = layout(&[(595.0, 842.0)], &s).unwrap();
|
||||
assert_eq!(sheets.len(), 1);
|
||||
assert_eq!(sheets[0].size, (288.0, 432.0));
|
||||
let pl = sheets[0].placed[0];
|
||||
assert_eq!(pl.clip, rect);
|
||||
let [a, b, c, d, _, _] = pl.matrix.0;
|
||||
// A pure quarter turn with one scale: no stretching.
|
||||
assert!((a.hypot(b) - c.hypot(d)).abs() < 1e-9 && (a * c + b * d).abs() < 1e-9);
|
||||
let dir = (a * page_dir.0 + c * page_dir.1, b * page_dir.0 + d * page_dir.1);
|
||||
let n = dir.0.hypot(dir.1);
|
||||
assert!((dir.0 / n - want_text_dir.0).abs() < 1e-9 && (dir.1 / n - want_text_dir.1).abs() < 1e-9, "text_up={text_up} flip={flip}");
|
||||
let corners = [(rect[0], rect[1]), (rect[2], rect[1]), (rect[2], rect[3]), (rect[0], rect[3])].map(|(x, y)| pl.matrix.apply(x, y));
|
||||
let (x0, x1) = corners.iter().fold((f64::MAX, f64::MIN), |m, p| (m.0.min(p.0), m.1.max(p.0)));
|
||||
let (y0, y1) = corners.iter().fold((f64::MAX, f64::MIN), |m, p| (m.0.min(p.1), m.1.max(p.1)));
|
||||
assert!(x0 >= LABEL_MARGIN - 1e-6 && x1 <= 288.0 - LABEL_MARGIN + 1e-6 && y0 >= LABEL_MARGIN - 1e-6 && y1 <= 432.0 - LABEL_MARGIN + 1e-6);
|
||||
// Fills the label's width (the crop's height, 390 pt, becomes it).
|
||||
assert!((x1 - x0 - (288.0 - 2.0 * LABEL_MARGIN)).abs() < 1e-6);
|
||||
}
|
||||
// No crop: the whole page, upright.
|
||||
let whole =
|
||||
layout(&[(200.0, 300.0)], &Settings { pages: vec![0], paper: (288.0, 432.0), layout: Layout::Label { flip: false }, ..Settings::default() })
|
||||
.unwrap();
|
||||
assert_eq!(whole[0].placed[0].clip, [0.0, 0.0, 200.0, 300.0]);
|
||||
assert_eq!(whole[0].placed[0].matrix.0[1], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_direction_is_read_from_text_and_form_matrices() {
|
||||
let doc = fixture(1);
|
||||
assert!(!text_reads_up(&doc, 0), "upright text");
|
||||
assert!(!text_reads_up(&doc, 9), "no such page");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imposed_pages_preserve_large_user_unit_matrices() {
|
||||
let (unit, mode) = (75000.0, SizeMode::Fit);
|
||||
|
||||
@@ -836,6 +836,9 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
close = true;
|
||||
return;
|
||||
};
|
||||
if app.print_draft.handling == crate::print_ui::Handling::Label {
|
||||
app.read_label_crops(id);
|
||||
}
|
||||
let Some(doc) = app.session.get(id) else { return };
|
||||
let sizes: Vec<(f64, f64)> = doc.info.pages.iter().map(|p| (p.width as f64, p.height as f64)).collect();
|
||||
let labels: Vec<String> = doc.info.pages.iter().map(|p| p.label.clone()).collect();
|
||||
|
||||
@@ -2395,6 +2395,9 @@ animation effects Off معطّلة
|
||||
This document is trying to open a local file: يحاول هذا المستند فتح ملف محلي:
|
||||
Move tag earlier نقل العلامة للأعلى
|
||||
Move tag later نقل العلامة للأسفل
|
||||
Turn upside down (180°) اقلب رأسًا على عقب (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) ملصق 4 × 6 بوصة (101.6 × 152.4 مم)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. يقص كل صفحة إلى المنطقة المطبوعة، ويديرها لتصبح مستقيمة ويلائمها مع الورقة دون تمديد. ملصق واحد لكل ورقة.
|
||||
Find untagged content العثور على محتوى غير مُعلَّم
|
||||
No untagged content was found. لم يتم العثور على محتوى غير معلَّم.
|
||||
Found {n} untagged content problems. تم العثور على {n} مشكلة في محتوى غير معلَّم.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2337,6 +2337,9 @@ animation effects Off Изключени
|
||||
This document is trying to open a local file: Този документ се опитва да отвори локален файл:
|
||||
Move tag earlier Преместване на етикета по-рано
|
||||
Move tag later Преместване на етикета по-късно
|
||||
Turn upside down (180°) Обърни наопаки (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Етикет 4 × 6 in (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Изрязва всяка страница до отпечатаната ѝ област, завърта я изправено и я побира в листа без разтягане. Един етикет на лист.
|
||||
Find untagged content Намиране на немаркирано съдържание
|
||||
No untagged content was found. Не е намерено немаркирано съдържание.
|
||||
Found {n} untagged content problems. Открити са {n} проблема с немаркирано съдържание.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 16.
|
@@ -2448,6 +2448,9 @@ animation effects Off Vypnuto
|
||||
This document is trying to open a local file: Tento dokument se pokouší otevřít místní soubor:
|
||||
Move tag earlier Přesunout značku dříve
|
||||
Move tag later Přesunout značku později
|
||||
Turn upside down (180°) Otočit vzhůru nohama (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Štítek 4 × 6 in (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Ořízne každou stránku na potištěnou oblast, otočí ji svisle a přizpůsobí listu bez deformace. Jeden štítek na list.
|
||||
Find untagged content Najít neoznačený obsah
|
||||
No untagged content was found. Nebyl nalezen žádný neoznačený obsah.
|
||||
Found {n} untagged content problems. Nalezeno problémů s neoznačeným obsahem: {n}.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2375,6 +2375,9 @@ animation effects Off Aus
|
||||
This document is trying to open a local file: Dieses Dokument versucht, eine lokale Datei zu öffnen:
|
||||
Move tag earlier Tag früher verschieben
|
||||
Move tag later Tag später verschieben
|
||||
Turn upside down (180°) Auf den Kopf drehen (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Etikett 4 × 6 Zoll (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Schneidet jede Seite auf ihren bedruckten Bereich zu, richtet sie auf und passt sie ohne Verzerrung an das Blatt an. Ein Etikett pro Blatt.
|
||||
Find untagged content Nicht markierten Inhalt suchen
|
||||
No untagged content was found. Es wurde kein unmarkierter Inhalt gefunden.
|
||||
Found {n} untagged content problems. {n} Probleme mit unmarkiertem Inhalt gefunden.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 51.
|
@@ -2384,6 +2384,9 @@ animation effects Off Ανενεργά
|
||||
This document is trying to open a local file: Αυτό το έγγραφο προσπαθεί να ανοίξει ένα τοπικό αρχείο:
|
||||
Move tag earlier Μετακίνηση ετικέτας νωρίτερα
|
||||
Move tag later Μετακίνηση ετικέτας αργότερα
|
||||
Turn upside down (180°) Αναποδογύρισμα (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Ετικέτα 4 × 6 in (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Περικόπτει κάθε σελίδα στην τυπωμένη περιοχή της, την ισιώνει και την προσαρμόζει στο φύλλο χωρίς παραμόρφωση. Μία ετικέτα ανά φύλλο.
|
||||
Find untagged content Εύρεση μη επισημασμένου περιεχομένου
|
||||
No untagged content was found. Δεν βρέθηκε μη επισημασμένο περιεχόμενο.
|
||||
Found {n} untagged content problems. Βρέθηκαν {n} προβλήματα μη επισημασμένου περιεχομένου.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 476 and column 9.
|
@@ -2514,6 +2514,9 @@ animation effects Off Desactivados
|
||||
This document is trying to open a local file: Este documento intenta abrir un archivo local:
|
||||
Move tag earlier Mover etiqueta antes
|
||||
Move tag later Mover etiqueta después
|
||||
Turn upside down (180°) Girar boca abajo (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Etiqueta 4 × 6 in (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Recorta cada página a su área impresa, la endereza y la ajusta a la hoja sin deformarla. Una etiqueta por hoja.
|
||||
Find untagged content Buscar contenido sin etiquetar
|
||||
No untagged content was found. No se encontró contenido sin etiquetar.
|
||||
Found {n} untagged content problems. Se encontraron {n} problemas de contenido sin etiquetar.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 60.
|
@@ -2374,6 +2374,9 @@ animation effects Off Désactivés
|
||||
This document is trying to open a local file: Ce document tente d’ouvrir un fichier local :
|
||||
Move tag earlier Déplacer la balise plus tôt
|
||||
Move tag later Déplacer la balise plus tard
|
||||
Turn upside down (180°) Retourner (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Étiquette 4 × 6 po (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Rogne chaque page à sa zone imprimée, la redresse et l’ajuste à la feuille sans la déformer. Une étiquette par feuille.
|
||||
Find untagged content Rechercher le contenu non balisé
|
||||
No untagged content was found. Aucun contenu non balisé n’a été trouvé.
|
||||
Found {n} untagged content problems. {n} problèmes de contenu non balisé trouvés.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 39.
|
@@ -2373,6 +2373,9 @@ animation effects Off Ki
|
||||
This document is trying to open a local file: Ez a dokumentum egy helyi fájlt próbál megnyitni:
|
||||
Move tag earlier Címke korábbra helyezése
|
||||
Move tag later Címke későbbre helyezése
|
||||
Turn upside down (180°) Fejjel lefelé fordítás (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) 4 × 6 hüvelykes címke (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Minden oldalt a nyomtatott területére vág, függőlegesre fordítja, és torzítás nélkül a laphoz igazítja. Laponként egy címke.
|
||||
Find untagged content Címkézetlen tartalom keresése
|
||||
No untagged content was found. Nem található címkézetlen tartalom.
|
||||
Found {n} untagged content problems. {n} címkézetlen tartalmi probléma található.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 44.
|
@@ -2383,6 +2383,9 @@ animation effects Off Disattivati
|
||||
This document is trying to open a local file: Questo documento sta tentando di aprire un file locale:
|
||||
Move tag earlier Sposta tag prima
|
||||
Move tag later Sposta tag dopo
|
||||
Turn upside down (180°) Capovolgi (ruota di 180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Etichetta 4×6 - 101,6 × 152,4 mm
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Ritaglia l'area stampata di ogni pagina, la raddrizza e la adatta al foglio senza deformarla. Un'etichetta per foglio.
|
||||
Find untagged content Trova contenuto senza tag
|
||||
No untagged content was found. Non è stato trovato contenuto senza tag.
|
||||
Found {n} untagged content problems. Trovati {n} problemi di contenuto senza tag.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2374,6 +2374,9 @@ animation effects Off オフ
|
||||
This document is trying to open a local file: この文書はローカルファイルを開こうとしています:
|
||||
Move tag earlier タグを前へ移動
|
||||
Move tag later タグを後へ移動
|
||||
Turn upside down (180°) 上下反転 (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) 4 × 6 インチ ラベル (101.6 × 152.4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. 各ページを印刷領域で切り抜き、正しい向きに回転して、伸縮させずに用紙に合わせます。1 枚に 1 ラベル。
|
||||
Find untagged content タグなしのコンテンツを検索
|
||||
No untagged content was found. タグなしのコンテンツは見つかりませんでした。
|
||||
Found {n} untagged content problems. タグなしのコンテンツの問題が {n} 件見つかりました。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2372,6 +2372,9 @@ animation effects Off Өшірулі
|
||||
This document is trying to open a local file: Бұл құжат жергілікті файлды ашуға әрекеттеніп жатыр:
|
||||
Move tag earlier Тегті ертерек жылжыту
|
||||
Move tag later Тегті кейінірек жылжыту
|
||||
Turn upside down (180°) Төңкеру (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) 4 × 6 дюйм жапсырма (101,6 × 152,4 мм)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Әр бетті басылған аймағына дейін қиып, тік бұрып, созбай параққа сыйғызады. Әр параққа бір жапсырма.
|
||||
Find untagged content Тегтелмеген мазмұнды табу
|
||||
No untagged content was found. Тегтелмеген мазмұн табылмады.
|
||||
Found {n} untagged content problems. Тегтелмеген мазмұнға қатысты {n} мәселе табылды.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 825 and column 9.
|
@@ -2618,6 +2618,9 @@ key Space Tarpas
|
||||
This document is trying to open a local file: Šis dokumentas bando atverti vietinį failą:
|
||||
Move tag earlier Perkelti žymą anksčiau
|
||||
Move tag later Perkelti žymą vėliau
|
||||
Turn upside down (180°) Apversti aukštyn kojomis (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) 4 × 6 col. etiketė (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Apkerpa kiekvieną puslapį iki spausdinamos srities, pasuka tiesiai ir pritaiko lapui neiškraipydama. Viena etiketė lape.
|
||||
Find untagged content Rasti nepažymėtą turinį
|
||||
No untagged content was found. Nepažymėto turinio nerasta.
|
||||
Found {n} untagged content problems. Rasta nepažymėto turinio problemų: {n}.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 402 and column 9.
|
@@ -2354,6 +2354,9 @@ animation effects Off Wyłączone
|
||||
This document is trying to open a local file: Ten dokument próbuje otworzyć plik lokalny:
|
||||
Move tag earlier Przenieś znacznik wcześniej
|
||||
Move tag later Przenieś znacznik później
|
||||
Turn upside down (180°) Obróć do góry nogami (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Etykieta 4 × 6 cali (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Przycina każdą stronę do zadrukowanego obszaru, obraca ją pionowo i dopasowuje do arkusza bez rozciągania. Jedna etykieta na arkusz.
|
||||
Find untagged content Znajdź nieoznaczoną zawartość
|
||||
No untagged content was found. Nie znaleziono nieoznaczonej zawartości.
|
||||
Found {n} untagged content problems. Znaleziono problemów z nieoznaczoną zawartością: {n}.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2388,6 +2388,9 @@ animation effects Off Desativados
|
||||
This document is trying to open a local file: Este documento está tentando abrir um arquivo local:
|
||||
Move tag earlier Mover tag para antes
|
||||
Move tag later Mover tag para depois
|
||||
Turn upside down (180°) Virar de cabeça para baixo (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Etiqueta 4 × 6 pol (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Recorta cada página na área impressa, deixa-a na vertical e ajusta-a à folha sem distorcer. Uma etiqueta por folha.
|
||||
Find untagged content Encontrar conteúdo sem marcação
|
||||
No untagged content was found. Nenhum conteúdo sem marcação foi encontrado.
|
||||
Found {n} untagged content problems. Foram encontrados {n} problemas de conteúdo sem marcação.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -2287,6 +2287,9 @@ animation effects Off Выкл
|
||||
This document is trying to open a local file: Этот документ пытается открыть локальный файл:
|
||||
Move tag earlier Переместить тег раньше
|
||||
Move tag later Переместить тег позже
|
||||
Turn upside down (180°) Перевернуть (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Этикетка 4 × 6 дюйма (101,6 × 152,4 мм)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Обрезает каждую страницу по напечатанной области, разворачивает её прямо и вписывает в лист без искажений. Одна этикетка на лист.
|
||||
Find untagged content Найти нетегированный контент
|
||||
No untagged content was found. Нетегированный контент не найден.
|
||||
Found {n} untagged content problems. Найдено проблем с нетегированным контентом: {n}.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -2379,6 +2379,9 @@ animation effects Off Izklopljeni
|
||||
This document is trying to open a local file: Ta dokument poskuša odpreti lokalno datoteko:
|
||||
Move tag earlier Premakni oznako prej
|
||||
Move tag later Premakni oznako pozneje
|
||||
Turn upside down (180°) Obrni na glavo (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Nalepka 4 × 6 in (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Vsako stran obreže na natisnjeno območje, jo poravna pokonci in prilagodi listu brez raztegovanja. Ena nalepka na list.
|
||||
Find untagged content Poišči neoznačeno vsebino
|
||||
No untagged content was found. Neoznačene vsebine ni bilo mogoče najti.
|
||||
Found {n} untagged content problems. Najdenih je {n} težav z neoznačeno vsebino.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -2287,6 +2287,9 @@ animation effects Off ఆఫ్
|
||||
This document is trying to open a local file: ఈ పత్రం ఒక స్థానిక ఫైల్ను తెరవడానికి ప్రయత్నిస్తోంది:
|
||||
Move tag earlier ట్యాగ్ను ముందుకు తరలించు
|
||||
Move tag later ట్యాగ్ను వెనుకకు తరలించు
|
||||
Turn upside down (180°) తలకిందులుగా తిప్పు (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) 4 × 6 అం. లేబుల్ (101.6 × 152.4 మి.మీ.)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. ప్రతి పేజీని ముద్రిత ప్రాంతానికి కత్తిరించి, నిటారుగా తిప్పి, సాగదీయకుండా షీట్కు సరిపెడుతుంది. ఒక్కో షీట్కు ఒక లేబుల్.
|
||||
Find untagged content ట్యాగ్ చేయని కంటెంట్ను కనుగొను
|
||||
No untagged content was found. ట్యాగ్ చేయని కంటెంట్ కనుగొనబడలేదు.
|
||||
Found {n} untagged content problems. ట్యాగ్ చేయని కంటెంట్లో {n} సమస్యలు కనుగొనబడ్డాయి.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 13 and column 16.
|
@@ -2370,6 +2370,9 @@ animation effects Off Kapalı
|
||||
This document is trying to open a local file: Bu belge yerel bir dosya açmaya çalışıyor:
|
||||
Move tag earlier Etiketi daha erken taşı
|
||||
Move tag later Etiketi daha geç taşı
|
||||
Turn upside down (180°) Baş aşağı çevir (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) 4 × 6 inç etiket (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Her sayfayı basılı alanına göre kırpar, dik çevirir ve germeden sayfaya sığdırır. Sayfa başına bir etiket.
|
||||
Find untagged content Etiketlenmemiş içeriği bul
|
||||
No untagged content was found. Etiketlenmemiş içerik bulunamadı.
|
||||
Found {n} untagged content problems. {n} etiketlenmemiş içerik sorunu bulundu.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 824 and column 9.
|
@@ -2396,6 +2396,9 @@ animation effects Off Вимкнено
|
||||
This document is trying to open a local file: Цей документ намагається відкрити локальний файл:
|
||||
Move tag earlier Перемістити тег раніше
|
||||
Move tag later Перемістити тег пізніше
|
||||
Turn upside down (180°) Перевернути (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Етикетка 4 × 6 дюйма (101,6 × 152,4 мм)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Обрізає кожну сторінку за надрукованою областю, розвертає її прямо та вписує в аркуш без спотворень. Одна етикетка на аркуш.
|
||||
Find untagged content Знайти нетегований вміст
|
||||
No untagged content was found. Нетегованого вмісту не знайдено.
|
||||
Found {n} untagged content problems. Знайдено проблем із нетегованим вмістом: {n}.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2603,6 +2603,9 @@ key Space Space
|
||||
Restore Khôi phục
|
||||
Use system title bar Dùng thanh tiêu đề hệ thống
|
||||
Show a basic title bar for the window buttons instead of the tab strip. GNOME doesn't draw title bars for apps, so this one doesn't follow its theme. Hiện thanh tiêu đề đơn giản cho các nút cửa sổ thay vì dải tab. GNOME không vẽ thanh tiêu đề cho ứng dụng, nên thanh này không theo chủ đề của nó.
|
||||
Turn upside down (180°) Lật ngược (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) Nhãn 4 × 6 in (101,6 × 152,4 mm)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. Cắt mỗi trang theo vùng được in, xoay thẳng và vừa với tờ giấy mà không kéo giãn. Mỗi tờ một nhãn.
|
||||
Find untagged content Tìm nội dung chưa được gắn thẻ
|
||||
No untagged content was found. Không tìm thấy nội dung nào chưa được gắn thẻ.
|
||||
Found {n} untagged content problems. Đã tìm thấy {n} vấn đề về nội dung chưa được gắn thẻ.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 488 and column 12.
|
@@ -2338,6 +2338,9 @@ animation effects Off 关
|
||||
This document is trying to open a local file: 此文档正尝试打开本地文件:
|
||||
Move tag earlier 将标签前移
|
||||
Move tag later 将标签后移
|
||||
Turn upside down (180°) 上下颠倒 (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) 4 × 6 英寸标签 (101.6 × 152.4 毫米)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. 将每页裁切到其打印区域,转正后不拉伸地适合纸张。每张纸一个标签。
|
||||
Find untagged content 查找未标记的内容
|
||||
No untagged content was found. 未找到未标记的内容。
|
||||
Found {n} untagged content problems. 发现 {n} 个未标记内容问题。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -2250,6 +2250,9 @@ animation effects Off 關
|
||||
This document is trying to open a local file: 此文件正嘗試開啟本機檔案:
|
||||
Move tag earlier 將標記前移
|
||||
Move tag later 將標記後移
|
||||
Turn upside down (180°) 上下顛倒 (180°)
|
||||
4 x 6 in label (101.6 x 152.4 mm) 4 × 6 英吋標籤 (101.6 × 152.4 公釐)
|
||||
Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet. 將每頁裁切至其列印區域,轉正後不拉伸地符合紙張。每張紙一個標籤。
|
||||
Find untagged content 尋找未標記的內容
|
||||
No untagged content was found. 找不到未標記的內容。
|
||||
Found {n} untagged content problems. 發現 {n} 個未標記內容問題。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -14,6 +14,9 @@ const PREVIEW_W: f32 = 320.0;
|
||||
const PREVIEW_H: f32 = 380.0;
|
||||
const PREVIEW_INSET: f32 = 12.0;
|
||||
pub(crate) const PREVIEW_BOX: (f32, f32) = (PREVIEW_W - 2.0 * PREVIEW_INSET, PREVIEW_H - 2.0 * PREVIEW_INSET);
|
||||
/// Label printing reads the printed area of at most this many pages (each is a quick render on
|
||||
/// the UI thread); further pages print whole.
|
||||
pub(crate) const LABEL_CROP_PAGES: usize = 50;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Which {
|
||||
@@ -30,6 +33,8 @@ pub enum Handling {
|
||||
Poster,
|
||||
Multiple,
|
||||
Booklet,
|
||||
/// Shipping label: cut to the printed area, turned upright, one per sheet.
|
||||
Label,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -81,6 +86,13 @@ pub struct PrintDraft {
|
||||
pub show_driver_options: bool,
|
||||
/// Why the printer's preferences window didn't open (Windows).
|
||||
pub driver_error: Option<String>,
|
||||
/// Label: turn a further 180°.
|
||||
pub label_flip: bool,
|
||||
/// Label: each page's printed area and text direction, read when Label is chosen
|
||||
/// ([`PdfCraftApp::read_label_crops`]); empty until then.
|
||||
pub crops: Vec<Option<print::Crop>>,
|
||||
/// The pages `crops` were read for, so a changed page selection reads them again.
|
||||
pub crop_pages: Option<Vec<usize>>,
|
||||
}
|
||||
|
||||
impl Default for PrintDraft {
|
||||
@@ -123,6 +135,9 @@ impl Default for PrintDraft {
|
||||
driver_choices: Default::default(),
|
||||
show_driver_options: false,
|
||||
driver_error: None,
|
||||
label_flip: false,
|
||||
crops: Vec::new(),
|
||||
crop_pages: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,6 +175,7 @@ impl PrintDraft {
|
||||
},
|
||||
Handling::Booklet => Layout::Booklet { subset: self.booklet_subset, binding: self.binding },
|
||||
Handling::Poster => Layout::Poster { scale: self.poster_scale, overlap: self.overlap, cut_marks: self.cut_marks },
|
||||
Handling::Label => Layout::Label { flip: self.label_flip },
|
||||
};
|
||||
Ok(print::Settings {
|
||||
pages,
|
||||
@@ -168,6 +184,7 @@ impl PrintDraft {
|
||||
layout,
|
||||
content: self.content,
|
||||
raster: self.as_image.then_some(print::Raster { dpi: self.dpi, grayscale: self.grayscale }),
|
||||
crops: self.crops.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -184,7 +201,11 @@ impl PrintDraft {
|
||||
options: match (&self.printer, &self.driver_options) {
|
||||
(Some(p), Some((read_for, _))) if p == read_for => self.driver_choices.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
|
||||
_ => Vec::new(),
|
||||
},
|
||||
}
|
||||
.into_iter()
|
||||
// The sheet the PDF was laid out on, not the queue's default paper.
|
||||
.chain(print::cups_media(self.paper).map(|m| ("media".to_string(), m.to_string())))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,6 +313,8 @@ impl PdfCraftApp {
|
||||
(true, other) => other,
|
||||
};
|
||||
self.print_draft = PrintDraft {
|
||||
crops: Vec::new(),
|
||||
crop_pages: None,
|
||||
printers,
|
||||
printers_loading,
|
||||
printer_picked: false,
|
||||
@@ -306,6 +329,50 @@ impl PdfCraftApp {
|
||||
self.dialog = Some(crate::Dialog::Print);
|
||||
}
|
||||
|
||||
/// Label printing: find each printed page's printed area (a quick low-resolution render, as
|
||||
/// Fit Visible does) and which way its text runs. Only the pages being printed are read, at
|
||||
/// most [`LABEL_CROP_PAGES`] of them (the rest print whole); read again when they change.
|
||||
pub(crate) fn read_label_crops(&mut self, id: pdfcraft_engine::DocId) {
|
||||
let Some(doc) = self.session.get(id) else { return };
|
||||
let labels: Vec<String> = doc.info.pages.iter().map(|p| p.label.clone()).collect();
|
||||
let mut pages = self.print_draft.settings(doc.info.pages.len(), &labels).map(|s| s.pages).unwrap_or_default();
|
||||
pages.sort_unstable();
|
||||
pages.dedup();
|
||||
pages.truncate(LABEL_CROP_PAGES);
|
||||
if self.print_draft.crop_pages.as_ref() == Some(&pages) {
|
||||
return;
|
||||
}
|
||||
let up = self.session.print_text_up(id, &pages);
|
||||
let config = pdfcraft_render::RenderConfig { password: doc.password.as_deref().map(std::sync::Arc::from), ..Default::default() };
|
||||
let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), config);
|
||||
self.print_draft.crops = doc
|
||||
.info
|
||||
.pages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(page, p)| {
|
||||
let (w, h) = (f64::from(p.width), f64::from(p.height));
|
||||
if pages.binary_search(&page).is_err() || w <= 0.0 || h <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let out = r.render(pdfcraft_render::RenderRequest {
|
||||
page,
|
||||
kind: pdfcraft_render::RequestKind::Pixels,
|
||||
tile: None,
|
||||
scale: (1000.0 / w.max(h) as f32).clamp(0.1, 4.0),
|
||||
tag: 0,
|
||||
preview: false,
|
||||
});
|
||||
let [u0, v0, u1, v1] = crate::zoom_snap::ink_bounds(out.width as usize, out.height as usize, &out.rgba)?;
|
||||
// Image rows run down from the top; display space runs up. Half a millimetre of slack.
|
||||
let pad = 1.5;
|
||||
let rect = [f64::from(u0) * w - pad, (1.0 - f64::from(v1)) * h - pad, f64::from(u1) * w + pad, (1.0 - f64::from(v0)) * h + pad];
|
||||
Some(print::Crop { rect, text_up: up.get(page).copied().unwrap_or(false) })
|
||||
})
|
||||
.collect();
|
||||
self.print_draft.crop_pages = Some(pages);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn list_printers_in_background(&mut self, source: PrinterSource) {
|
||||
// A list already being read is as fresh as a new one would be.
|
||||
@@ -838,11 +905,17 @@ pub(crate) fn body(
|
||||
ui.end_row();
|
||||
ui.label(tl!("Paper:"));
|
||||
let paper_name = PAPERS.get(d.paper).map_or("US Letter", |p| p.0);
|
||||
egui::ComboBox::from_id_salt("paper").selected_text(paper_name).width(260.0).show_ui(ui, |ui| {
|
||||
let before = d.paper;
|
||||
egui::ComboBox::from_id_salt("paper").selected_text(tl!(paper_name)).width(260.0).show_ui(ui, |ui| {
|
||||
for (i, (name, _)) in PAPERS.iter().enumerate() {
|
||||
ui.selectable_value(&mut d.paper, i, *name);
|
||||
ui.selectable_value(&mut d.paper, i, tl!(name));
|
||||
}
|
||||
});
|
||||
// Label stock gets the label layout (cut, turned upright, fitted).
|
||||
if d.paper != before && d.paper == print::LABEL_PAPER {
|
||||
d.handling = Handling::Label;
|
||||
d.sheet = 0;
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
// Windows says which drivers can't print two-sided as they're asked; they keep their
|
||||
@@ -914,10 +987,15 @@ pub(crate) fn body(
|
||||
(Handling::Poster, tl!("Poster")),
|
||||
(Handling::Multiple, tl!("Multiple")),
|
||||
(Handling::Booklet, tl!("Booklet")),
|
||||
(Handling::Label, tl!("Label")),
|
||||
] {
|
||||
if widgets::mode_tab(ui, label, d.handling == h).clicked() {
|
||||
d.handling = h;
|
||||
d.sheet = 0;
|
||||
// A label goes on label stock.
|
||||
if h == Handling::Label {
|
||||
d.paper = print::LABEL_PAPER;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -937,6 +1015,14 @@ pub(crate) fn body(
|
||||
ui.add_enabled(custom, egui::DragValue::new(&mut d.custom_scale).range(1.0..=1000.0).suffix(" %"));
|
||||
});
|
||||
}
|
||||
Handling::Label => {
|
||||
ui.add(widgets::checkbox(&mut d.label_flip, tl!("Turn upside down (180°)")));
|
||||
ui.label(
|
||||
egui::RichText::new(tl!("Cuts each page to its printed area, turns it upright and fits it to the sheet without stretching. One label per sheet."))
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
}
|
||||
Handling::Poster => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Tile scale:"));
|
||||
|
||||
Reference in New Issue
Block a user