Phase 4 Iteration 2: sửa text GIỮ NGUYÊN FONT (chuẩn Foxit) + UX edit mượt
Engine: - fontmatch.rs mới: làm sạch tên PostScript/subset, kiểm coverage cmap bằng ttf-parser, match font hệ thống CÙNG HỌ (bảng Windows/macOS, Liberation metric-compatible + fc-match trên Linux, alias base-14 Helvetica→Arial...) - edit.rs: SetText quyết định 3 tầng — (1) sửa TẠI CHỖ giữ nguyên font gốc (charset-subset / base-14+ASCII / cmap phủ đủ, gồm font nhúng + tiếng Việt); (2) thiếu glyph → thay font cùng họ đúng đậm/nghiêng; (3) mới đến fallback. Đổi cỡ chữ = scale matrix neo baseline, không đụng font; fix bug phóng đại kép khi text có matrix scale. ObjectInfo thêm font_family/bold/italic/embedded. SetText/AddText nhận font_family + bold/italic dạng Option (None = giữ nguyên) - annot.rs: fallback Linux có đủ biến thể đậm/nghiêng (DejaVu/Liberation) UI (chuẩn trải nghiệm Foxit): - Double-click sửa CẢ DÒNG (gộp run cùng baseline), commit 1 batch = 1 undo - Ô sửa WYSIWYG: đúng font/cỡ/màu/đậm-nghiêng của run gốc khi gõ - Toolbar Format: dropdown font "(giữ nguyên: X)", nút B/I theo kiểu thật, đổi cỡ/màu/kiểu gửi đúng field đó (null = giữ nguyên → không đổi font) - Kéo di chuyển/resize có phản hồi trực tiếp (live), resize neo góc trên-trái - Dọn file tạm ff_edit_* khi thoát/lưu (command edit_cleanup) Test: 14 test edit (7 mới: giữ font ASCII/base-14, giữ font NHÚNG tiếng Việt, match cùng họ, cỡ chữ neo + hồi quy matrix, bold override, gộp dòng) + 3 unit fontmatch. Toàn bộ suite engine ngoài qpdf: 50/50 xanh (chạy trên Linux). Docs: 12 (iteration 2), 13 (E17–E25), 14 MỚI — phân tích định hướng/công nghệ/ tiến độ so với tham vọng thay Foxit, gap còn lại + giải pháp; README cập nhật. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HgnbPwSoRGNH7LHsDUmRpk
This commit is contained in:
+359
-95
@@ -3,22 +3,34 @@
|
||||
//!
|
||||
//! PDFium đã expose page object cấp cao nên KHÔNG cần tự parse content stream:
|
||||
//! mỗi `PdfPageTextObject` là 1 text run sẵn để sửa (có text/font/size/matrix/
|
||||
//! màu riêng). Xem khảo sát trong kế hoạch Phase 4.
|
||||
//! màu riêng).
|
||||
//!
|
||||
//! Bài học tái dùng:
|
||||
//! - Sửa/Thêm text dùng FULL font nhúng (`find_font_bytes` +
|
||||
//! `load_true_type_from_bytes(..., true)`) để tiếng Việt hiển thị đúng dấu —
|
||||
//! `set_text()` của PDFium re-encode theo font HIỆN TẠI (thường là subset)
|
||||
//! nên glyph ngoài subset sẽ mất; vì vậy SetText = tạo lại object bằng full
|
||||
//! font, giữ nguyên matrix/cỡ/màu gốc.
|
||||
//! - PDFium cần `regenerate_content()` trước khi lưu, nếu không mất thay đổi.
|
||||
//! ## Giữ font gốc khi sửa (chuẩn Foxit — iteration 2)
|
||||
//! Sửa text KHÔNG được đổi font. Quyết định theo 3 tầng cho mỗi `SetText`:
|
||||
//! 1. **Giữ nguyên font gốc, sửa tại chỗ** (`FPDFText_SetText` re-encode theo
|
||||
//! charmap của font hiện tại) khi chắc chắn an toàn: text mới chỉ dùng ký tự
|
||||
//! đã có trong text cũ, HOẶC cmap của font (đọc qua `FPDFFont_GetFontData`)
|
||||
//! phủ đủ mọi ký tự mới. Đổi cỡ chữ tại chỗ = scale matrix (không đụng font).
|
||||
//! 2. Font gốc thiếu glyph (subset) → thay bằng font hệ thống **cùng họ, đúng
|
||||
//! đậm/nghiêng** (`fontmatch::find_family_font_bytes`) — gần như không nhìn
|
||||
//! ra khác biệt.
|
||||
//! 3. Bất đắc dĩ mới rơi về font mặc định (`annot::find_font_bytes`).
|
||||
//!
|
||||
//! Cỡ chữ trong `SetText`/`ObjectInfo` theo nghĩa **hiển thị** (đã nhân scale
|
||||
//! của matrix — đúng như UI thấy). Tạo lại object phải quy đổi ngược về cỡ
|
||||
//! "unscaled" trước khi áp matrix gốc, nếu không chữ sẽ phóng đại kép.
|
||||
//!
|
||||
//! Bài học PDFium giữ nguyên: `regenerate_content()` trước khi lưu; object gỡ
|
||||
//! bằng `remove_object_at_index` phải `std::mem::forget` (Drop gọi
|
||||
//! FPDFPageObj_Destroy → SEGFAULT).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use pdfium_render::prelude::*;
|
||||
|
||||
use crate::annot::find_font_bytes;
|
||||
use crate::fontmatch;
|
||||
use crate::text::Rect;
|
||||
use crate::EngineError;
|
||||
|
||||
@@ -67,9 +79,16 @@ pub struct ObjectInfo {
|
||||
pub rect: Rect,
|
||||
/// Chỉ với text: nội dung hiện tại.
|
||||
pub text: Option<String>,
|
||||
/// Chỉ với text: tên font.
|
||||
/// Chỉ với text: tên font gốc trong PDF (có thể kèm prefix subset).
|
||||
pub font_name: Option<String>,
|
||||
/// Chỉ với text: cỡ chữ hiệu dụng (đã tính scale của matrix).
|
||||
/// Chỉ với text: family đã làm sạch (để UI hiển thị + CSS xấp xỉ).
|
||||
pub font_family: Option<String>,
|
||||
/// Chỉ với text: font đậm / nghiêng (từ weight/italic-angle + tên font).
|
||||
pub font_bold: Option<bool>,
|
||||
pub font_italic: Option<bool>,
|
||||
/// Chỉ với text: font có nhúng trong file không.
|
||||
pub font_embedded: Option<bool>,
|
||||
/// Chỉ với text: cỡ chữ hiển thị (đã tính scale của matrix).
|
||||
pub font_size: Option<f32>,
|
||||
/// Chỉ với text: màu chữ RGBA.
|
||||
pub color: Option<[u8; 4]>,
|
||||
@@ -81,14 +100,34 @@ pub enum EditOp {
|
||||
/// Dịch (dx,dy) và/hoặc scale (sx,sy) object `index` — scale quanh góc
|
||||
/// dưới-trái của object để giữ neo góc khi resize.
|
||||
Transform { index: u16, dx: f32, dy: f32, sx: f32, sy: f32 },
|
||||
/// Sửa nội dung text object `index` (tạo lại bằng full font, giữ matrix gốc).
|
||||
SetText { index: u16, text: String, font_size: Option<f32>, color: Option<[u8; 4]>, bold: bool, italic: bool },
|
||||
/// Sửa text object `index`. Mặc định GIỮ NGUYÊN font/cỡ/màu/kiểu gốc:
|
||||
/// mọi field `None` nghĩa là "không đổi". `font_size` theo cỡ hiển thị.
|
||||
/// `font_family`/`bold`/`italic` = Some(...) chủ động đổi font (match font
|
||||
/// hệ thống cùng họ).
|
||||
SetText {
|
||||
index: u16,
|
||||
text: String,
|
||||
font_size: Option<f32>,
|
||||
color: Option<[u8; 4]>,
|
||||
font_family: Option<String>,
|
||||
bold: Option<bool>,
|
||||
italic: Option<bool>,
|
||||
},
|
||||
/// Xoá object `index`.
|
||||
Delete { index: u16 },
|
||||
/// Thay ảnh của image object `index` bằng ảnh từ `image_path`, giữ khung cũ.
|
||||
ReplaceImage { index: u16, image_path: String },
|
||||
/// Thêm text box mới tại (x,y) (gốc dưới-trái, điểm PDF).
|
||||
AddText { x: f32, y: f32, text: String, font_size: f32, color: [u8; 4], bold: bool, italic: bool },
|
||||
AddText {
|
||||
x: f32,
|
||||
y: f32,
|
||||
text: String,
|
||||
font_size: f32,
|
||||
color: [u8; 4],
|
||||
font_family: Option<String>,
|
||||
bold: bool,
|
||||
italic: bool,
|
||||
},
|
||||
/// Thêm ảnh mới từ file, khung width_pt × height_pt tại (x,y).
|
||||
AddImage { x: f32, y: f32, width_pt: f32, height_pt: f32, image_path: String },
|
||||
}
|
||||
@@ -102,6 +141,29 @@ fn quad_to_rect(q: &PdfQuadPoints) -> Rect {
|
||||
}
|
||||
}
|
||||
|
||||
/// Kiểu chữ (đậm/nghiêng) của 1 text object, tổng hợp từ weight, italic-angle
|
||||
/// và tên font. Family lấy từ `name()` (BaseFont khai báo trong PDF) — với
|
||||
/// font KHÔNG nhúng, `family()` trả tên font stub nội bộ của PDFium ("Chrom
|
||||
/// Sans OTF") chứ không phải font thật, nên chỉ dùng làm fallback.
|
||||
fn text_object_style(t: &PdfPageTextObject) -> (String, bool, bool) {
|
||||
let raw_name = t.font().name();
|
||||
let family_src = if raw_name.trim().is_empty() {
|
||||
t.font().family()
|
||||
} else {
|
||||
raw_name
|
||||
};
|
||||
let (family, name_bold, name_italic) = fontmatch::clean_font_name(&family_src);
|
||||
let weight_bold = matches!(
|
||||
t.font().weight(),
|
||||
Ok(PdfFontWeight::Weight600)
|
||||
| Ok(PdfFontWeight::Weight700Bold)
|
||||
| Ok(PdfFontWeight::Weight800)
|
||||
| Ok(PdfFontWeight::Weight900)
|
||||
) || matches!(t.font().weight(), Ok(PdfFontWeight::Custom(v)) if v >= 600);
|
||||
let angle_italic = t.font().italic_angle().map(|a| a != 0).unwrap_or(false);
|
||||
(family, weight_bold || name_bold, angle_italic || name_italic)
|
||||
}
|
||||
|
||||
/// Liệt kê các page object của 1 trang để UI vẽ overlay chỉnh sửa.
|
||||
pub fn list_objects(
|
||||
pdfium: &Pdfium,
|
||||
@@ -125,43 +187,89 @@ pub fn list_objects(
|
||||
.map(|q| quad_to_rect(&q))
|
||||
.unwrap_or(Rect { left: 0.0, bottom: 0.0, right: 0.0, top: 0.0 });
|
||||
|
||||
let (text, font_name, font_size, color) = if let Some(t) = object.as_text_object() {
|
||||
let c = t.fill_color().ok();
|
||||
(
|
||||
Some(t.text()),
|
||||
Some(t.font().name()),
|
||||
Some(t.scaled_font_size().value),
|
||||
c.map(|c| [c.red(), c.green(), c.blue(), c.alpha()]),
|
||||
)
|
||||
} else {
|
||||
(None, None, None, None)
|
||||
};
|
||||
|
||||
out.push(ObjectInfo {
|
||||
let mut info = ObjectInfo {
|
||||
index: i as u16,
|
||||
kind,
|
||||
rect,
|
||||
text,
|
||||
font_name,
|
||||
font_size,
|
||||
color,
|
||||
});
|
||||
text: None,
|
||||
font_name: None,
|
||||
font_family: None,
|
||||
font_bold: None,
|
||||
font_italic: None,
|
||||
font_embedded: None,
|
||||
font_size: None,
|
||||
color: None,
|
||||
};
|
||||
if let Some(t) = object.as_text_object() {
|
||||
let (family, bold, italic) = text_object_style(t);
|
||||
info.text = Some(t.text());
|
||||
info.font_name = Some(t.font().name());
|
||||
info.font_family = Some(family);
|
||||
info.font_bold = Some(bold);
|
||||
info.font_italic = Some(italic);
|
||||
info.font_embedded = t.font().is_embedded().ok();
|
||||
info.font_size = Some(t.scaled_font_size().value);
|
||||
info.color = t
|
||||
.fill_color()
|
||||
.ok()
|
||||
.map(|c| [c.red(), c.green(), c.blue(), c.alpha()]);
|
||||
}
|
||||
out.push(info);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Key nhận diện 1 font thay thế cần nạp: (family chuẩn hoá — rỗng = font mặc
|
||||
/// định, đậm, nghiêng).
|
||||
type FontKey = (String, bool, bool);
|
||||
|
||||
/// Cách xử lý 1 op SetText, quyết định ở pha lập kế hoạch.
|
||||
enum SetTextMode {
|
||||
/// Giữ nguyên font gốc — sửa text/cỡ/màu ngay trên object.
|
||||
InPlace,
|
||||
/// Phải thay font (glyph thiếu hoặc người dùng đổi family/kiểu) — xoá và
|
||||
/// tạo lại object với font `key`.
|
||||
Substitute(FontKey),
|
||||
}
|
||||
|
||||
/// Dữ liệu chụp lại từ 1 object trước khi xoá để tạo lại bản thay thế.
|
||||
struct Captured {
|
||||
matrix: PdfMatrix,
|
||||
unscaled_font_size: f32,
|
||||
scaled_font_size: f32,
|
||||
color: PdfColor,
|
||||
rect: Rect,
|
||||
}
|
||||
|
||||
/// Chọn font thay thế cho (family mong muốn, kiểu chữ, text cần hiển thị):
|
||||
/// font hệ thống cùng họ nếu có + đủ glyph, ngược lại font mặc định.
|
||||
fn resolve_substitute_font(
|
||||
family: &str,
|
||||
bold: bool,
|
||||
italic: bool,
|
||||
text: &str,
|
||||
) -> Result<(FontKey, Vec<u8>), EngineError> {
|
||||
if !family.trim().is_empty() {
|
||||
if let Some(bytes) = fontmatch::find_family_font_bytes(family, bold, italic) {
|
||||
if fontmatch::coverage_ok(&bytes, text) {
|
||||
return Ok(((fontmatch::normalize_key(family), bold, italic), bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
let bytes = find_font_bytes(bold, italic).ok_or_else(|| {
|
||||
EngineError::Pdfium("không tìm được font hệ thống để sửa/thêm chữ".into())
|
||||
})?;
|
||||
Ok(((String::new(), bold, italic), bytes))
|
||||
}
|
||||
|
||||
/// Áp danh sách `ops` lên trang `page_index` của `input`, ghi ra `output`.
|
||||
/// Không sửa `input`. Thứ tự xử lý giữ index gốc hợp lệ: Transform in-place →
|
||||
/// chụp dữ liệu object sắp thay/xoá → xoá theo index GIẢM DẦN → thêm bản thay
|
||||
/// thế → thêm object mới → regenerate → lưu.
|
||||
/// Không sửa `input`.
|
||||
///
|
||||
/// Trình tự: (A) lập kế hoạch font cho SetText/AddText (đọc trang lần 1) →
|
||||
/// (B) nạp các font thay thế cần thiết vào document → (C) áp thay đổi (đọc
|
||||
/// trang lần 2): Transform in-place → SetText in-place (giữ font gốc) → chụp
|
||||
/// dữ liệu object sắp thay → xoá theo index GIẢM DẦN → thêm bản thay thế →
|
||||
/// thêm object mới → regenerate → lưu.
|
||||
pub fn apply_edits(
|
||||
pdfium: &Pdfium,
|
||||
input: &Path,
|
||||
@@ -174,32 +282,109 @@ pub fn apply_edits(
|
||||
.load_pdf_from_file(input, password)
|
||||
.map_err(|e| EngineError::Pdfium(e.to_string()))?;
|
||||
|
||||
// Nạp trước token cho mọi tổ hợp (bold,italic) cần dùng (SetText/AddText) —
|
||||
// phải làm TRƯỚC khi mượn page vì cần `document.fonts_mut()`.
|
||||
let mut needed_fonts: Vec<(bool, bool)> = Vec::new();
|
||||
for op in ops {
|
||||
match op {
|
||||
EditOp::SetText { bold, italic, .. } | EditOp::AddText { bold, italic, .. } => {
|
||||
if !needed_fonts.contains(&(*bold, *italic)) {
|
||||
needed_fonts.push((*bold, *italic));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut tokens: HashMap<(bool, bool), PdfFontToken> = HashMap::new();
|
||||
for (bold, italic) in needed_fonts {
|
||||
let bytes = find_font_bytes(bold, italic)
|
||||
.ok_or_else(|| EngineError::Pdfium("không tìm được font hệ thống để sửa/thêm chữ".into()))?;
|
||||
let token = document
|
||||
.fonts_mut()
|
||||
.load_true_type_from_bytes(&bytes, true)
|
||||
.map_err(|e| EngineError::Pdfium(format!("nạp font: {e}")))?;
|
||||
tokens.insert((bold, italic), token);
|
||||
}
|
||||
|
||||
let err = |e: PdfiumError| EngineError::Pdfium(format!("sửa nội dung: {e}"));
|
||||
|
||||
// ---- (A) Lập kế hoạch font (mượn trang lần 1, chỉ đọc) ----
|
||||
let mut set_text_modes: HashMap<usize, SetTextMode> = HashMap::new();
|
||||
let mut add_text_keys: HashMap<usize, FontKey> = HashMap::new();
|
||||
let mut font_bytes_needed: HashMap<FontKey, Vec<u8>> = HashMap::new();
|
||||
{
|
||||
let page = document
|
||||
.pages()
|
||||
.get(page_index)
|
||||
.map_err(|e| EngineError::Pdfium(format!("không lấy được trang {page_index}: {e}")))?;
|
||||
let obj_count = page.objects().len();
|
||||
let valid = |i: u16| (i as usize) < obj_count;
|
||||
|
||||
for (opi, op) in ops.iter().enumerate() {
|
||||
match op {
|
||||
EditOp::SetText { index, text, font_family, bold, italic, .. } => {
|
||||
if !valid(*index) {
|
||||
continue;
|
||||
}
|
||||
let obj = page.objects().get(*index as usize).map_err(err)?;
|
||||
let t = match obj.as_text_object() {
|
||||
Some(t) => t,
|
||||
None => continue, // không phải text → bỏ qua op
|
||||
};
|
||||
let (cur_family, cur_bold, cur_italic) = text_object_style(t);
|
||||
let target_bold = bold.unwrap_or(cur_bold);
|
||||
let target_italic = italic.unwrap_or(cur_italic);
|
||||
let style_change = target_bold != cur_bold || target_italic != cur_italic;
|
||||
let family_change = font_family.as_deref().map_or(false, |f| {
|
||||
!f.trim().is_empty()
|
||||
&& fontmatch::normalize_key(f) != fontmatch::normalize_key(&cur_family)
|
||||
});
|
||||
|
||||
let mode = if !style_change && !family_change {
|
||||
let old_text = t.text();
|
||||
let old_chars: HashSet<char> = old_text.chars().collect();
|
||||
let new_covered_by_old = text
|
||||
.chars()
|
||||
.all(|c| c.is_control() || old_chars.contains(&c));
|
||||
// Font KHÔNG nhúng (base-14 Helvetica/Times...): viewer nào
|
||||
// cũng tự thay bằng font hệ thống đủ Latin → re-encode text
|
||||
// ASCII tại chỗ chắc chắn an toàn, BaseFont khai báo giữ
|
||||
// nguyên trong file (đúng hành vi Foxit với base-14).
|
||||
let non_embedded = !t.font().is_embedded().unwrap_or(true);
|
||||
let ascii_only = text
|
||||
.chars()
|
||||
.all(|c| c.is_control() || (' '..='~').contains(&c));
|
||||
if *text == old_text || new_covered_by_old {
|
||||
SetTextMode::InPlace
|
||||
} else if non_embedded && ascii_only {
|
||||
SetTextMode::InPlace
|
||||
} else if t
|
||||
.font()
|
||||
.data()
|
||||
.ok()
|
||||
.map_or(false, |bytes| fontmatch::coverage_ok(&bytes, text))
|
||||
{
|
||||
SetTextMode::InPlace
|
||||
} else {
|
||||
let (key, bytes) = resolve_substitute_font(
|
||||
&cur_family,
|
||||
target_bold,
|
||||
target_italic,
|
||||
text,
|
||||
)?;
|
||||
font_bytes_needed.entry(key.clone()).or_insert(bytes);
|
||||
SetTextMode::Substitute(key)
|
||||
}
|
||||
} else {
|
||||
let family = font_family
|
||||
.clone()
|
||||
.filter(|f| !f.trim().is_empty())
|
||||
.unwrap_or(cur_family);
|
||||
let (key, bytes) =
|
||||
resolve_substitute_font(&family, target_bold, target_italic, text)?;
|
||||
font_bytes_needed.entry(key.clone()).or_insert(bytes);
|
||||
SetTextMode::Substitute(key)
|
||||
};
|
||||
set_text_modes.insert(opi, mode);
|
||||
}
|
||||
EditOp::AddText { text, font_family, bold, italic, .. } => {
|
||||
let family = font_family.clone().unwrap_or_default();
|
||||
let (key, bytes) = resolve_substitute_font(&family, *bold, *italic, text)?;
|
||||
font_bytes_needed.entry(key.clone()).or_insert(bytes);
|
||||
add_text_keys.insert(opi, key);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- (B) Nạp các font thay thế cần dùng (cần fonts_mut → ngoài mượn trang) ----
|
||||
let mut tokens: HashMap<FontKey, PdfFontToken> = HashMap::new();
|
||||
for (key, bytes) in &font_bytes_needed {
|
||||
let token = document
|
||||
.fonts_mut()
|
||||
.load_true_type_from_bytes(bytes, true)
|
||||
.map_err(|e| EngineError::Pdfium(format!("nạp font: {e}")))?;
|
||||
tokens.insert(key.clone(), token);
|
||||
}
|
||||
|
||||
// ---- (C) Áp thay đổi (mượn trang lần 2) ----
|
||||
{
|
||||
let mut page = document
|
||||
.pages()
|
||||
@@ -231,73 +416,150 @@ pub fn apply_edits(
|
||||
}
|
||||
}
|
||||
|
||||
// (2) Chụp dữ liệu cho object sắp THAY (SetText/ReplaceImage) — sau khi
|
||||
// đã Transform để bản thay thế kế thừa cả phép biến đổi đó.
|
||||
// (2) SetText IN-PLACE — giữ nguyên font gốc (chuẩn Foxit). Đổi cỡ chữ
|
||||
// = scale matrix quanh gốc text (giữ điểm neo baseline e,f).
|
||||
for (opi, op) in ops.iter().enumerate() {
|
||||
let EditOp::SetText { index, text, font_size, color, .. } = op else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(set_text_modes.get(&opi), Some(SetTextMode::InPlace)) {
|
||||
continue;
|
||||
}
|
||||
let mut obj = page.objects().get(*index as usize).map_err(err)?;
|
||||
let (old_text, scaled) = match obj.as_text_object() {
|
||||
Some(t) => (t.text(), t.scaled_font_size().value),
|
||||
None => continue,
|
||||
};
|
||||
if *text != old_text {
|
||||
obj.as_text_object_mut()
|
||||
.ok_or_else(|| EngineError::Pdfium("object không phải text".into()))?
|
||||
.set_text(text)
|
||||
.map_err(err)?;
|
||||
}
|
||||
if let Some(target) = font_size {
|
||||
if scaled > 0.01 && (*target - scaled).abs() > 0.01 {
|
||||
let k = *target / scaled;
|
||||
let m = obj.matrix().map_err(err)?;
|
||||
// PDFium post-multiply: M' = M · [k,0,0,k, e(1−k), f(1−k)]
|
||||
// → phần tuyến tính nhân k (đổi cỡ), translation (e,f) giữ
|
||||
// nguyên (neo tại gốc baseline như Foxit).
|
||||
obj.apply_matrix(PdfMatrix::new(
|
||||
k,
|
||||
0.0,
|
||||
0.0,
|
||||
k,
|
||||
m.e() * (1.0 - k),
|
||||
m.f() * (1.0 - k),
|
||||
))
|
||||
.map_err(err)?;
|
||||
}
|
||||
}
|
||||
if let Some(c) = color {
|
||||
obj.set_fill_color(PdfColor::new(c[0], c[1], c[2], c[3])).map_err(err)?;
|
||||
}
|
||||
}
|
||||
|
||||
// (3) Chụp dữ liệu cho object sắp THAY (SetText-substitute/ReplaceImage)
|
||||
// — sau Transform để bản thay thế kế thừa cả phép biến đổi đó.
|
||||
let mut captured: HashMap<u16, Captured> = HashMap::new();
|
||||
for op in ops {
|
||||
for (opi, op) in ops.iter().enumerate() {
|
||||
let idx = match op {
|
||||
EditOp::SetText { index, .. } | EditOp::ReplaceImage { index, .. } => *index,
|
||||
EditOp::SetText { index, .. }
|
||||
if matches!(set_text_modes.get(&opi), Some(SetTextMode::Substitute(_))) =>
|
||||
{
|
||||
*index
|
||||
}
|
||||
EditOp::ReplaceImage { index, .. } => *index,
|
||||
_ => continue,
|
||||
};
|
||||
if !valid(idx) || captured.contains_key(&idx) {
|
||||
continue;
|
||||
}
|
||||
let obj = page.objects().get(idx as usize).map_err(err)?;
|
||||
let rect = obj.bounds().map(|q| quad_to_rect(&q)).unwrap_or(Rect { left: 0.0, bottom: 0.0, right: 0.0, top: 0.0 });
|
||||
let (matrix, unscaled, color) = if let Some(t) = obj.as_text_object() {
|
||||
let rect = obj
|
||||
.bounds()
|
||||
.map(|q| quad_to_rect(&q))
|
||||
.unwrap_or(Rect { left: 0.0, bottom: 0.0, right: 0.0, top: 0.0 });
|
||||
let (matrix, unscaled, scaled, color) = if let Some(t) = obj.as_text_object() {
|
||||
(
|
||||
obj.matrix().map_err(err)?,
|
||||
t.unscaled_font_size().value,
|
||||
t.scaled_font_size().value,
|
||||
t.fill_color().unwrap_or(PdfColor::new(0, 0, 0, 255)),
|
||||
)
|
||||
} else {
|
||||
(obj.matrix().map_err(err)?, 0.0, PdfColor::new(0, 0, 0, 255))
|
||||
(obj.matrix().map_err(err)?, 0.0, 0.0, PdfColor::new(0, 0, 0, 255))
|
||||
};
|
||||
captured.insert(idx, Captured { matrix, unscaled_font_size: unscaled, color, rect });
|
||||
captured.insert(
|
||||
idx,
|
||||
Captured { matrix, unscaled_font_size: unscaled, scaled_font_size: scaled, color, rect },
|
||||
);
|
||||
}
|
||||
|
||||
// (3) Xoá mọi index thuộc SetText/ReplaceImage/Delete theo GIẢM DẦN.
|
||||
let mut to_remove: Vec<u16> = ops
|
||||
.iter()
|
||||
.filter_map(|op| match op {
|
||||
// (4) Xoá mọi index thuộc SetText-substitute/ReplaceImage/Delete GIẢM DẦN.
|
||||
let mut to_remove: Vec<u16> = Vec::new();
|
||||
for (opi, op) in ops.iter().enumerate() {
|
||||
match op {
|
||||
EditOp::SetText { index, .. }
|
||||
| EditOp::ReplaceImage { index, .. }
|
||||
| EditOp::Delete { index } => Some(*index),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|i| valid(*i))
|
||||
.collect();
|
||||
if matches!(set_text_modes.get(&opi), Some(SetTextMode::Substitute(_))) =>
|
||||
{
|
||||
to_remove.push(*index)
|
||||
}
|
||||
EditOp::ReplaceImage { index, .. } | EditOp::Delete { index } => {
|
||||
to_remove.push(*index)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
to_remove.retain(|i| valid(*i));
|
||||
to_remove.sort_unstable();
|
||||
to_remove.dedup();
|
||||
for idx in to_remove.into_iter().rev() {
|
||||
let removed = page.objects_mut().remove_object_at_index(idx as usize).map_err(err)?;
|
||||
// BẪY PDFium: object vừa tách khỏi trang bị đánh dấu "unowned" → Drop
|
||||
// gọi FPDFPageObj_Destroy, mà destroy object vốn thuộc document gây
|
||||
// SEGFAULT (chính pdfium-render cũng cảnh báo điều này trong Drop của
|
||||
// PdfPageObject). Object đã được FPDFPage_RemoveObject tách ra nên sẽ
|
||||
// không còn render/lưu; ta `forget` để KHỎI gọi destroy. Rò rỉ không
|
||||
// đáng kể (giải phóng khi document đóng ngay sau khi lưu).
|
||||
// SEGFAULT. Object đã tách nên không còn render/lưu; `forget` để khỏi
|
||||
// destroy (rò rỉ nhỏ, giải phóng khi đóng document).
|
||||
std::mem::forget(removed);
|
||||
}
|
||||
|
||||
// (4) Thêm bản thay thế cho SetText / ReplaceImage.
|
||||
for op in ops {
|
||||
// (5) Thêm bản thay thế cho SetText-substitute / ReplaceImage.
|
||||
for (opi, op) in ops.iter().enumerate() {
|
||||
match op {
|
||||
EditOp::SetText { index, text, font_size, color, bold, italic } => {
|
||||
EditOp::SetText { index, text, font_size, color, .. } => {
|
||||
let key = match set_text_modes.get(&opi) {
|
||||
Some(SetTextMode::Substitute(key)) => key,
|
||||
_ => continue,
|
||||
};
|
||||
let cap = match captured.get(index) {
|
||||
Some(c) => c,
|
||||
None => continue,
|
||||
};
|
||||
let token = tokens.get(&(*bold, *italic)).copied().ok_or_else(|| {
|
||||
let token = tokens.get(key).copied().ok_or_else(|| {
|
||||
EngineError::Pdfium("thiếu token font cho SetText".into())
|
||||
})?;
|
||||
let size = font_size.unwrap_or(cap.unscaled_font_size).max(1.0);
|
||||
// Quy đổi cỡ hiển thị mong muốn → cỡ unscaled trước khi áp
|
||||
// matrix gốc (matrix có thể chứa scale — tránh phóng đại kép).
|
||||
let tf = match font_size {
|
||||
Some(target) if cap.scaled_font_size > 0.01 => {
|
||||
cap.unscaled_font_size * (*target / cap.scaled_font_size)
|
||||
}
|
||||
Some(target) => *target,
|
||||
None => cap.unscaled_font_size,
|
||||
}
|
||||
.max(1.0);
|
||||
let mut obj = page
|
||||
.objects_mut()
|
||||
.create_text_object(PdfPoints::ZERO, PdfPoints::ZERO, text.clone(), token, PdfPoints::new(size))
|
||||
.create_text_object(
|
||||
PdfPoints::ZERO,
|
||||
PdfPoints::ZERO,
|
||||
text.clone(),
|
||||
token,
|
||||
PdfPoints::new(tf),
|
||||
)
|
||||
.map_err(err)?;
|
||||
// Object mới tạo tại gốc (0,0) cỡ unscaled → matrix ~identity,
|
||||
// nên apply_matrix(matrix gốc) tương đương đặt lại đúng vị trí/scale.
|
||||
// Object mới tạo tại gốc (0,0) → matrix ~identity, nên
|
||||
// apply_matrix(matrix gốc) đặt lại đúng vị trí/scale.
|
||||
obj.apply_matrix(cap.matrix).map_err(err)?;
|
||||
let c = color
|
||||
.map(|c| PdfColor::new(c[0], c[1], c[2], c[3]))
|
||||
@@ -327,13 +589,15 @@ pub fn apply_edits(
|
||||
}
|
||||
}
|
||||
|
||||
// (5) Thêm object MỚI (AddText / AddImage).
|
||||
for op in ops {
|
||||
// (6) Thêm object MỚI (AddText / AddImage).
|
||||
for (opi, op) in ops.iter().enumerate() {
|
||||
match op {
|
||||
EditOp::AddText { x, y, text, font_size, color, bold, italic } => {
|
||||
let token = tokens.get(&(*bold, *italic)).copied().ok_or_else(|| {
|
||||
EngineError::Pdfium("thiếu token font cho AddText".into())
|
||||
})?;
|
||||
EditOp::AddText { x, y, text, font_size, color, .. } => {
|
||||
let token = add_text_keys
|
||||
.get(&opi)
|
||||
.and_then(|k| tokens.get(k))
|
||||
.copied()
|
||||
.ok_or_else(|| EngineError::Pdfium("thiếu token font cho AddText".into()))?;
|
||||
let mut obj = page
|
||||
.objects_mut()
|
||||
.create_text_object(
|
||||
|
||||
Reference in New Issue
Block a user