mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
f1864952ad
commit
41a50d9e96
17 files changed
+531
-83
No files matched your search
+44
-17
@@ -126,6 +126,7 @@ function createDocumentState() {
|
|||||||
searchMatches: [],
|
searchMatches: [],
|
||||||
searchIndex: -1,
|
searchIndex: -1,
|
||||||
searchRunning: false,
|
searchRunning: false,
|
||||||
|
searchTruncated: false,
|
||||||
navigationScroll: new Map(),
|
navigationScroll: new Map(),
|
||||||
fontSyncPending: false,
|
fontSyncPending: false,
|
||||||
fontCatalogLoading: false,
|
fontCatalogLoading: false,
|
||||||
@@ -4254,7 +4255,7 @@ async function beginCanvasText(index, box, surface, page) {
|
|||||||
const model = richTextModel(item);
|
const model = richTextModel(item);
|
||||||
const key = `temp-font-${crypto.randomUUID()}`;
|
const key = `temp-font-${crypto.randomUUID()}`;
|
||||||
model.runs[0].format.font = key;
|
model.runs[0].format.font = key;
|
||||||
canvasEditor.editText(item, face, null, state.composite ? null : { model, faces: new Map([[key, face]]), fonts: [{ key, data }] });
|
canvasEditor.editText(item, face, null, { model, faces: new Map([[key, face]]), fonts: [{ key, data }] });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isForegroundDocument(tab) && openSeq === state.openSeq) showError(err, false);
|
if (isForegroundDocument(tab) && openSeq === state.openSeq) showError(err, false);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -4704,13 +4705,13 @@ async function changeDocument(name, item, ...args) {
|
|||||||
const annotated = !scoped && ids.some(id => id.startsWith("annotation:"));
|
const annotated = !scoped && ids.some(id => id.startsWith("annotation:"));
|
||||||
const inserting = ["ofdgoInsertShape", "ofdgoInsertText", "ofdgoInsertImage", "ofdgoInsertInk"].includes(name) || name === "ofdgoSaveRichText" && !item;
|
const inserting = ["ofdgoInsertShape", "ofdgoInsertText", "ofdgoInsertImage", "ofdgoInsertInk"].includes(name) || name === "ofdgoSaveRichText" && !item;
|
||||||
const editingAnnotation = name === "ofdgoWriteAnnotation" && Boolean(state.annotationCreate?.item);
|
const editingAnnotation = name === "ofdgoWriteAnnotation" && Boolean(state.annotationCreate?.item);
|
||||||
if (scope && inserting && name === "ofdgoSaveRichText") throw new Error("组合内部暂不支持新增富文本");
|
const scopedRichText = scoped && name === "ofdgoSaveRichText";
|
||||||
if (scope && inserting) args.push(scope.key);
|
if (scope && (inserting || scopedRichText)) args.push(scope.key);
|
||||||
const textSelection = name === "ofdgoUpdateTextSelection";
|
const textSelection = name === "ofdgoUpdateTextSelection";
|
||||||
if (scoped && !operation && !textSelection) throw new Error("内部对象暂不支持此操作");
|
if (scoped && !operation && !textSelection && !scopedRichText) throw new Error("内部对象暂不支持此操作");
|
||||||
const doc = textSelection
|
const doc = textSelection
|
||||||
? await callWASM(name, item.index, item.selectionIDs, ...args)
|
? await callWASM(name, item.index, item.selectionIDs, ...args)
|
||||||
: scoped
|
: scoped && !scopedRichText
|
||||||
? await callWASM("ofdgoChangeCompositeObjects", item.index, scope.key, Array.isArray(item.id) ? item.id : members.map(member => member.id), operation, ...args)
|
? await callWASM("ofdgoChangeCompositeObjects", item.index, scope.key, Array.isArray(item.id) ? item.id : members.map(member => member.id), operation, ...args)
|
||||||
: annotated && operation ? await callWASM("ofdgoChangeSelection", item.index, ids, operation, ...args)
|
: annotated && operation ? await callWASM("ofdgoChangeSelection", item.index, ids, operation, ...args)
|
||||||
: await callWASM(name, ...(item ? [item.index, item.id] : []), ...args);
|
: await callWASM(name, ...(item ? [item.index, item.id] : []), ...args);
|
||||||
@@ -9797,7 +9798,7 @@ async function runBatch() {
|
|||||||
try {
|
try {
|
||||||
const result = await batchCall(slot, "ofdgoConvertFile", [item.file, options, fonts, destination], progress => batchProgress(item, position, jobs.length, progress));
|
const result = await batchCall(slot, "ofdgoConvertFile", [item.file, options, fonts, destination], progress => batchProgress(item, position, jobs.length, progress));
|
||||||
results[position] = result.files;
|
results[position] = result.files;
|
||||||
item.details = (result.pdf?.Warnings || []).map(warning => `${warning.Page ? `第${warning.Page}页:` : ""}${warning.Message}`);
|
item.details = (result.pdf?.Warnings || []).map(conversionWarningText);
|
||||||
for (const font of result.fonts || []) item.details.push(`${font.name}:${font.changed ? options.fontMode === 1 ? "已嵌入" : "已外置" : `未处理 · ${fontBindingError(font.reason)}`}`);
|
for (const font of result.fonts || []) item.details.push(`${font.name}:${font.changed ? options.fontMode === 1 ? "已嵌入" : "已外置" : `未处理 · ${fontBindingError(font.reason)}`}`);
|
||||||
if (result.unchanged && !result.bytes) {
|
if (result.unchanged && !result.bytes) {
|
||||||
batchStatus(item, "unchanged", "原样");
|
batchStatus(item, "unchanged", "原样");
|
||||||
@@ -10760,7 +10761,11 @@ function renderOutlines(reset = true) {
|
|||||||
showNavigation(selected && !selected.hidden ? selected : el.pagesTab);
|
showNavigation(selected && !selected.hidden ? selected : el.pagesTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SEARCH_MATCH_LIMIT = 10000;
|
||||||
|
|
||||||
function resetSearch(clearInput = true) {
|
function resetSearch(clearInput = true) {
|
||||||
|
for (const [id, request] of wasmRequests) if (request.tab === documentTab() && request.name === "ofdgoSearchPage") cancelOperation(id);
|
||||||
|
state.searchTruncated = false;
|
||||||
state.searchRunning = false;
|
state.searchRunning = false;
|
||||||
state.searchSeq += 1;
|
state.searchSeq += 1;
|
||||||
state.searchQuery = "";
|
state.searchQuery = "";
|
||||||
@@ -10777,16 +10782,20 @@ function resetSearch(clearInput = true) {
|
|||||||
|
|
||||||
async function refreshSearchPages(pages, openSeq) {
|
async function refreshSearchPages(pages, openSeq) {
|
||||||
const tab = documentTab(), target = tab.state, query = target.searchQuery;
|
const tab = documentTab(), target = tab.state, query = target.searchQuery;
|
||||||
if (!query || target.searchRunning) { resetSearch(false); return; }
|
if (!query || target.searchRunning || target.searchTruncated) { resetSearch(false); return; }
|
||||||
const seq = ++target.searchSeq;
|
const seq = ++target.searchSeq;
|
||||||
target.searchRunning = true;
|
target.searchRunning = true;
|
||||||
const selected = target.searchMatches[target.searchIndex];
|
const selected = target.searchMatches[target.searchIndex];
|
||||||
try {
|
try {
|
||||||
const matches = target.searchMatches.filter(match => !pages.includes(match.page));
|
const matches = target.searchMatches.filter(match => !pages.includes(match.page));
|
||||||
for (const page of pages) {
|
for (const page of pages) {
|
||||||
const result = await callTabWASM(tab, "ofdgoSearchPage", page, query);
|
const result = await callTabWASM(tab, "ofdgoSearchPage", page, query, SEARCH_MATCH_LIMIT - matches.length + 1);
|
||||||
if (tab.closed || target.openSeq !== openSeq || target.searchSeq !== seq) return;
|
if (tab.closed || target.openSeq !== openSeq || target.searchSeq !== seq) return;
|
||||||
for (const match of result || []) matches.push({...match, page});
|
for (const match of result || []) {
|
||||||
|
if (matches.length === SEARCH_MATCH_LIMIT) { target.searchTruncated = true; break; }
|
||||||
|
matches.push({...match, page});
|
||||||
|
}
|
||||||
|
if (target.searchTruncated) break;
|
||||||
}
|
}
|
||||||
matches.sort((a,b) => a.page - b.page);
|
matches.sort((a,b) => a.page - b.page);
|
||||||
target.searchMatches = matches;
|
target.searchMatches = matches;
|
||||||
@@ -10794,7 +10803,7 @@ async function refreshSearchPages(pages, openSeq) {
|
|||||||
if (isForegroundDocument(tab)) {
|
if (isForegroundDocument(tab)) {
|
||||||
el.searchResults.replaceChildren();
|
el.searchResults.replaceChildren();
|
||||||
clearSearchHighlights();
|
clearSearchHighlights();
|
||||||
el.searchStatus.textContent = matches.length ? "搜索完成" : "暂无结果";
|
el.searchStatus.textContent = target.searchTruncated ? "已达结果上限" : matches.length ? "搜索完成" : "暂无结果";
|
||||||
renderSearchResults();
|
renderSearchResults();
|
||||||
if (target.searchIndex >= 0) renderSearchHighlights(matches[target.searchIndex].page);
|
if (target.searchIndex >= 0) renderSearchHighlights(matches[target.searchIndex].page);
|
||||||
}
|
}
|
||||||
@@ -10829,8 +10838,8 @@ async function searchDocument() {
|
|||||||
target.searchRunning = true;
|
target.searchRunning = true;
|
||||||
try {
|
try {
|
||||||
for (let page = 0; page < pageCount; page += 1) {
|
for (let page = 0; page < pageCount; page += 1) {
|
||||||
const matches = await callWASM("ofdgoSearchPage", page, query);
|
const matches = await callWASM("ofdgoSearchPage", page, query, SEARCH_MATCH_LIMIT - target.searchMatches.length + 1);
|
||||||
if (seq !== state.searchSeq || openSeq !== state.openSeq) {
|
if (target !== state || seq !== target.searchSeq || openSeq !== target.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.searchStatus.textContent = `正在搜索文字 · ${page + 1} / ${pageCount} 页`;
|
el.searchStatus.textContent = `正在搜索文字 · ${page + 1} / ${pageCount} 页`;
|
||||||
@@ -10838,16 +10847,18 @@ async function searchDocument() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
|
if (target.searchMatches.length === SEARCH_MATCH_LIMIT) { target.searchTruncated = true; break; }
|
||||||
state.searchMatches.push({ ...match, page });
|
state.searchMatches.push({ ...match, page });
|
||||||
}
|
}
|
||||||
renderSearchResults();
|
renderSearchResults();
|
||||||
|
if (target.searchTruncated) break;
|
||||||
}
|
}
|
||||||
el.searchStatus.textContent = state.searchMatches.length ? "搜索完成" : "暂无结果";
|
el.searchStatus.textContent = target.searchTruncated ? "已达结果上限" : state.searchMatches.length ? "搜索完成" : "暂无结果";
|
||||||
if (state.searchIndex < 0 && state.searchMatches.length && !el.searchPanel.hidden && state.showPages) {
|
if (state.searchIndex < 0 && state.searchMatches.length && !el.searchPanel.hidden && state.showPages) {
|
||||||
await selectSearchMatch(0);
|
await selectSearchMatch(0);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (seq === state.searchSeq && openSeq === state.openSeq) {
|
if (target === state && seq === target.searchSeq && openSeq === target.openSeq) {
|
||||||
state.searchQuery = "";
|
state.searchQuery = "";
|
||||||
el.searchStatus.textContent = "搜索失败";
|
el.searchStatus.textContent = "搜索失败";
|
||||||
showError(err, false);
|
showError(err, false);
|
||||||
@@ -10900,6 +10911,7 @@ async function selectSearchMatch(index) {
|
|||||||
index = (index + count) % count;
|
index = (index + count) % count;
|
||||||
state.searchIndex = index;
|
state.searchIndex = index;
|
||||||
const match = state.searchMatches[index];
|
const match = state.searchMatches[index];
|
||||||
|
const target = state;
|
||||||
const seq = state.searchSeq;
|
const seq = state.searchSeq;
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
clearSearchHighlights();
|
clearSearchHighlights();
|
||||||
@@ -10908,7 +10920,7 @@ async function selectSearchMatch(index) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await nextFrame();
|
await nextFrame();
|
||||||
if (seq !== state.searchSeq || openSeq !== state.openSeq || state.searchIndex !== index || state.pageIndex !== match.page) {
|
if (target !== state || seq !== state.searchSeq || openSeq !== state.openSeq || state.searchIndex !== index || state.pageIndex !== match.page) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderSearchHighlights(match.page);
|
renderSearchHighlights(match.page);
|
||||||
@@ -10928,7 +10940,7 @@ async function selectSearchMatch(index) {
|
|||||||
if (state.editing) {
|
if (state.editing) {
|
||||||
const node = [...pageView.pageShell(match.page).querySelectorAll(".edit-object")].find(node => canvasEditor.nodes.get(node)?.id === match.id);
|
const node = [...pageView.pageShell(match.page).querySelectorAll(".edit-object")].find(node => canvasEditor.nodes.get(node)?.id === match.id);
|
||||||
const item = node && canvasEditor.nodes.get(node);
|
const item = node && canvasEditor.nodes.get(node);
|
||||||
if (item?.type === "TextObject") {
|
if (item?.type === "TextObject" && !match.parts?.length) {
|
||||||
state.selectObjects = true;
|
state.selectObjects = true;
|
||||||
pageView.setPan(false);
|
pageView.setPan(false);
|
||||||
canvasEditor.setTool("");
|
canvasEditor.setTool("");
|
||||||
@@ -11379,13 +11391,28 @@ function enablePageDrag(button, index) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function conversionWarningText(warning) {
|
||||||
|
const location = [];
|
||||||
|
if (warning.Page > 0) location.push(`第${warning.Page}页`);
|
||||||
|
if (warning.Offset > 0) location.push(`位置${warning.Offset}`);
|
||||||
|
const messages = [
|
||||||
|
[/^PDF output intents preserved as metadata; not applied to rendering$/, "输出配置仅保留元数据"],
|
||||||
|
[/^PDF annotation has no usable static appearance$/, "注解缺少可用外观"],
|
||||||
|
[/^invalid PDF widget appearance replaced from field properties$/, "控件外观已按属性重建"],
|
||||||
|
[/^PDF 3D rendering unavailable; normal appearance retained:/, "三维内容使用静态外观"],
|
||||||
|
[/^PDF 3D markup default view visibility applied; view switching not transferred$/, "三维视图仅保留默认状态"]
|
||||||
|
];
|
||||||
|
const message = messages.find(([pattern]) => pattern.test(warning.Message))?.[1] || warning.Message;
|
||||||
|
return `${location.length ? location.join(" · ") + ":" : ""}${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderMeta(keepDetails = false) {
|
function renderMeta(keepDetails = false) {
|
||||||
renderSecurity();
|
renderSecurity();
|
||||||
el.conversionNotice.hidden = !state.conversionWarnings.length;
|
el.conversionNotice.hidden = !state.conversionWarnings.length;
|
||||||
renderMetaContent(el.conversionWarnings, state.conversionWarnings, () => {
|
renderMetaContent(el.conversionWarnings, state.conversionWarnings, () => {
|
||||||
el.conversionWarnings.replaceChildren(...state.conversionWarnings.map(warning => {
|
el.conversionWarnings.replaceChildren(...state.conversionWarnings.map(warning => {
|
||||||
const item = document.createElement("p");
|
const item = document.createElement("p");
|
||||||
item.textContent = `第${warning.Page}页 · 位置${warning.Offset}:${warning.Message}`;
|
item.textContent = conversionWarningText(warning);
|
||||||
return item;
|
return item;
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -382,6 +382,7 @@ export class PrintDialog {
|
|||||||
this.root = document.querySelector("#ofdgoPrintDocument") || this.createRoot();
|
this.root = document.querySelector("#ofdgoPrintDocument") || this.createRoot();
|
||||||
this.root.replaceChildren();
|
this.root.replaceChildren();
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
|
style.textContent = this.layout.sides.map((side, i) => `@page ofdgoPrint${i}{size:${side.width}mm ${side.height}mm;margin:0}`).join("");
|
||||||
this.root.append(style);
|
this.root.append(style);
|
||||||
this.status("正在准备纸面", true, 0, this.layout.sides.length);
|
this.status("正在准备纸面", true, 0, this.layout.sides.length);
|
||||||
for (let i = 0; i < this.layout.sides.length; i++) {
|
for (let i = 0; i < this.layout.sides.length; i++) {
|
||||||
@@ -394,7 +395,6 @@ export class PrintDialog {
|
|||||||
img.src = url;
|
img.src = url;
|
||||||
section.append(img);
|
section.append(img);
|
||||||
section.style.cssText = `page:ofdgoPrint${i};width:${side.width}mm;height:${side.height}mm`;
|
section.style.cssText = `page:ofdgoPrint${i};width:${side.width}mm;height:${side.height}mm`;
|
||||||
style.textContent += `@page ofdgoPrint${i}{size:${side.width}mm ${side.height}mm;margin:0}`;
|
|
||||||
this.root.append(section);
|
this.root.append(section);
|
||||||
await img.decode();
|
await img.decode();
|
||||||
if (this.current(sequence)) this.status("正在准备纸面", true, i + 1, this.layout.sides.length);
|
if (this.current(sequence)) this.status("正在准备纸面", true, i + 1, this.layout.sides.length);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ require (
|
|||||||
github.com/xiaoqidun/guigo v0.0.0-20261026011429-36a970cd3fb6
|
github.com/xiaoqidun/guigo v0.0.0-20261026011429-36a970cd3fb6
|
||||||
github.com/xiaoqidun/j2kgo v1.0.2
|
github.com/xiaoqidun/j2kgo v1.0.2
|
||||||
github.com/xiaoqidun/jbig2 v1.1.8
|
github.com/xiaoqidun/jbig2 v1.1.8
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261023171626-3473b442eed5
|
github.com/xiaoqidun/pdfgo v0.0.0-20261026125713-90454771d5d0
|
||||||
golang.org/x/crypto v0.58.0
|
golang.org/x/crypto v0.58.0
|
||||||
golang.org/x/image v0.47.0
|
golang.org/x/image v0.47.0
|
||||||
golang.org/x/sys v0.49.0
|
golang.org/x/sys v0.49.0
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw
|
|||||||
github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo=
|
github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.8/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
|
github.com/xiaoqidun/jbig2 v1.1.8/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261023171626-3473b442eed5 h1:TudqqH4zfCt7jrBTN/yhMQqa7BvkozYJoF9q+T6FE6s=
|
github.com/xiaoqidun/pdfgo v0.0.0-20261026125713-90454771d5d0 h1:fBOCrYQedZ3ZxAlpI5bXeVs+/kWxWbVrt0iFlmTYMpU=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261023171626-3473b442eed5/go.mod h1:AaabzsObZ8Z76AcaWbo6swRROrJsblyIGB7HbieeJA4=
|
github.com/xiaoqidun/pdfgo v0.0.0-20261026125713-90454771d5d0/go.mod h1:AaabzsObZ8Z76AcaWbo6swRROrJsblyIGB7HbieeJA4=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
+76
-20
@@ -611,7 +611,7 @@ func (s *Session) PageText(index int) (*ofdgo.PageText, error) {
|
|||||||
text := s.cachedPageText(index)
|
text := s.cachedPageText(index)
|
||||||
if text == nil {
|
if text == nil {
|
||||||
var err error
|
var err error
|
||||||
text, err = s.readPageText(index)
|
text, err = s.readPageText(context.Background(), index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -651,7 +651,7 @@ func (s *Session) PageTextString(index int) (string, error) {
|
|||||||
text := s.cachedPageText(index)
|
text := s.cachedPageText(index)
|
||||||
if text == nil {
|
if text == nil {
|
||||||
var err error
|
var err error
|
||||||
text, err = s.readPageText(index)
|
text, err = s.readPageText(context.Background(), index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -663,11 +663,37 @@ func (s *Session) PageTextString(index int) (string, error) {
|
|||||||
// 入参: index 页面索引, query 搜索文字
|
// 入参: index 页面索引, query 搜索文字
|
||||||
// 返回: []ofdgo.TextMatch 匹配结果, error 错误信息
|
// 返回: []ofdgo.TextMatch 匹配结果, error 错误信息
|
||||||
func (s *Session) SearchPage(index int, query string) ([]ofdgo.TextMatch, error) {
|
func (s *Session) SearchPage(index int, query string) ([]ofdgo.TextMatch, error) {
|
||||||
text, err := s.PageText(index)
|
return s.SearchPageContext(context.Background(), index, query, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchPageContext 搜索页面文字并限制累计结果,零上限返回全部,取消时不返回部分结果
|
||||||
|
// 入参: ctx 取消上下文, index 页面索引, query 文字, limit 结果上限
|
||||||
|
// 返回: []ofdgo.TextMatch 匹配结果, error 取消或读取错误
|
||||||
|
func (s *Session) SearchPageContext(ctx context.Context, index int, query string, limit int) ([]ofdgo.TextMatch, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
text := s.cachedPageText(index)
|
||||||
|
if text == nil {
|
||||||
|
var err error
|
||||||
|
text, err = s.readPageText(ctx, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return text.Search(query), nil
|
s.cachePageText(index, text)
|
||||||
|
}
|
||||||
|
var matches []ofdgo.TextMatch
|
||||||
|
err := text.SearchEach(ctx, query, func(match ofdgo.TextMatch) error {
|
||||||
|
matches = append(matches, match)
|
||||||
|
if limit > 0 && len(matches) >= limit {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return matches, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary 获取首屏所需信息,不解析页面图元或执行验签
|
// Summary 获取首屏所需信息,不解析页面图元或执行验签
|
||||||
@@ -1018,13 +1044,13 @@ func (s *Session) scanFontInfoBatch() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// readPageText 复用已加载页面,否则只解析文字所需图元
|
// readPageText 复用已加载页面,否则只解析文字所需图元
|
||||||
// 入参: index 页面索引
|
// 入参: ctx 取消上下文, index 页面索引
|
||||||
// 返回: *ofdgo.PageText 页面文字, error 错误信息
|
// 返回: *ofdgo.PageText 页面文字, error 错误信息
|
||||||
func (s *Session) readPageText(index int) (*ofdgo.PageText, error) {
|
func (s *Session) readPageText(ctx context.Context, index int) (*ofdgo.PageText, error) {
|
||||||
if page := s.pageCache[index]; page != nil {
|
if page := s.pageCache[index]; page != nil {
|
||||||
return s.Renderer.PageText(page)
|
return s.Renderer.PageTextContext(ctx, page)
|
||||||
}
|
}
|
||||||
return s.Renderer.PageTextByIndex(index)
|
return s.Renderer.PageTextByIndexContext(ctx, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
// pageInfos 获取全部真实页面尺寸,首屏、配置及详情共用区域缓存
|
// pageInfos 获取全部真实页面尺寸,首屏、配置及详情共用区域缓存
|
||||||
@@ -1549,7 +1575,6 @@ func (e *businessEngine) finishPDF() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, e.pdfError(err)
|
return nil, e.pdfError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reader, err := editor.Reader(e.pdf.operation)
|
reader, err := editor.Reader(e.pdf.operation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -3001,7 +3026,11 @@ func (e *businessEngine) searchPage(args []platformValue) (any, error) {
|
|||||||
if e.currentSession == nil {
|
if e.currentSession == nil {
|
||||||
return nil, fmt.Errorf("ofd document is not opened")
|
return nil, fmt.Errorf("ofd document is not opened")
|
||||||
}
|
}
|
||||||
return e.currentSession.SearchPage(args[0].Int(), args[1].String())
|
limit := 10001
|
||||||
|
if len(args) > 2 {
|
||||||
|
limit = max(1, min(limit, args[2].Int()))
|
||||||
|
}
|
||||||
|
return e.currentSession.SearchPageContext(e.operationContext(), args[0].Int(), args[1].String(), limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// exportFormats 获取导出格式
|
// exportFormats 获取导出格式
|
||||||
@@ -3387,7 +3416,9 @@ func (e *businessEngine) editorObjects(index int, text *ofdgo.PageText) ([]any,
|
|||||||
}
|
}
|
||||||
if object.Type == "CompositeObject" {
|
if object.Type == "CompositeObject" {
|
||||||
if value, richErr := e.currentEditor.RichText(index, id); richErr == nil {
|
if value, richErr := e.currentEditor.RichText(index, id); richErr == nil {
|
||||||
e.editorRichText(index, item, object, value, fontNames)
|
if local, matrix, err := e.currentEditor.RichTextFrame(index, id); err == nil {
|
||||||
|
e.editorRichText(item, object, value, fontNames, local, matrix)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
objects = append(objects, item)
|
objects = append(objects, item)
|
||||||
@@ -3975,6 +4006,11 @@ func (e *businessEngine) compositeObjects(args []platformValue) (any, error) {
|
|||||||
item["imageBounds"] = e.editorBox(box)
|
item["imageBounds"] = e.editorBox(box)
|
||||||
item["cropped"] = member.Cropped
|
item["cropped"] = member.Cropped
|
||||||
}
|
}
|
||||||
|
if member.Object.Type == "CompositeObject" {
|
||||||
|
if value, frame, matrix, err := e.currentEditor.CompositeRichText(args[0].Int(), member); err == nil {
|
||||||
|
e.editorRichText(item, member.Object, value, fontNames, frame, matrix)
|
||||||
|
}
|
||||||
|
}
|
||||||
objects = append(objects, item)
|
objects = append(objects, item)
|
||||||
}
|
}
|
||||||
return objects, nil
|
return objects, nil
|
||||||
@@ -4831,13 +4867,9 @@ func (e *businessEngine) styleText(args []platformValue) (any, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// editorRichText 将富文本复合图元作为一个文本框提供给编辑器
|
// editorRichText 将富文本复合图元作为一个文本框提供给编辑器
|
||||||
// 入参: page 页面索引, item 浏览器对象, object 标准复合图元, value 富文本, fonts 字体名称
|
// 入参: item 浏览器对象, object 标准复合图元, value 富文本, fonts 字体名称, local 局部编辑框, matrix 页面变换
|
||||||
func (e *businessEngine) editorRichText(page int, item map[string]any, object ofdgo.GraphicObject, value ofdgo.RichText, fonts map[string]string) {
|
func (e *businessEngine) editorRichText(item map[string]any, object ofdgo.GraphicObject, value ofdgo.RichText, fonts map[string]string, local ofdgo.Box, matrix ofdgo.Matrix) {
|
||||||
if len(value.Runs) == 0 {
|
if len(value.Runs) == 0 || object.CompositeGraphicUnit.Clips != nil {
|
||||||
return
|
|
||||||
}
|
|
||||||
local, matrix, err := e.currentEditor.RichTextFrame(page, object.CompositeGraphicUnit.ID)
|
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
width, height := local.W, local.H
|
width, height := local.W, local.H
|
||||||
@@ -4957,7 +4989,7 @@ func (e *businessEngine) readRichText(value platformValue) (ofdgo.RichText, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// saveRichText 原子保存文字、样式和新增字体,空标识创建文本框
|
// saveRichText 原子保存文字、样式和新增字体,空标识创建文本框
|
||||||
// 入参: args 页码、标识、富文本JSON、新建边界和可选字体集合
|
// 入参: args 页码、标识、富文本JSON、新建边界、可选字体集合和父路径
|
||||||
// 返回: any 文档信息及文字选区, error 错误信息
|
// 返回: any 文档信息及文字选区, error 错误信息
|
||||||
func (e *businessEngine) saveRichText(args []platformValue) (any, error) {
|
func (e *businessEngine) saveRichText(args []platformValue) (any, error) {
|
||||||
value, err := e.readRichText(args[2])
|
value, err := e.readRichText(args[2])
|
||||||
@@ -4971,6 +5003,31 @@ func (e *businessEngine) saveRichText(args []platformValue) (any, error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(args) > 5 && args[5].String() != "" {
|
||||||
|
key := args[5].String()
|
||||||
|
path, err := e.compositePath(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
box := args[3]
|
||||||
|
if id != "" {
|
||||||
|
indexes, err := e.compositeIndexes(key, []string{id})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
width, offset := 0.0, 0.0
|
||||||
|
if !box.IsNull() && !box.IsUndefined() && box.Get("frameWidth").Type() == platformTypeNumber {
|
||||||
|
width = box.Get("frameWidth").Float()
|
||||||
|
if box.Get("frameOffset").Type() == platformTypeNumber {
|
||||||
|
offset = box.Get("frameOffset").Float()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.currentEditor.UpdateCompositeRichText(page, path, indexes[0], value, offset, width)
|
||||||
|
}
|
||||||
|
index, err := e.currentEditor.AddCompositeRichText(page, path, ofdgo.Box{X: box.Get("x").Float(), Y: box.Get("y").Float(), W: box.Get("width").Float(), H: box.Get("height").Float()}, value)
|
||||||
|
id = fmt.Sprintf("%s/%d", key, index)
|
||||||
|
return err
|
||||||
|
}
|
||||||
if id != "" {
|
if id != "" {
|
||||||
if err := e.currentEditor.UpdateRichText(page, id, value); err != nil {
|
if err := e.currentEditor.UpdateRichText(page, id, value); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -7893,6 +7950,5 @@ func failureResult(err error) apiResult {
|
|||||||
if errors.As(err, &result.MissingGlyphs) {
|
if errors.As(err, &result.MissingGlyphs) {
|
||||||
result.ReasonCode = ofdgo.EditMissingGlyphs
|
result.ReasonCode = ofdgo.EditMissingGlyphs
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ func (e *Editor) SetPageActions(index int, actions []Action) error {
|
|||||||
}
|
}
|
||||||
before, after := cloneEditorData(page.Actions), cloneEditorData(actions)
|
before, after := cloneEditorData(page.Actions), cloneEditorData(actions)
|
||||||
page.Actions = cloneEditorData(after)
|
page.Actions = cloneEditorData(after)
|
||||||
if change := e.recordChange(); change != nil {
|
if change := e.recordChange(index); change != nil {
|
||||||
change.undo = func(e *Editor) { e.pages[index].Actions = cloneEditorData(before) }
|
change.undo = func(e *Editor) { e.pages[index].Actions = cloneEditorData(before) }
|
||||||
change.redo = func(e *Editor) { e.pages[index].Actions = cloneEditorData(after) }
|
change.redo = func(e *Editor) { e.pages[index].Actions = cloneEditorData(after) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,6 +418,7 @@ func (e *Editor) compositeScope(page int, path ObjectPath) (*Reader, *Renderer,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, nil, err
|
return nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
root.node.parent = &editorXML{name: xml.Name{Local: "Layer"}, attrs: []xml.Attr{{Name: xml.Name{Local: "ID"}, Value: layer.ID}, {Name: xml.Name{Local: "DrawParam"}, Value: layer.DrawParam}}, parent: &editorXML{name: xml.Name{Local: "Content"}}}
|
||||||
root.setStates(object.CompositeGraphicUnit.states)
|
root.setStates(object.CompositeGraphicUnit.states)
|
||||||
reader, err := e.reader(nil)
|
reader, err := e.reader(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -848,6 +849,7 @@ func (e *Editor) editCompositeScope(page int, path ObjectPath, edit func(*Render
|
|||||||
next.setStates(root.states)
|
next.setStates(root.states)
|
||||||
next.object.CompositeGraphicUnit.states = next.states
|
next.object.CompositeGraphicUnit.states = next.states
|
||||||
origin := e.objectOrigin(path.ID)
|
origin := e.objectOrigin(path.ID)
|
||||||
|
next.node.parent = root.node.parent
|
||||||
updated := &editorObjectOrigin{data: data, node: next.node, object: next.object}
|
updated := &editorObjectOrigin{data: data, node: next.node, object: next.object}
|
||||||
if origin != nil {
|
if origin != nil {
|
||||||
next.node.parent = origin.node.parent
|
next.node.parent = origin.node.parent
|
||||||
|
|||||||
@@ -16,12 +16,13 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CompositeSelection 同一内部容器的独立选区快照,共享不可变字体和图片资源
|
// CompositeSelection 同一内部容器的独立选区快照,共享不可变字体和图片资源
|
||||||
@@ -36,6 +37,7 @@ type CompositeSelection struct {
|
|||||||
containers []int
|
containers []int
|
||||||
nodes []*editorCompositeNode
|
nodes []*editorCompositeNode
|
||||||
source *editorSourcePage
|
source *editorSourcePage
|
||||||
|
richText map[int]richTextMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// PasteCompositeSelection 将内部对象快照粘贴到页面顶层,保持嵌套结构、源继承外观和裁剪
|
// PasteCompositeSelection 将内部对象快照粘贴到页面顶层,保持嵌套结构、源继承外观和裁剪
|
||||||
@@ -73,9 +75,11 @@ func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []in
|
|||||||
slices.Sort(indexes)
|
slices.Sort(indexes)
|
||||||
var owner *editorCompositeNode
|
var owner *editorCompositeNode
|
||||||
var container *editorXML
|
var container *editorXML
|
||||||
selection := &CompositeSelection{editor: e, document: e.documentIdentity(), page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: slices.Clone(path.Children)}}
|
selection := &CompositeSelection{richText: make(map[int]richTextMetadata), editor: e, document: e.documentIdentity(), page: e.pages[page].ID, path: ObjectPath{ID: path.ID, Annotation: path.Annotation, Children: slices.Clone(path.Children)}}
|
||||||
if path.Annotation == "" {
|
if path.Annotation == "" {
|
||||||
selection.source = e.objectOrigin(path.ID).page
|
if origin := e.objectOrigin(path.ID); origin != nil {
|
||||||
|
selection.source = origin.page
|
||||||
|
}
|
||||||
} else if e.source != nil {
|
} else if e.source != nil {
|
||||||
selection.source = e.source.pages[e.pages[page].ID]
|
selection.source = e.source.pages[e.pages[page].ID]
|
||||||
}
|
}
|
||||||
@@ -95,6 +99,11 @@ func (e *Editor) CaptureCompositeObjects(page int, path ObjectPath, indexes []in
|
|||||||
copy := *node
|
copy := *node
|
||||||
copy.owner, copy.span = nil, nil
|
copy.owner, copy.span = nil, nil
|
||||||
selection.nodes = append(selection.nodes, ©)
|
selection.nodes = append(selection.nodes, ©)
|
||||||
|
if entry, err := e.richTextEntry(page, editorObjectID(node.object)); err == nil {
|
||||||
|
selection.richText[j] = cloneEditorData(entry.metadata)
|
||||||
|
} else if !errors.Is(err, ErrNotRichText) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
scope := root.loadedScope(path.Children)
|
scope := root.loadedScope(path.Children)
|
||||||
for scope != owner {
|
for scope != owner {
|
||||||
@@ -134,6 +143,7 @@ func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *Com
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
var result []int
|
var result []int
|
||||||
|
err := e.Transaction(func(e *Editor) error {
|
||||||
err := e.editCompositeScope(page, path, func(renderer *Renderer, root *editorCompositeNode, _ []*editorCompositeNode) error {
|
err := e.editCompositeScope(page, path, func(renderer *Renderer, root *editorCompositeNode, _ []*editorCompositeNode) error {
|
||||||
scope := root.loadedScope(path.Children)
|
scope := root.loadedScope(path.Children)
|
||||||
owner := scope
|
owner := scope
|
||||||
@@ -191,6 +201,11 @@ func (e *Editor) PasteCompositeObjects(page int, path ObjectPath, selection *Com
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.putCompositeRichTextCopies(page, path, result, selection.richText)
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -226,7 +241,7 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
|
|||||||
renderer := e.newRenderer(reader)
|
renderer := e.newRenderer(reader)
|
||||||
var objects []GraphicObject
|
var objects []GraphicObject
|
||||||
var remaps []map[string]string
|
var remaps []map[string]string
|
||||||
for _, source := range selection.nodes {
|
for i, source := range selection.nodes {
|
||||||
copy, err := newEditorCompositeNode(source.data)
|
copy, err := newEditorCompositeNode(source.data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -329,6 +344,14 @@ func (e *Editor) pasteCompositeSelection(selection *CompositeSelection, paste fu
|
|||||||
copy.object.CompositeGraphicUnit.states = object.CompositeGraphicUnit.states
|
copy.object.CompositeGraphicUnit.states = object.CompositeGraphicUnit.states
|
||||||
}
|
}
|
||||||
object.origin = &editorObjectOrigin{editor: e, document: e.documentIdentity(), page: selection.source, data: copy.data, node: copy.node, object: copy.object}
|
object.origin = &editorObjectOrigin{editor: e, document: e.documentIdentity(), page: selection.source, data: copy.data, node: copy.node, object: copy.object}
|
||||||
|
if meta, ok := selection.richText[i]; ok {
|
||||||
|
unit, err := e.compositeDefinition(object.CompositeGraphicUnit.ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
meta.Digest = richTextDigest(unit.object.CompositeGraphicUnit.Objects)
|
||||||
|
object.richText = &meta
|
||||||
|
}
|
||||||
objects = append(objects, object)
|
objects = append(objects, object)
|
||||||
remaps = append(remaps, copy.remaps...)
|
remaps = append(remaps, copy.remaps...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,29 @@ func (e *Editor) AddCompositeObject(page int, path ObjectPath, object GraphicObj
|
|||||||
// 入参: page 页面索引, path 父路径, objects 当前文档快照或基本对象, dx 页面横向位移, dy 页面纵向位移
|
// 入参: page 页面索引, path 父路径, objects 当前文档快照或基本对象, dx 页面横向位移, dy 页面纵向位移
|
||||||
// 返回: []int 新成员序号, error 错误信息
|
// 返回: []int 新成员序号, error 错误信息
|
||||||
func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []GraphicObject, dx, dy float64) ([]int, error) {
|
func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []GraphicObject, dx, dy float64) ([]int, error) {
|
||||||
|
metadata, err := e.copiedRichTextMetadata(objects)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(metadata) == 0 {
|
||||||
|
return e.copyObjectsToComposite(page, path, objects, dx, dy)
|
||||||
|
}
|
||||||
|
var indexes []int
|
||||||
|
err = e.Transaction(func(e *Editor) error {
|
||||||
|
var err error
|
||||||
|
indexes, err = e.copyObjectsToComposite(page, path, objects, dx, dy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.putCompositeRichTextCopies(page, path, indexes, metadata)
|
||||||
|
})
|
||||||
|
return indexes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyObjectsToComposite 插入对象原文,由调用方同步富文本语义
|
||||||
|
// 入参: page 页面索引, path 父路径, objects 对象快照, dx 横向位移, dy 纵向位移
|
||||||
|
// 返回: []int 成员序号, error 校验或提交错误
|
||||||
|
func (e *Editor) copyObjectsToComposite(page int, path ObjectPath, objects []GraphicObject, dx, dy float64) ([]int, error) {
|
||||||
if !finite(dx) || !finite(dy) {
|
if !finite(dx) || !finite(dy) {
|
||||||
return nil, fmt.Errorf("copy requires finite offsets")
|
return nil, fmt.Errorf("copy requires finite offsets")
|
||||||
}
|
}
|
||||||
@@ -49,6 +72,9 @@ func (e *Editor) CopyObjectsToComposite(page int, path ObjectPath, objects []Gra
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
owner := root.loadedScope(path.Children)
|
owner := root.loadedScope(path.Children)
|
||||||
|
if owner.ref != nil {
|
||||||
|
owner = owner.ref
|
||||||
|
}
|
||||||
box, _ := ParseBox(owner.object.CompositeGraphicUnit.Boundary)
|
box, _ := ParseBox(owner.object.CompositeGraphicUnit.Boundary)
|
||||||
parent := owner.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(NewMatrix(owner.object.CompositeGraphicUnit.CTM))
|
parent := owner.parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(NewMatrix(owner.object.CompositeGraphicUnit.CTM))
|
||||||
inverse, ok := parent.Invert()
|
inverse, ok := parent.Invert()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -45,6 +46,8 @@ func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
return nil, fmt.Errorf("copy requires finite offsets")
|
return nil, fmt.Errorf("copy requires finite offsets")
|
||||||
}
|
}
|
||||||
var result []int
|
var result []int
|
||||||
|
metadata := make(map[int]richTextMetadata)
|
||||||
|
err := e.Transaction(func(e *Editor) error {
|
||||||
err := e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
err := e.editCompositeObjects(page, path, indexes, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
container := nodes[0].span.parent
|
container := nodes[0].span.parent
|
||||||
for i, node := range nodes {
|
for i, node := range nodes {
|
||||||
@@ -53,6 +56,16 @@ func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index })
|
slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index })
|
||||||
|
for i, node := range nodes {
|
||||||
|
if node.object.Type == "CompositeObject" {
|
||||||
|
entry, err := e.richTextEntry(page, editorObjectID(node.object))
|
||||||
|
if err == nil {
|
||||||
|
metadata[i] = entry.metadata
|
||||||
|
} else if !errors.Is(err, ErrNotRichText) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
data, states, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
|
data, states, err := e.copyCompositeNodes(renderer, nodes, dx, dy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -71,6 +84,11 @@ func (e *Editor) CopyCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.putCompositeRichTextCopies(page, path, result, metadata)
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -711,6 +711,7 @@ func editorPreservedObject(before, after GraphicObject) bool {
|
|||||||
}
|
}
|
||||||
before = cloneEditorData(before)
|
before = cloneEditorData(before)
|
||||||
before.state = after.state
|
before.state = after.state
|
||||||
|
before.richText = after.richText
|
||||||
switch before.Type {
|
switch before.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
a, b := &before.TextObject, after.TextObject
|
a, b := &before.TextObject, after.TextObject
|
||||||
|
|||||||
+204
-3
@@ -56,6 +56,7 @@ type richTextEntry struct {
|
|||||||
type richTextIndex struct {
|
type richTextIndex struct {
|
||||||
source *editorSource
|
source *editorSource
|
||||||
revision uint64
|
revision uint64
|
||||||
|
remaps map[string]int
|
||||||
entries map[string]richTextEntry
|
entries map[string]richTextEntry
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
@@ -204,13 +205,20 @@ func (e *Editor) RichText(page int, id string) (RichText, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return RichText{}, err
|
return RichText{}, err
|
||||||
}
|
}
|
||||||
|
return e.richTextObject(page, object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// richTextObject 读取对象的编辑标引并校验标准内容
|
||||||
|
// 入参: page 页面索引, object 当前对象
|
||||||
|
// 返回: RichText 编辑内容, error 识别或持久化错误
|
||||||
|
func (e *Editor) richTextObject(page int, object GraphicObject) (RichText, error) {
|
||||||
if object.Type == "TextObject" {
|
if object.Type == "TextObject" {
|
||||||
return e.promoteRichText(page, object)
|
return e.promoteRichText(page, object)
|
||||||
}
|
}
|
||||||
if object.Type != "CompositeObject" {
|
if object.Type != "CompositeObject" {
|
||||||
return RichText{}, ErrNotRichText
|
return RichText{}, ErrNotRichText
|
||||||
}
|
}
|
||||||
entry, err := e.richTextEntry(page, id)
|
entry, err := e.richTextEntry(page, editorObjectID(object))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return RichText{}, err
|
return RichText{}, err
|
||||||
}
|
}
|
||||||
@@ -275,6 +283,163 @@ func (e *Editor) RichTextFrame(page int, id string) (Box, Matrix, error) {
|
|||||||
return entry.metadata.Frame, TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(object.CompositeGraphicUnit.CTM)).Multiply(TranslationMatrix(entry.metadata.Offset[0], entry.metadata.Offset[1])), nil
|
return entry.metadata.Frame, TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(object.CompositeGraphicUnit.CTM)).Multiply(TranslationMatrix(entry.metadata.Offset[0], entry.metadata.Offset[1])), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddCompositeRichText 向内部范围添加富文本,使用页面坐标并保存编辑标引
|
||||||
|
// 入参: page 页面索引, path 父路径, box 页面编辑框, value 富文本
|
||||||
|
// 返回: int 新成员序号, error 排版或提交错误
|
||||||
|
func (e *Editor) AddCompositeRichText(page int, path ObjectPath, box Box, value RichText) (int, error) {
|
||||||
|
var index int
|
||||||
|
err := e.Transaction(func(e *Editor) error {
|
||||||
|
if _, err := creationBox(editorBoxString(box)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
object, meta, err := e.prepareRichText(box, value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
index, err = e.AddCompositeObject(page, path, object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
member, err := e.CompositeObject(page, path, index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.putCompositeRichTextMetadata(page, member.Object, meta, -1)
|
||||||
|
})
|
||||||
|
return index, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompositeRichText 读取内部富文本值及局部编辑框,不提升未标引的普通文字
|
||||||
|
// 入参: page 页面索引, member 当前内部成员快照
|
||||||
|
// 返回: RichText 编辑内容, Box 局部编辑框, Matrix 页面变换, error 识别或持久化错误
|
||||||
|
func (e *Editor) CompositeRichText(page int, member CompositeMember) (RichText, Box, Matrix, error) {
|
||||||
|
if _, err := e.page(page); err != nil {
|
||||||
|
return RichText{}, Box{}, Matrix{}, err
|
||||||
|
}
|
||||||
|
if member.Object.Type != "CompositeObject" {
|
||||||
|
return RichText{}, Box{}, Matrix{}, ErrNotRichText
|
||||||
|
}
|
||||||
|
value, err := e.richTextObject(page, member.Object)
|
||||||
|
if err != nil {
|
||||||
|
return RichText{}, Box{}, Matrix{}, err
|
||||||
|
}
|
||||||
|
entry, err := e.richTextEntry(page, editorObjectID(member.Object))
|
||||||
|
if err != nil {
|
||||||
|
return RichText{}, Box{}, Matrix{}, err
|
||||||
|
}
|
||||||
|
return value, entry.metadata.Frame, member.Matrix.Multiply(TranslationMatrix(entry.metadata.Offset[0], entry.metadata.Offset[1])), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCompositeRichText 原子重排内部富文本,保留父变换并隔离共享实例
|
||||||
|
// 入参: page 页面索引, path 父路径, index 成员序号, value 富文本, offset 局部横向偏移, width 新段落宽度,零值保留
|
||||||
|
// 返回: error 校验或提交错误
|
||||||
|
func (e *Editor) UpdateCompositeRichText(page int, path ObjectPath, index int, value RichText, offset, width float64) error {
|
||||||
|
if !finite(offset) || !finite(width) || width < 0 || width == 0 && offset != 0 {
|
||||||
|
return fmt.Errorf("invalid rich text frame")
|
||||||
|
}
|
||||||
|
return e.Transaction(func(e *Editor) error {
|
||||||
|
var metadata richTextMetadata
|
||||||
|
entryIndex := -1
|
||||||
|
changed := false
|
||||||
|
err := e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
node := nodes[0]
|
||||||
|
current, frame, _, err := e.CompositeRichText(page, members[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read composite member %s: %w", editorObjectID(members[0].Object), err)
|
||||||
|
}
|
||||||
|
if err := value.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if offset == 0 && (width == 0 || width == frame.W) && reflect.DeepEqual(normalizeRichText(current), normalizeRichText(value)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if node.object.CompositeGraphicUnit.Clips != nil {
|
||||||
|
return fmt.Errorf("clipped rich text requires explicit clip removal before reflow")
|
||||||
|
}
|
||||||
|
entry, err := e.richTextEntry(page, editorObjectID(node.object))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read composite node %s: %w", editorObjectID(node.object), err)
|
||||||
|
}
|
||||||
|
object := cloneEditorData(node.object)
|
||||||
|
if !node.boundaryInCTM {
|
||||||
|
object = compositeBoundary(object, node.parent, true)
|
||||||
|
}
|
||||||
|
unit := &object.CompositeGraphicUnit
|
||||||
|
box, err := creationBox(unit.Boundary)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
matrix := TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(unit.CTM)).Multiply(TranslationMatrix(entry.metadata.Offset[0]+offset, entry.metadata.Offset[1]))
|
||||||
|
if width > 0 {
|
||||||
|
frame.W = width
|
||||||
|
}
|
||||||
|
next, meta, err := e.prepareRichText(frame, value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
placement := richTextPlacement(meta, matrix)
|
||||||
|
unit.ResourceID, unit.Boundary, unit.CTM = next.CompositeGraphicUnit.ResourceID, placement.Boundary, placement.CTM
|
||||||
|
if !node.boundaryInCTM {
|
||||||
|
object = compositeBoundary(object, node.parent, false)
|
||||||
|
}
|
||||||
|
metadata, entryIndex, changed = meta, entry.index, true
|
||||||
|
return node.update(object)
|
||||||
|
})
|
||||||
|
if err != nil || !changed {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
member, err := e.CompositeObject(page, path, index)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if entry, err := e.richTextEntry(page, editorObjectID(member.Object)); err == nil {
|
||||||
|
entryIndex = entry.index
|
||||||
|
} else if !errors.Is(err, ErrNotRichText) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.putCompositeRichTextMetadata(page, member.Object, metadata, entryIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// putCompositeRichTextMetadata 在容器坐标规范化后更新标准内容摘要和编辑标引
|
||||||
|
// 入参: page 页面索引, object 当前成员, meta 编辑信息, index 原入口或-1
|
||||||
|
// 返回: error 资源或提交错误
|
||||||
|
func (e *Editor) putCompositeRichTextMetadata(page int, object GraphicObject, meta richTextMetadata, index int) error {
|
||||||
|
unit, err := e.compositeDefinition(object.CompositeGraphicUnit.ResourceID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
meta.Digest = richTextDigest(unit.object.CompositeGraphicUnit.Objects)
|
||||||
|
return e.putRichTextMetadata(page, editorObjectID(object), meta, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// putCompositeRichTextCopies 一次读取复制后的选区,按对象顺序恢复富文本标引
|
||||||
|
// 入参: page 页面索引, path 父路径, indexes 新成员序号, metadata 选区位置对应的编辑语义
|
||||||
|
// 返回: error 读取或提交错误
|
||||||
|
func (e *Editor) putCompositeRichTextCopies(page int, path ObjectPath, indexes []int, metadata map[int]richTextMetadata) error {
|
||||||
|
if len(metadata) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
selected := make([]int, 0, len(metadata))
|
||||||
|
values := make([]richTextMetadata, 0, len(metadata))
|
||||||
|
for i, index := range indexes {
|
||||||
|
if meta, ok := metadata[i]; ok {
|
||||||
|
selected = append(selected, index)
|
||||||
|
values = append(values, meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
members, err := e.CompositeObjectsAt(page, path, selected)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, member := range members {
|
||||||
|
if err := e.putCompositeRichTextMetadata(page, member.Object, values[i], -1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// prepareRichText 构造独立组合资源和不含字体编号的编辑标引
|
// prepareRichText 构造独立组合资源和不含字体编号的编辑标引
|
||||||
// 入参: box 页面或局部编辑框, value 富文本
|
// 入参: box 页面或局部编辑框, value 富文本
|
||||||
// 返回: GraphicObject 标准组合, richTextMetadata 标引, error 准备错误
|
// 返回: GraphicObject 标准组合, richTextMetadata 标引, error 准备错误
|
||||||
@@ -385,9 +550,15 @@ func (e *Editor) richTextEntry(page int, id string) (richTextEntry, error) {
|
|||||||
// 返回: *richTextIndex 只读缓存
|
// 返回: *richTextIndex 只读缓存
|
||||||
func (e *Editor) richTextMetadataIndex() *richTextIndex {
|
func (e *Editor) richTextMetadataIndex() *richTextIndex {
|
||||||
if index := e.richIndex; index != nil && index.source == e.source && index.revision == e.revision {
|
if index := e.richIndex; index != nil && index.source == e.source && index.revision == e.revision {
|
||||||
|
valid := len(index.remaps) == len(e.structureRemaps)
|
||||||
|
for page, remaps := range e.structureRemaps {
|
||||||
|
valid = valid && index.remaps[page] == len(remaps)
|
||||||
|
}
|
||||||
|
if valid {
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
index := &richTextIndex{source: e.source, revision: e.revision, entries: make(map[string]richTextEntry)}
|
}
|
||||||
|
index := &richTextIndex{source: e.source, revision: e.revision, remaps: e.structureRemapStarts(), entries: make(map[string]richTextEntry)}
|
||||||
e.richIndex = index
|
e.richIndex = index
|
||||||
if e.source == nil {
|
if e.source == nil {
|
||||||
return index
|
return index
|
||||||
@@ -434,6 +605,17 @@ func (e *Editor) richTextMetadataIndex() *richTextIndex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for page, remaps := range e.structureRemaps {
|
||||||
|
for _, ids := range remaps {
|
||||||
|
for from, to := range ids {
|
||||||
|
if _, exists := index.entries[page+"/"+to]; !exists {
|
||||||
|
if entry, ok := index.entries[page+"/"+from]; ok {
|
||||||
|
index.entries[page+"/"+to] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,7 +629,26 @@ func (e *Editor) putRichTextMetadata(page int, id string, meta richTextMetadata,
|
|||||||
}
|
}
|
||||||
root := StructureNode{Name: "RichText", Namespace: richTextNamespace, Text: string(data)}
|
root := StructureNode{Name: "RichText", Namespace: richTextNamespace, Text: string(data)}
|
||||||
if index >= 0 {
|
if index >= 0 {
|
||||||
return e.UpdateStructureNode(index, nil, root)
|
separate := false
|
||||||
|
err := e.editStructure(index, func(data []byte, node *editorXML, tag CustomTag) ([]byte, error) {
|
||||||
|
updated, err := rewriteStructureReferences(context.Background(), data, tag, func(pageID string, ids []string) (string, []string, error) {
|
||||||
|
var remaining []string
|
||||||
|
for _, objectID := range ids {
|
||||||
|
if editorResourceID(pageID) != editorResourceID(e.pages[page].ID) || editorResourceID(objectID) != editorResourceID(id) {
|
||||||
|
remaining = append(remaining, objectID)
|
||||||
|
separate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pageID, remaining, nil
|
||||||
|
})
|
||||||
|
if err != nil || separate {
|
||||||
|
return updated, err
|
||||||
|
}
|
||||||
|
return updateStructureNodeXML(data, node, root)
|
||||||
|
})
|
||||||
|
if err != nil || !separate {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
root.Children = []StructureNode{{Name: "ObjectRef", Namespace: richTextNamespace, Attributes: []StructureAttribute{{Name: "PageRef", Value: e.pages[page].ID}}, Text: id}}
|
root.Children = []StructureNode{{Name: "ObjectRef", Namespace: richTextNamespace, Attributes: []StructureAttribute{{Name: "PageRef", Value: e.pages[page].ID}}, Text: id}}
|
||||||
_, err = e.AddStructure(CustomTag{TypeID: richTextTagType, NameSpace: richTextNamespace}, root)
|
_, err = e.AddStructure(CustomTag{TypeID: richTextTagType, NameSpace: richTextNamespace}, root)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -337,7 +338,7 @@ func editorStructureIndexContext(ctx context.Context, base *editorSource, parts
|
|||||||
if base.document.CustomTags.Path == "" {
|
if base.document.CustomTags.Path == "" {
|
||||||
container := root.child("CustomTags")
|
container := root.child("CustomTags")
|
||||||
if container == nil || len(container.children) == 0 {
|
if container == nil || len(container.children) == 0 {
|
||||||
name = packageAvailableName(base.reader, nil, base.reader.RootDir+"/CustomTags.xml")
|
name = packageAvailableName(base.reader, nil, path.Join(base.reader.RootDir, "CustomTags.xml"))
|
||||||
data, err = root.containerXML("CustomTags", nil, nil)
|
data, err = root.containerXML("CustomTags", nil, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
root, err = parseEditorXMLContext(ctx, data)
|
root, err = parseEditorXMLContext(ctx, data)
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ func (e *Editor) UpdateTextContent(page int, id, value string, style TextStyle)
|
|||||||
|
|
||||||
// RewriteText 保留原文的字形定位,不修改对象样式或文档
|
// RewriteText 保留原文的字形定位,不修改对象样式或文档
|
||||||
// 等长替换保留各字原位;单段增删保留前缀和后缀步进,新增字符沿用局部字距
|
// 等长替换保留各字原位;单段增删保留前缀和后缀步进,新增字符沿用局部字距
|
||||||
// 方向、缩放和修饰原样保留;复杂布局仅支持等长替换,合并字形需显式重排
|
// 方向、缩放和修饰原样保留;单段纵排沿用现有纵向步进,其他复杂布局仅支持等长替换,合并字形需显式重排
|
||||||
// 首段缺失坐标仅在临时校验中恢复,不改写源坐标
|
// 首段缺失坐标仅在临时校验中恢复,不改写源坐标
|
||||||
// 入参: obj 原文字对象, value 新内容
|
// 入参: obj 原文字对象, value 新内容
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
@@ -210,7 +210,8 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
old, next := []rune(obj.Text()), []rune(value)
|
old, next := []rune(obj.Text()), []rune(value)
|
||||||
complex := obj.ReadDirection != 0 || obj.CharDirection != 0 || len(obj.CGTransform) != 0 || obj.VScale != 0 || obj.Decoration != ""
|
vertical := (obj.ReadDirection == 90 || obj.ReadDirection == 270) && obj.CharDirection == 0 && len(obj.TextCode) == 1 && obj.TextCode[0].DeltaY != ""
|
||||||
|
complex := obj.ReadDirection != 0 && !vertical || obj.CharDirection != 0 || len(obj.CGTransform) != 0 || obj.VScale != 0 || obj.Decoration != ""
|
||||||
if complex && len(old) != len(next) {
|
if complex && len(old) != len(next) {
|
||||||
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing positioned text length requires explicit layout")}
|
return &EditError{Code: EditLayoutRequired, Err: fmt.Errorf("changing positioned text length requires explicit layout")}
|
||||||
}
|
}
|
||||||
@@ -279,7 +280,12 @@ func (e *Editor) RewriteText(obj *TextObject, value string) error {
|
|||||||
if obj.HScale != 0 {
|
if obj.HScale != 0 {
|
||||||
unit *= obj.HScale
|
unit *= obj.HScale
|
||||||
}
|
}
|
||||||
advance := func(char rune) float64 { return float64(sfnt.GlyphAdvance(sfnt.GlyphIndex(char))) * unit }
|
advance := func(char rune) float64 {
|
||||||
|
if vertical {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(sfnt.GlyphAdvance(sfnt.GlyphIndex(char))) * unit
|
||||||
|
}
|
||||||
if codes[i].DeltaX != "" {
|
if codes[i].DeltaX != "" {
|
||||||
codes[i].DeltaX = rewriteTextDeltas(codes[i].DeltaX, runes, replacement, from, to, advance)
|
codes[i].DeltaX = rewriteTextDeltas(codes[i].DeltaX, runes, replacement, from, to, advance)
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -313,11 +314,26 @@ func NewPDFImport(ctx context.Context, input *Input, options PDFImportOptions) (
|
|||||||
return nil, fmt.Errorf("import PDF output intents: %w", err)
|
return nil, fmt.Errorf("import PDF output intents: %w", err)
|
||||||
}
|
}
|
||||||
if len(outputIntents) != 0 {
|
if len(outputIntents) != 0 {
|
||||||
|
var profile []byte
|
||||||
|
applicable := true
|
||||||
|
for _, intent := range outputIntents {
|
||||||
|
if intent.Subtype != "GTS_PDFA1" && intent.Subtype != "GTS_PDFX" || intent.Space == nil || len(intent.Profile) == 0 || profile != nil && !bytes.Equal(profile, intent.Profile) {
|
||||||
|
applicable = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
profile = intent.Profile
|
||||||
|
}
|
||||||
|
if applicable {
|
||||||
|
if err := reader.SetDeviceColorProfile(profile); err != nil {
|
||||||
|
return nil, fmt.Errorf("import PDF device color profile: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if options.Strict {
|
if options.Strict {
|
||||||
return nil, &pdfgo.UnsupportedError{Feature: "PDF output intent conversion"}
|
return nil, &pdfgo.UnsupportedError{Feature: "PDF output intent conversion"}
|
||||||
}
|
}
|
||||||
importer.report.Warnings = append(importer.report.Warnings, pdfgo.Diagnostic{Message: "PDF output intents preserved as metadata; not applied to rendering"})
|
importer.report.Warnings = append(importer.report.Warnings, pdfgo.Diagnostic{Message: "PDF output intents preserved as metadata; not applied to rendering"})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
importer.pageIndexes = make(map[pdfgo.Reference]int)
|
importer.pageIndexes = make(map[pdfgo.Reference]int)
|
||||||
importer.resolveFile = options.ResolveFile
|
importer.resolveFile = options.ResolveFile
|
||||||
importer.resolveReference = options.ResolveReference
|
importer.resolveReference = options.ResolveReference
|
||||||
@@ -475,7 +491,6 @@ func (p *PDFImport) ConvertPage(ctx context.Context, index int) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ready[index] = true
|
p.ready[index] = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-13
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@@ -76,18 +77,22 @@ func (p *PrintDocument) renderPrintSVG(ctx context.Context, index int, writer io
|
|||||||
if writer == nil {
|
if writer == nil {
|
||||||
return fmt.Errorf("missing print writer")
|
return fmt.Errorf("missing print writer")
|
||||||
}
|
}
|
||||||
var out bytes.Buffer
|
out := bufio.NewWriter(convertWriter{ctx: ctx, writer: writer, count: new(int64)})
|
||||||
fmt.Fprintf(&out, `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%gmm" height="%gmm" viewBox="0 0 %g %g"><rect width="100%%" height="100%%" fill="white"/>`, side.Width, side.Height, side.Width, side.Height)
|
fmt.Fprintf(out, `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%gmm" height="%gmm" viewBox="0 0 %g %g"><rect width="100%%" height="100%%" fill="white"/>`, side.Width, side.Height, side.Width, side.Height)
|
||||||
if p.options.Raster || p.options.Grayscale {
|
if p.options.Raster || p.options.Grayscale {
|
||||||
img, err := p.renderPrintImage(ctx, index, p.options.DPI)
|
img, err := p.renderPrintImage(ctx, index, p.options.DPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var data bytes.Buffer
|
fmt.Fprintf(out, `<image width="%g" height="%g" xlink:href="data:image/png;base64,`, side.Width, side.Height)
|
||||||
if err := png.Encode(&data, img); err != nil {
|
encoder := base64.NewEncoder(base64.StdEncoding, out)
|
||||||
|
if err := png.Encode(encoder, img); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&out, `<image width="%g" height="%g" xlink:href="data:image/png;base64,%s"/>`, side.Width, side.Height, base64.StdEncoding.EncodeToString(data.Bytes()))
|
if err := encoder.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out.WriteString(`"/>`)
|
||||||
} else {
|
} else {
|
||||||
for i, placement := range side.Placements {
|
for i, placement := range side.Placements {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
@@ -97,23 +102,26 @@ func (p *PrintDocument) renderPrintSVG(ctx context.Context, index int, writer io
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var data bytes.Buffer
|
c, m := placement.Clip, placement.Transform
|
||||||
if err := p.renderer.RenderToSVG(page, &data); err != nil {
|
fmt.Fprintf(out, `<defs><clipPath id="print%d"><rect x="%g" y="%g" width="%g" height="%g"/></clipPath></defs><g clip-path="url(#print%d)"><image width="%g" height="%g" transform="matrix(%g %g %g %g %g %g)" xlink:href="data:image/svg+xml;base64,`, i, c.X, c.Y, c.W, c.H, i, placement.Width, placement.Height, m[0], m[1], m[2], m[3], m[4], m[5])
|
||||||
|
encoder := base64.NewEncoder(base64.StdEncoding, out)
|
||||||
|
if err := p.renderer.RenderToSVG(page, encoder); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c, m := placement.Clip, placement.Transform
|
if err := encoder.Close(); err != nil {
|
||||||
fmt.Fprintf(&out, `<defs><clipPath id="print%d"><rect x="%g" y="%g" width="%g" height="%g"/></clipPath></defs><g clip-path="url(#print%d)"><image width="%g" height="%g" transform="matrix(%g %g %g %g %g %g)" xlink:href="data:image/svg+xml;base64,%s"/></g>`, i, c.X, c.Y, c.W, c.H, i, placement.Width, placement.Height, m[0], m[1], m[2], m[3], m[4], m[5], base64.StdEncoding.EncodeToString(data.Bytes()))
|
return err
|
||||||
|
}
|
||||||
|
out.WriteString(`"/></g>`)
|
||||||
}
|
}
|
||||||
for _, line := range p.printLines(side) {
|
for _, line := range p.printLines(side) {
|
||||||
fmt.Fprintf(&out, `<path d="M %g %g L %g %g" fill="none" stroke="black" stroke-width="0.15"/>`, line[0], line[1], line[2], line[3])
|
fmt.Fprintf(out, `<path d="M %g %g L %g %g" fill="none" stroke="black" stroke-width="0.15"/>`, line[0], line[1], line[2], line[3])
|
||||||
}
|
}
|
||||||
if side.Label != "" {
|
if side.Label != "" {
|
||||||
fmt.Fprintf(&out, `<text x="%g" y="%g" font-family="sans-serif" font-size="2.8" fill="black">%s</text>`, side.LabelX, side.LabelY, html.EscapeString(side.Label))
|
fmt.Fprintf(out, `<text x="%g" y="%g" font-family="sans-serif" font-size="2.8" fill="black">%s</text>`, side.LabelX, side.LabelY, html.EscapeString(side.Label))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.WriteString(`</svg>`)
|
out.WriteString(`</svg>`)
|
||||||
_, err = io.Copy(convertWriter{ctx: ctx, writer: writer, count: new(int64)}, &out)
|
return out.Flush()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderImage 按指定DPI输出一个纸面的独立白底图像,dpi为0时采用任务配置
|
// RenderImage 按指定DPI输出一个纸面的独立白底图像,dpi为0时采用任务配置
|
||||||
|
|||||||
+79
-15
@@ -20,6 +20,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@@ -53,7 +54,8 @@ type TextSpan struct {
|
|||||||
Matrix [6]float64 `json:"matrix"`
|
Matrix [6]float64 `json:"matrix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextMatch 文字匹配结果,ID为对象标识,Run为文本对象索引,Start和End为原文Unicode字符区间
|
// TextMatch 文字匹配结果,ID和Run定位首个对象,Start和End为该对象内的Unicode字符区间
|
||||||
|
// 跨对象匹配通过Parts保留各对象区间,Text和Boxes包含完整命中
|
||||||
type TextMatch struct {
|
type TextMatch struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Run int `json:"run"`
|
Run int `json:"run"`
|
||||||
@@ -63,6 +65,15 @@ type TextMatch struct {
|
|||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
After string `json:"after"`
|
After string `json:"after"`
|
||||||
Boxes []Box `json:"boxes"`
|
Boxes []Box `json:"boxes"`
|
||||||
|
Parts []TextMatchPart `json:"parts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextMatchPart 跨对象匹配中的原文区间,Start和End为对象内Unicode字符偏移
|
||||||
|
type TextMatchPart struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Run int `json:"run"`
|
||||||
|
Start int `json:"start"`
|
||||||
|
End int `json:"end"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token 读取文字页面所需的XML节点
|
// Token 读取文字页面所需的XML节点
|
||||||
@@ -172,7 +183,7 @@ func (p *PageText) WriteTo(writer io.Writer) (int64, error) {
|
|||||||
return p.writeTo(writer, false)
|
return p.writeTo(writer, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search 按原文进行忽略大小写的字面匹配,不跨文本对象拼接或识别图像
|
// Search 按原文进行忽略大小写的字面匹配,拼接同一行紧邻的文本对象,不识别图像
|
||||||
// 入参: query 搜索文字
|
// 入参: query 搜索文字
|
||||||
// 返回: []TextMatch 匹配结果及上下文
|
// 返回: []TextMatch 匹配结果及上下文
|
||||||
func (p *PageText) Search(query string) []TextMatch {
|
func (p *PageText) Search(query string) []TextMatch {
|
||||||
@@ -184,7 +195,7 @@ func (p *PageText) Search(query string) []TextMatch {
|
|||||||
return matches
|
return matches
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchEach 逐项返回忽略大小写的字面匹配,不累积全部结果,在对象与匹配之间检查取消
|
// SearchEach 逐项返回忽略大小写的字面匹配,拼接同一行紧邻对象,在索引构建及匹配之间检查取消
|
||||||
// 入参: ctx 取消上下文, query 搜索文字, visit 匹配访问函数,可返回错误停止
|
// 入参: ctx 取消上下文, query 搜索文字, visit 匹配访问函数,可返回错误停止
|
||||||
// 返回: error 取消或访问错误
|
// 返回: error 取消或访问错误
|
||||||
func (p *PageText) SearchEach(ctx context.Context, query string, visit func(TextMatch) error) error {
|
func (p *PageText) SearchEach(ctx context.Context, query string, visit func(TextMatch) error) error {
|
||||||
@@ -268,11 +279,39 @@ func (p *PageText) searchEach(ctx context.Context, query string, visit func(Text
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
length := utf8.RuneCountInString(query)
|
length := utf8.RuneCountInString(query)
|
||||||
for index, run := range p.Runs {
|
for first := 0; first < len(p.Runs); {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
text := foldText(run.Text)
|
last := first + 1
|
||||||
|
for last < len(p.Runs) && adjacentTextRuns(p.Runs[last-1], p.Runs[last]) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
last++
|
||||||
|
}
|
||||||
|
var source strings.Builder
|
||||||
|
offsets := make([]int, last-first+1)
|
||||||
|
for index := first; index < last; index++ {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
source.WriteString(p.Runs[index].Text)
|
||||||
|
offsets[index-first+1] = offsets[index-first] + utf8.RuneCountInString(p.Runs[index].Text)
|
||||||
|
}
|
||||||
|
var folded strings.Builder
|
||||||
|
folded.Grow(source.Len())
|
||||||
|
polled := 0
|
||||||
|
for _, char := range source.String() {
|
||||||
|
if polled%256 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
polled++
|
||||||
|
folded.WriteRune(foldRune(char))
|
||||||
|
}
|
||||||
|
text := folded.String()
|
||||||
var runes []rune
|
var runes []rune
|
||||||
codeOffset := 0
|
codeOffset := 0
|
||||||
for offset := 0; offset < len(text); {
|
for offset := 0; offset < len(text); {
|
||||||
@@ -284,29 +323,33 @@ func (p *PageText) searchEach(ctx context.Context, query string, visit func(Text
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if runes == nil {
|
if runes == nil {
|
||||||
runes = []rune(run.Text)
|
runes = []rune(source.String())
|
||||||
}
|
}
|
||||||
start := codeOffset + utf8.RuneCountInString(text[offset:offset+found])
|
start := codeOffset + utf8.RuneCountInString(text[offset:offset+found])
|
||||||
offset += found
|
offset += found
|
||||||
end := start + length
|
end := start + length
|
||||||
match := TextMatch{
|
begin := sort.Search(len(offsets)-1, func(i int) bool { return offsets[i+1] > start })
|
||||||
ID: run.ID,
|
match := TextMatch{ID: p.Runs[first+begin].ID, Run: first + begin, Start: start - offsets[begin], End: min(end, offsets[begin+1]) - offsets[begin],
|
||||||
Run: index, Start: start, End: end,
|
Before: string(runes[max(0, start-16):start]), Text: string(runes[start:end]), After: string(runes[end:min(len(runes), end+16)])}
|
||||||
Before: string(runes[max(0, start-16):start]),
|
for index := begin; index < last-first && offsets[index] < end; index++ {
|
||||||
Text: string(runes[start:end]),
|
run := p.Runs[first+index]
|
||||||
After: string(runes[end:min(len(runes), end+16)]),
|
from, to := max(start, offsets[index])-offsets[index], min(end, offsets[index+1])-offsets[index]
|
||||||
|
if end > offsets[begin+1] {
|
||||||
|
match.Parts = append(match.Parts, TextMatchPart{ID: run.ID, Run: first + index, Start: from, End: to})
|
||||||
}
|
}
|
||||||
for _, box := range run.Boxes[min(start, len(run.Boxes)):min(end, len(run.Boxes))] {
|
for _, box := range run.Boxes[min(from, len(run.Boxes)):min(to, len(run.Boxes))] {
|
||||||
if box.W > 0 && box.H > 0 && (len(match.Boxes) == 0 || match.Boxes[len(match.Boxes)-1] != box) {
|
if box.W > 0 && box.H > 0 && (len(match.Boxes) == 0 || match.Boxes[len(match.Boxes)-1] != box) {
|
||||||
match.Boxes = append(match.Boxes, box)
|
match.Boxes = append(match.Boxes, box)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if err := visit(match); err != nil {
|
if err := visit(match); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
offset += len(query)
|
offset += len(query)
|
||||||
codeOffset = end
|
codeOffset = end
|
||||||
}
|
}
|
||||||
|
first = last
|
||||||
}
|
}
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -315,7 +358,13 @@ func (p *PageText) searchEach(ctx context.Context, query string, visit func(Text
|
|||||||
// 入参: text 原文
|
// 入参: text 原文
|
||||||
// 返回: string 匹配用文字
|
// 返回: string 匹配用文字
|
||||||
func foldText(text string) string {
|
func foldText(text string) string {
|
||||||
return strings.Map(func(char rune) rune {
|
return strings.Map(foldRune, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// foldRune 统一单个Unicode大小写等价字符
|
||||||
|
// 入参: char 原字符
|
||||||
|
// 返回: rune 匹配字符
|
||||||
|
func foldRune(char rune) rune {
|
||||||
if char < utf8.RuneSelf {
|
if char < utf8.RuneSelf {
|
||||||
if char >= 'a' && char <= 'z' {
|
if char >= 'a' && char <= 'z' {
|
||||||
return char - ('a' - 'A')
|
return char - ('a' - 'A')
|
||||||
@@ -327,7 +376,22 @@ func foldText(text string) string {
|
|||||||
folded = min(folded, next)
|
folded = min(folded, next)
|
||||||
}
|
}
|
||||||
return folded
|
return folded
|
||||||
}, text)
|
}
|
||||||
|
|
||||||
|
// adjacentTextRuns 判断绘制顺序相邻的文字是否在同一行连续,缺少定位时保持独立
|
||||||
|
// 入参: before 前对象, after 后对象
|
||||||
|
// 返回: bool 是否可连续搜索
|
||||||
|
func adjacentTextRuns(before, after TextRun) bool {
|
||||||
|
if before.Text == "" || after.Text == "" || len(before.Boxes) != utf8.RuneCountInString(before.Text) || len(after.Boxes) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
a, b := before.Boxes[len(before.Boxes)-1], after.Boxes[0]
|
||||||
|
size := math.Min(a.H, b.H)
|
||||||
|
if size <= 0 || a.W <= 0 || b.W <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
gap := b.X - a.X - a.W
|
||||||
|
return math.Abs(a.Y+a.H/2-b.Y-b.H/2) <= size*.25 && gap >= -size*.15 && gap <= size*.35 && b.X > a.X
|
||||||
}
|
}
|
||||||
|
|
||||||
// addRun 收集原始文本,字形索引不替代Unicode原文
|
// addRun 收集原始文本,字形索引不替代Unicode原文
|
||||||
|
|||||||
Reference in new issue
Block a user