mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
This commit is contained in:
1 parent
9f2ef7e212
commit
2ffbf573ba
11 files changed
+497
-128
No files matched your search
@@ -264,6 +264,12 @@
|
|||||||
<div class="form-actions"><div id="infoStatus" class="form-status" role="status"></div><button id="infoCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
<div class="form-actions"><div id="infoStatus" class="form-status" role="status"></div><button id="infoCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<dialog id="annotationNote" class="form-dialog" aria-label="注解">
|
||||||
|
<form method="dialog">
|
||||||
|
<div id="annotationDetail" class="annotation-detail"></div>
|
||||||
|
<div class="form-actions"><button class="button" type="submit" autofocus>关闭</button></div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
<dialog id="pagePanel" class="form-dialog" aria-label="页面设置">
|
<dialog id="pagePanel" class="form-dialog" aria-label="页面设置">
|
||||||
<form id="pageForm">
|
<form id="pageForm">
|
||||||
<div id="pageRangeRow" class="form-row" hidden><span>范围</span><output id="pageRange"></output></div>
|
<div id="pageRangeRow" class="form-row" hidden><span>范围</span><output id="pageRange"></output></div>
|
||||||
@@ -444,10 +450,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="signatureList" class="signature-list"></div>
|
<div id="signatureList" class="signature-list"></div>
|
||||||
</section>
|
</section>
|
||||||
<section id="annotationPanel" class="annotation-panel" aria-labelledby="annotationPanelTitle" hidden>
|
|
||||||
<div id="annotationPanelTitle" class="panel-title">注解</div>
|
|
||||||
<div id="annotationList" class="annotation-list"></div>
|
|
||||||
</section>
|
|
||||||
<section class="doc-font-panel" aria-labelledby="docFontPanelTitle">
|
<section class="doc-font-panel" aria-labelledby="docFontPanelTitle">
|
||||||
<div class="panel-row">
|
<div class="panel-row">
|
||||||
<div id="docFontPanelTitle" class="panel-title compact">OFD 字体</div>
|
<div id="docFontPanelTitle" class="panel-title compact">OFD 字体</div>
|
||||||
|
|||||||
+27
-16
@@ -1777,6 +1777,32 @@ button.outline-link:hover {
|
|||||||
background: rgba(15, 123, 104, 0.08);
|
background: rgba(15, 123, 104, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.annotation-note {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--line-strong);
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotation-detail {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editing .annotation-note {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.page-placeholder {
|
.page-placeholder {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -1845,7 +1871,6 @@ button.outline-link:hover {
|
|||||||
.font-panel,
|
.font-panel,
|
||||||
.doc-font-panel,
|
.doc-font-panel,
|
||||||
.attachment-panel,
|
.attachment-panel,
|
||||||
.annotation-panel,
|
|
||||||
.signature-panel {
|
.signature-panel {
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
@@ -1898,14 +1923,12 @@ button.outline-link:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.doc-font-list,
|
.doc-font-list,
|
||||||
.annotation-list,
|
|
||||||
.signature-list {
|
.signature-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-font-row,
|
.doc-font-row,
|
||||||
.annotation-row,
|
|
||||||
.signature-row {
|
.signature-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -1918,7 +1941,6 @@ button.outline-link:hover {
|
|||||||
.page-list-item:not(:disabled):hover,
|
.page-list-item:not(:disabled):hover,
|
||||||
.doc-font-row:hover,
|
.doc-font-row:hover,
|
||||||
.signature-row:hover,
|
.signature-row:hover,
|
||||||
.annotation-row:hover,
|
|
||||||
.font-row:hover {
|
.font-row:hover {
|
||||||
border-color: var(--line-strong);
|
border-color: var(--line-strong);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -1945,7 +1967,6 @@ button.outline-link:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.doc-font-head,
|
.doc-font-head,
|
||||||
.annotation-head,
|
|
||||||
.signature-head {
|
.signature-head {
|
||||||
display: flow-root;
|
display: flow-root;
|
||||||
}
|
}
|
||||||
@@ -1976,11 +1997,6 @@ button.outline-link:hover {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotation-head .info-name-button {
|
|
||||||
color: var(--accent-strong);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-stamp-group {
|
.signature-stamp-group {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -2015,16 +2031,11 @@ button.outline-link:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.doc-font-badges,
|
.doc-font-badges,
|
||||||
.signature-badges,
|
.signature-badges {
|
||||||
.annotation-head > .font-badge {
|
|
||||||
float: right;
|
float: right;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.annotation-row .info-value {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-badge {
|
.font-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+96
-55
@@ -17,6 +17,7 @@ const STATUS = {
|
|||||||
pageExporting: "正在导出单页",
|
pageExporting: "正在导出单页",
|
||||||
};
|
};
|
||||||
const wasmRequests = new Map();
|
const wasmRequests = new Map();
|
||||||
|
const metaContents = new WeakMap();
|
||||||
|
|
||||||
let wasmPromise = null;
|
let wasmPromise = null;
|
||||||
let wasmWorker = null;
|
let wasmWorker = null;
|
||||||
@@ -332,8 +333,8 @@ const el = {
|
|||||||
signaturePanel: document.querySelector("#signaturePanel"),
|
signaturePanel: document.querySelector("#signaturePanel"),
|
||||||
signatureSummary: document.querySelector("#signatureSummary"),
|
signatureSummary: document.querySelector("#signatureSummary"),
|
||||||
signatureList: document.querySelector("#signatureList"),
|
signatureList: document.querySelector("#signatureList"),
|
||||||
annotationPanel: document.querySelector("#annotationPanel"),
|
annotationNote: document.querySelector("#annotationNote"),
|
||||||
annotationList: document.querySelector("#annotationList"),
|
annotationDetail: document.querySelector("#annotationDetail"),
|
||||||
docFontList: document.querySelector("#docFontList"),
|
docFontList: document.querySelector("#docFontList"),
|
||||||
docFontSummary: document.querySelector("#docFontSummary"),
|
docFontSummary: document.querySelector("#docFontSummary"),
|
||||||
availableFontSummary: document.querySelector("#availableFontSummary"),
|
availableFontSummary: document.querySelector("#availableFontSummary"),
|
||||||
@@ -711,7 +712,11 @@ el.objectBoundsForm.addEventListener("submit", async event => {
|
|||||||
const item = canvasEditor.selected;
|
const item = canvasEditor.selected;
|
||||||
const values = [el.objectX, el.objectY, el.objectWidth, el.objectHeight].map(objectDimension);
|
const values = [el.objectX, el.objectY, el.objectWidth, el.objectHeight].map(objectDimension);
|
||||||
let saved;
|
let saved;
|
||||||
if (item.shape) {
|
if (item.oriented) {
|
||||||
|
const [a, b, c, d, e, f] = item.oriented.matrix, determinant = a * d - b * c;
|
||||||
|
const x = values[0] - e, y = values[1] - f;
|
||||||
|
saved = await changeDocument("ofdgoReshapeObject", item, (d * x - c * y) / determinant, (a * y - b * x) / determinant, values[2] / Math.hypot(a, b), values[3] / Math.hypot(c, d), true);
|
||||||
|
} else if (item.shape) {
|
||||||
if (lineShape(item.shape)) { values[2] *= Math.sign(item.geometry.width); values[3] *= Math.sign(item.geometry.height); }
|
if (lineShape(item.shape)) { values[2] *= Math.sign(item.geometry.width); values[3] *= Math.sign(item.geometry.height); }
|
||||||
saved = await changeDocument("ofdgoReshapeObject", item, ...values);
|
saved = await changeDocument("ofdgoReshapeObject", item, ...values);
|
||||||
} else saved = await changeDocument("ofdgoResizeObjects", { ...item, id: canvasEditor.items().map(member => member.id) }, ...values);
|
} else saved = await changeDocument("ofdgoResizeObjects", { ...item, id: canvasEditor.items().map(member => member.id) }, ...values);
|
||||||
@@ -989,7 +994,7 @@ function handleKeyDown(event) {
|
|||||||
|
|
||||||
function formDialogOpen() {
|
function formDialogOpen() {
|
||||||
return el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
return el.exportPanel.open || el.createPanel.open || el.insertPanel.open || el.pagePanel.open || el.paragraphPanel.open || el.importPanel.open || el.infoPanel.open
|
||||||
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.outlinePanel.open || el.objectBoundsPanel.open;
|
|| el.batchPagesPanel.open || el.objectStylePanel.open || el.outlinePanel.open || el.objectBoundsPanel.open || el.annotationNote.open;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setObjectDimension(input, value) {
|
function setObjectDimension(input, value) {
|
||||||
@@ -1004,11 +1009,16 @@ function objectDimension(input) {
|
|||||||
|
|
||||||
function openObjectBounds() {
|
function openObjectBounds() {
|
||||||
const item = canvasEditor.selected;
|
const item = canvasEditor.selected;
|
||||||
const box = item.geometry || selectionBounds((item.items || [item]).map(member => member.bounds || member));
|
let box = item.geometry || selectionBounds((item.items || [item]).map(member => member.bounds || member));
|
||||||
|
if (item.oriented) {
|
||||||
|
const frame = item.oriented, [a, b, c, d, e, f] = frame.matrix;
|
||||||
|
box = { x: a * frame.box.x + c * frame.box.y + e, y: b * frame.box.x + d * frame.box.y + f,
|
||||||
|
width: frame.box.width * Math.hypot(a, b), height: frame.box.height * Math.hypot(c, d) };
|
||||||
|
}
|
||||||
state.boundsOriginal = { x: box.x, y: box.y, width: Math.abs(box.width), height: Math.abs(box.height) };
|
state.boundsOriginal = { x: box.x, y: box.y, width: Math.abs(box.width), height: Math.abs(box.height) };
|
||||||
for (const [input, key] of [[el.objectX,"x"], [el.objectY,"y"], [el.objectWidth,"width"], [el.objectHeight,"height"]]) setObjectDimension(input, state.boundsOriginal[key]);
|
for (const [input, key] of [[el.objectX,"x"], [el.objectY,"y"], [el.objectWidth,"width"], [el.objectHeight,"height"]]) setObjectDimension(input, state.boundsOriginal[key]);
|
||||||
el.objectAspect.checked = true;
|
el.objectAspect.checked = true;
|
||||||
el.objectAspect.disabled = !item.shape && (item.items || [item]).some(member => member.type !== "ImageObject" || member.imageBorder) || lineShape(item.shape) && (!box.width || !box.height);
|
el.objectAspect.disabled = !item.shape && !item.oriented && !canEditObject(item, "stretch") || lineShape(item.shape) && (!box.width || !box.height);
|
||||||
el.objectWidth.disabled = lineShape(item.shape) && !box.width;
|
el.objectWidth.disabled = lineShape(item.shape) && !box.width;
|
||||||
el.objectHeight.disabled = lineShape(item.shape) && !box.height;
|
el.objectHeight.disabled = lineShape(item.shape) && !box.height;
|
||||||
el.objectBoundsStatus.textContent = "";
|
el.objectBoundsStatus.textContent = "";
|
||||||
@@ -1026,8 +1036,8 @@ function openObjectStyle() {
|
|||||||
const items = canvasEditor.items();
|
const items = canvasEditor.items();
|
||||||
if (!items.length) return;
|
if (!items.length) return;
|
||||||
const ordered = [...items].sort((a, b) => a.position - b.position);
|
const ordered = [...items].sort((a, b) => a.position - b.position);
|
||||||
el.groupObjectsButton.disabled = Boolean(state.composite) || items.length < 2 || !ordered.every((item, index) => canEditObject(item, "copy") && canEditObject(item, "order") && item.container === ordered[0].container && item.position === ordered[0].position + index);
|
el.groupObjectsButton.disabled = items.length < 2 || !ordered.every((item, index) => canEditObject(item, "copy") && canEditObject(item, "order") && item.container === ordered[0].container && item.position === ordered[0].position + index);
|
||||||
el.ungroupObjectButton.disabled = Boolean(state.composite) || items.length !== 1 || !canEditObject(items[0], "ungroup");
|
el.ungroupObjectButton.disabled = items.length !== 1 || !canEditObject(items[0], "ungroup");
|
||||||
el.copyStyleButton.disabled = items.length !== 1 || !canCopyStyle(items[0]);
|
el.copyStyleButton.disabled = items.length !== 1 || !canCopyStyle(items[0]);
|
||||||
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
|
el.pasteStyleButton.disabled = !state.styleClipboard || !items.every(item => item.type === state.styleClipboard && canCopyStyle(item));
|
||||||
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
const shared = (key, fallback) => items.every(item => (item[key] ?? fallback) === (items[0][key] ?? fallback)) ? items[0][key] ?? fallback : null;
|
||||||
@@ -1248,6 +1258,7 @@ async function toggleEditor() {
|
|||||||
if (!state.doc || !state.ready || document.body.hasAttribute("aria-busy") || formDialogOpen()) return;
|
if (!state.doc || !state.ready || document.body.hasAttribute("aria-busy") || formDialogOpen()) return;
|
||||||
if (state.editorInfo) {
|
if (state.editorInfo) {
|
||||||
if (!await canvasEditor.commitText() || !await canvasEditor.commitCrop()) return;
|
if (!await canvasEditor.commitText() || !await canvasEditor.commitCrop()) return;
|
||||||
|
const anchor = scaleAnchor(0);
|
||||||
state.editing = !state.editing;
|
state.editing = !state.editing;
|
||||||
resetCompositeScope();
|
resetCompositeScope();
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
@@ -1260,10 +1271,11 @@ async function toggleEditor() {
|
|||||||
renderOutlines(false);
|
renderOutlines(false);
|
||||||
renderPageList();
|
renderPageList();
|
||||||
applyFit(false);
|
applyFit(false);
|
||||||
|
restoreScaleAnchor(anchor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { pageIndex, fitMode, scale } = state;
|
const { pageIndex, fitMode, scale } = state;
|
||||||
const { scrollLeft, scrollTop } = el.viewerPanel;
|
const anchor = scaleAnchor(0);
|
||||||
const openSeq = ++state.openSeq;
|
const openSeq = ++state.openSeq;
|
||||||
setBusy(true, "正在准备编辑", null, "正在准备编辑");
|
setBusy(true, "正在准备编辑", null, "正在准备编辑");
|
||||||
try {
|
try {
|
||||||
@@ -1281,7 +1293,7 @@ async function toggleEditor() {
|
|||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
setPan(false);
|
setPan(false);
|
||||||
await openDocument({ doc, openSeq, skipAutoFonts: true, pageIndex, fitMode, scale,
|
await openDocument({ doc, openSeq, skipAutoFonts: true, pageIndex, fitMode, scale,
|
||||||
keepPreview: true, previewScroll: { scrollLeft, scrollTop } });
|
keepPreview: true, previewAnchor: anchor });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) showError(err, false);
|
if (openSeq === state.openSeq) showError(err, false);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1887,7 +1899,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor",
|
ofdgoAlignObject: "align", ofdgoAlignObjects: "align", ofdgoDistributeObjects: "distribute", ofdgoStyleObjects: "style", ofdgoUpdatePathStyle: "paint", ofdgoCompositeTextColor: "textColor",
|
||||||
ofdgoUpdateText: "text", ofdgoStyleText: "textStyle", ofdgoCropImage: "crop", ofdgoLayoutText: "layout", ofdgoFitImage: "fit", ofdgoResetCompositeCrop: "resetCrop",
|
ofdgoUpdateText: "text", ofdgoStyleText: "textStyle", ofdgoCropImage: "crop", ofdgoLayoutText: "layout", ofdgoFitImage: "fit", ofdgoResetCompositeCrop: "resetCrop",
|
||||||
ofdgoReshapeObject: "reshape", ofdgoReshapeLine: "line", ofdgoEraseObjects: "erase", ofdgoEraseObjectsPath: "erasePath", ofdgoPasteStyle: "pasteStyle",
|
ofdgoReshapeObject: "reshape", ofdgoReshapeLine: "line", ofdgoEraseObjects: "erase", ofdgoEraseObjectsPath: "erasePath", ofdgoPasteStyle: "pasteStyle",
|
||||||
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order" }[name];
|
ofdgoDeleteObject: "delete", ofdgoDeleteObjects: "delete", ofdgoCopyObjects: "copy", ofdgoOrderObjects: "order", ofdgoGroupObjects: "group" }[name];
|
||||||
const members = item?.items || (item ? [item] : []);
|
const members = item?.items || (item ? [item] : []);
|
||||||
const scoped = scope && members.length && members.every(member => member.scoped);
|
const scoped = scope && members.length && members.every(member => member.scoped);
|
||||||
const inserting = ["ofdgoInsertShape", "ofdgoInsertText", "ofdgoInsertImage"].includes(name);
|
const inserting = ["ofdgoInsertShape", "ofdgoInsertText", "ofdgoInsertImage"].includes(name);
|
||||||
@@ -1903,7 +1915,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const view = name === "ofdgoUndo" ? state.editorViews.get(revision) : name === "ofdgoRedo" ? state.editorViews.get(doc.revision) : null;
|
const view = name === "ofdgoUndo" ? state.editorViews.get(revision) : name === "ofdgoRedo" ? state.editorViews.get(doc.revision) : null;
|
||||||
const reindex = !!scope && (scoped && ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoCopyObjects", "ofdgoOrderObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath"].includes(name)
|
const reindex = !!scope && (scoped && ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoCopyObjects", "ofdgoOrderObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath", "ofdgoGroupObjects"].includes(name)
|
||||||
|| inserting || name === "ofdgoPasteObjects" && !!args[4]);
|
|| inserting || name === "ofdgoPasteObjects" && !!args[4]);
|
||||||
const reindexed = restoring ? view?.reindex && view.before : reindex && before;
|
const reindexed = restoring ? view?.reindex && view.before : reindex && before;
|
||||||
const clipboard = state.objectClipboard;
|
const clipboard = state.objectClipboard;
|
||||||
@@ -2031,7 +2043,7 @@ async function refreshEditorPage(doc, index, openSeq, clearSelection = false) {
|
|||||||
} finally {
|
} finally {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
releasePageResources();
|
releasePageResources();
|
||||||
renderMeta();
|
renderMeta(true);
|
||||||
updateControls();
|
updateControls();
|
||||||
loadDocumentDetails(openSeq);
|
loadDocumentDetails(openSeq);
|
||||||
for (const page of doc.pages) {
|
for (const page of doc.pages) {
|
||||||
@@ -2521,6 +2533,7 @@ async function openOFD(file) {
|
|||||||
el.pagePanel.close();
|
el.pagePanel.close();
|
||||||
el.paragraphPanel.close();
|
el.paragraphPanel.close();
|
||||||
el.infoPanel.close();
|
el.infoPanel.close();
|
||||||
|
el.annotationNote.close();
|
||||||
el.importPanel.close();
|
el.importPanel.close();
|
||||||
el.batchPagesPanel.close();
|
el.batchPagesPanel.close();
|
||||||
el.objectStylePanel.close();
|
el.objectStylePanel.close();
|
||||||
@@ -2989,7 +3002,7 @@ async function openDocument(options = {}) {
|
|||||||
if (options.keepPreview || options.resetScroll || el.outlineList.childElementCount === 0) {
|
if (options.keepPreview || options.resetScroll || el.outlineList.childElementCount === 0) {
|
||||||
renderOutlines(!options.keepPreview);
|
renderOutlines(!options.keepPreview);
|
||||||
}
|
}
|
||||||
renderMeta();
|
renderMeta(options.keepPreview);
|
||||||
renderPageFlow();
|
renderPageFlow();
|
||||||
if (options.keepPreview) {
|
if (options.keepPreview) {
|
||||||
for (const [index, page] of state.pageCache) {
|
for (const [index, page] of state.pageCache) {
|
||||||
@@ -3007,7 +3020,9 @@ async function openDocument(options = {}) {
|
|||||||
}
|
}
|
||||||
applyFit(false);
|
applyFit(false);
|
||||||
if (options.keepPreview) {
|
if (options.keepPreview) {
|
||||||
if (options.previewScroll) {
|
if (options.previewAnchor) {
|
||||||
|
restoreScaleAnchor(options.previewAnchor);
|
||||||
|
} else if (options.previewScroll) {
|
||||||
Object.assign(el.viewerPanel, options.previewScroll);
|
Object.assign(el.viewerPanel, options.previewScroll);
|
||||||
} else {
|
} else {
|
||||||
scrollToPage(pageIndex);
|
scrollToPage(pageIndex);
|
||||||
@@ -3045,11 +3060,13 @@ async function loadDocumentDetails(openSeq) {
|
|||||||
try {
|
try {
|
||||||
const info = await callWASM("ofdgoDocumentInfo");
|
const info = await callWASM("ofdgoDocumentInfo");
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
Object.assign(state.doc, info, { detailsPending: false });
|
Object.assign(state.doc, info, { detailsPending: false, detailsError: "" });
|
||||||
renderMeta();
|
renderMeta();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
|
Object.assign(state.doc, { detailsPending: false, detailsError: err.message });
|
||||||
|
renderMeta();
|
||||||
showError(err, false);
|
showError(err, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3595,14 +3612,14 @@ function mountPageSVG(index, page, openSeq = state.openSeq) {
|
|||||||
if (state.editorInfo) {
|
if (state.editorInfo) {
|
||||||
mountEditorObjects(index, page, surface);
|
mountEditorObjects(index, page, surface);
|
||||||
}
|
}
|
||||||
|
mountAnnotationNotes(index, surface);
|
||||||
for (const link of page.links) {
|
for (const link of page.links) {
|
||||||
const anchor = document.createElement("a");
|
const anchor = document.createElement("a");
|
||||||
anchor.className = "page-link";
|
anchor.className = "page-link";
|
||||||
if (link.dest) {
|
if (link.dest) {
|
||||||
const target = state.doc.pages.findIndex(page => page.id === link.dest.pageID);
|
const target = state.doc.pages.findIndex(page => page.id === link.dest.pageID);
|
||||||
if (target < 0 || !["XYZ", "Fit", "FitH", "FitV", "FitR"].includes(link.dest.type)) continue;
|
anchor.href = target < 0 ? "#" : `#page-${target + 1}`;
|
||||||
anchor.href = `#page-${target + 1}`;
|
anchor.title = target < 0 ? "文档链接" : `第${target + 1}页`;
|
||||||
anchor.title = `第${target + 1}页`;
|
|
||||||
anchor.addEventListener("click", event => { event.preventDefault(); navigateDestination(link.dest); });
|
anchor.addEventListener("click", event => { event.preventDefault(); navigateDestination(link.dest); });
|
||||||
} else {
|
} else {
|
||||||
let url;
|
let url;
|
||||||
@@ -3637,7 +3654,14 @@ function destinationPoint(page, x, y) {
|
|||||||
async function navigateDestination(dest) {
|
async function navigateDestination(dest) {
|
||||||
if (document.body.hasAttribute("aria-busy")) return;
|
if (document.body.hasAttribute("aria-busy")) return;
|
||||||
const index = state.doc.pages.findIndex(page => page.id === dest.pageID);
|
const index = state.doc.pages.findIndex(page => page.id === dest.pageID);
|
||||||
if (index < 0) return;
|
if (index < 0) {
|
||||||
|
setStatus("目标页面不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!["XYZ", "Fit", "FitH", "FitV", "FitR"].includes(dest.type)) {
|
||||||
|
setStatus("暂不支持此跳转方式");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const seq = state.openSeq, scale = state.scale;
|
const seq = state.openSeq, scale = state.scale;
|
||||||
await renderPage(index, { fit: false, scroll: false });
|
await renderPage(index, { fit: false, scroll: false });
|
||||||
if (seq !== state.openSeq || state.pageIndex !== index) return;
|
if (seq !== state.openSeq || state.pageIndex !== index) return;
|
||||||
@@ -4873,8 +4897,9 @@ function markThumbnailError(index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMeta() {
|
function renderMeta(keepDetails = false) {
|
||||||
const doc = state.doc || {};
|
const doc = state.doc || {};
|
||||||
|
el.metaPanel.setAttribute("aria-busy", String(!!doc.detailsPending));
|
||||||
document.title = `OFDGo WebUI - ${state.fileName}`;
|
document.title = `OFDGo WebUI - ${state.fileName}`;
|
||||||
el.metaFile.textContent = state.fileName;
|
el.metaFile.textContent = state.fileName;
|
||||||
el.metaTitle.textContent = doc.title || "-";
|
el.metaTitle.textContent = doc.title || "-";
|
||||||
@@ -4889,17 +4914,25 @@ function renderMeta() {
|
|||||||
}
|
}
|
||||||
el.metaType.textContent = doc.docType || "-";
|
el.metaType.textContent = doc.docType || "-";
|
||||||
el.metaVersion.textContent = doc.version || "-";
|
el.metaVersion.textContent = doc.version || "-";
|
||||||
el.metaSignatures.textContent = doc.detailsPending ? "正在检查" : String(doc.signatureCount || 0);
|
|
||||||
el.metaFonts.textContent = String(doc.fontCount || 0);
|
el.metaFonts.textContent = String(doc.fontCount || 0);
|
||||||
el.pageTotal.textContent = String(doc.pageCount || 0);
|
el.pageTotal.textContent = String(doc.pageCount || 0);
|
||||||
renderAttachments();
|
if (keepDetails && doc.detailsPending) return;
|
||||||
renderSignatures();
|
el.metaSignatures.textContent = doc.detailsPending ? "正在检查" : doc.detailsError ? "读取失败" : String(doc.signatureCount || 0);
|
||||||
|
renderMetaContent(el.attachmentList, [doc.attachments, doc.attachmentError], renderAttachments);
|
||||||
|
renderMetaContent(el.signatureList, [doc.signatures, doc.signatureError], renderSignatures);
|
||||||
renderAnnotations();
|
renderAnnotations();
|
||||||
renderDocumentFonts();
|
renderMetaContent(el.docFontList, doc.fonts || [], renderDocumentFonts);
|
||||||
renderFontList();
|
updateTextFonts(canvasEditor.selected, true);
|
||||||
updateLocalFontButton();
|
updateLocalFontButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMetaContent(node, value, render) {
|
||||||
|
const key = JSON.stringify(value);
|
||||||
|
if (metaContents.get(node) === key) return;
|
||||||
|
render();
|
||||||
|
metaContents.set(node, key);
|
||||||
|
}
|
||||||
|
|
||||||
function renderAttachments() {
|
function renderAttachments() {
|
||||||
const attachments = state.doc?.attachments || [];
|
const attachments = state.doc?.attachments || [];
|
||||||
el.attachmentPanel.hidden = !attachments.length && !state.doc?.attachmentError;
|
el.attachmentPanel.hidden = !attachments.length && !state.doc?.attachmentError;
|
||||||
@@ -5157,32 +5190,38 @@ function clearRegionHighlights() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAnnotations() {
|
function renderAnnotations() {
|
||||||
const annotations = (state.doc?.annotations || []).filter((annotation) => annotation.visible);
|
for (const shell of el.svgHost.querySelectorAll(".page-shell.rendered")) {
|
||||||
el.annotationPanel.hidden = !annotations.length;
|
mountAnnotationNotes(Number(shell.dataset.pageIndex), shell.querySelector(".page-surface"));
|
||||||
el.annotationList.replaceChildren();
|
}
|
||||||
const types = { Link: "链接", Path: "路径", Highlight: "高亮", Stamp: "印章", Watermark: "水印" };
|
}
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
for (const annotation of annotations) {
|
function mountAnnotationNotes(index, surface) {
|
||||||
const row = document.createElement("div");
|
for (const note of surface.querySelectorAll(".annotation-note")) note.remove();
|
||||||
row.className = "annotation-row";
|
if (!state.renderAnnotations) return;
|
||||||
const head = document.createElement("div");
|
const page = state.doc.pages[index];
|
||||||
head.className = "annotation-head";
|
for (const annotation of state.doc.annotations || []) {
|
||||||
const name = document.createElement("button");
|
if (!annotation.visible || annotation.page !== index + 1 || !annotation.remark?.trim()) continue;
|
||||||
name.type = "button";
|
const note = document.createElement("button");
|
||||||
name.className = "info-name-button";
|
note.type = "button";
|
||||||
name.textContent = `第 ${annotation.page} 页`;
|
note.className = "annotation-note";
|
||||||
name.disabled = !state.renderAnnotations || annotation.width <= 0 || annotation.height <= 0;
|
note.textContent = "\u24d8";
|
||||||
name.title = state.renderAnnotations ? "定位注解" : "注解已隐藏";
|
note.title = "查看注解";
|
||||||
name.addEventListener("click", () => focusPageRegion(annotation, "注解"));
|
note.setAttribute("aria-label", "查看注解");
|
||||||
const badge = fontBadge(types[annotation.type] || "注解", "");
|
note.setAttribute("aria-haspopup", "dialog");
|
||||||
head.append(badge, name);
|
note.style.left = `${Math.max(22 / MM_TO_PX, Math.min(page.width, annotation.x + annotation.width)) * MM_TO_PX}px`;
|
||||||
row.append(head);
|
note.style.top = `${Math.max(0, Math.min(page.height - 22 / MM_TO_PX, annotation.y)) * MM_TO_PX}px`;
|
||||||
appendInfoLine(row, "内容", annotation.remark);
|
note.addEventListener("click", () => {
|
||||||
appendInfoLine(row, "作者", annotation.creator);
|
if (state.editing || document.body.hasAttribute("aria-busy")) return;
|
||||||
appendInfoLine(row, "时间", formatDocumentTime(annotation.lastModDate));
|
el.annotationDetail.replaceChildren();
|
||||||
fragment.append(row);
|
const content = document.createElement("div");
|
||||||
|
content.textContent = annotation.remark;
|
||||||
|
el.annotationDetail.append(content);
|
||||||
|
appendInfoLine(el.annotationDetail, "作者", annotation.creator);
|
||||||
|
appendInfoLine(el.annotationDetail, "时间", formatDocumentTime(annotation.lastModDate));
|
||||||
|
el.annotationNote.showModal();
|
||||||
|
});
|
||||||
|
surface.append(note);
|
||||||
}
|
}
|
||||||
el.annotationList.append(fragment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDocumentFonts() {
|
function renderDocumentFonts() {
|
||||||
@@ -5358,7 +5397,7 @@ function setPan(enabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startPan(event) {
|
function startPan(event) {
|
||||||
if (!state.panMode || !state.doc || event.pointerType !== "mouse" || event.button !== 0 || document.body.hasAttribute("aria-busy") || event.target.closest("a")) {
|
if (!state.panMode || !state.doc || event.pointerType !== "mouse" || event.button !== 0 || document.body.hasAttribute("aria-busy") || event.target.closest("a, button")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rect = el.viewerPanel.getBoundingClientRect();
|
const rect = el.viewerPanel.getBoundingClientRect();
|
||||||
@@ -5494,7 +5533,7 @@ function setScale(nextScale, updateStatus = true, fitMode = "free") {
|
|||||||
updateControls();
|
updateControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
function scaleAnchor() {
|
function scaleAnchor(viewY = 0.45) {
|
||||||
const shell = pageShellFromView() || pageShell(state.pageIndex);
|
const shell = pageShellFromView() || pageShell(state.pageIndex);
|
||||||
if (!shell) {
|
if (!shell) {
|
||||||
return null;
|
return null;
|
||||||
@@ -5503,8 +5542,9 @@ function scaleAnchor() {
|
|||||||
const shellRect = shell.getBoundingClientRect();
|
const shellRect = shell.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
index: Number.parseInt(shell.dataset.pageIndex, 10),
|
index: Number.parseInt(shell.dataset.pageIndex, 10),
|
||||||
|
viewY,
|
||||||
x: (viewerRect.left + viewerRect.width / 2 - shellRect.left) / Math.max(1, shellRect.width),
|
x: (viewerRect.left + viewerRect.width / 2 - shellRect.left) / Math.max(1, shellRect.width),
|
||||||
y: (viewerRect.top + viewerRect.height * 0.45 - shellRect.top) / Math.max(1, shellRect.height),
|
y: (viewerRect.top + viewerRect.height * viewY - shellRect.top) / Math.max(1, shellRect.height),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5519,7 +5559,7 @@ function restoreScaleAnchor(anchor) {
|
|||||||
const viewerRect = el.viewerPanel.getBoundingClientRect();
|
const viewerRect = el.viewerPanel.getBoundingClientRect();
|
||||||
const shellRect = shell.getBoundingClientRect();
|
const shellRect = shell.getBoundingClientRect();
|
||||||
el.viewerPanel.scrollLeft += shellRect.left + shellRect.width * anchor.x - viewerRect.left - viewerRect.width / 2;
|
el.viewerPanel.scrollLeft += shellRect.left + shellRect.width * anchor.x - viewerRect.left - viewerRect.width / 2;
|
||||||
el.viewerPanel.scrollTop += shellRect.top + shellRect.height * anchor.y - viewerRect.top - viewerRect.height * 0.45;
|
el.viewerPanel.scrollTop += shellRect.top + shellRect.height * anchor.y - viewerRect.top - viewerRect.height * anchor.viewY;
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentPageInfo() {
|
function currentPageInfo() {
|
||||||
@@ -5596,6 +5636,7 @@ function updateControls() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateEditorTools() {
|
function updateEditorTools() {
|
||||||
|
el.viewerPanel.classList.toggle("editing", state.editing);
|
||||||
el.editButton.disabled = !state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
|
el.editButton.disabled = !state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
|
||||||
el.editButton.setAttribute("aria-pressed", String(state.editing));
|
el.editButton.setAttribute("aria-pressed", String(state.editing));
|
||||||
el.editButton.title = state.editing ? "阅读" : "编辑";
|
el.editButton.title = state.editing ? "阅读" : "编辑";
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export class CanvasEditor {
|
|||||||
svg.setAttribute("aria-hidden", "true");
|
svg.setAttribute("aria-hidden", "true");
|
||||||
svg.setAttribute("preserveAspectRatio", "none");
|
svg.setAttribute("preserveAspectRatio", "none");
|
||||||
let contour;
|
let contour;
|
||||||
if (object.type === "PathObject" && (!object.scoped || object.shape || object.oriented)) {
|
if (object.type === "PathObject" && (object.shape || object.oriented || !object.contours?.length)) {
|
||||||
const shape = object.shape || object.oriented?.kind;
|
const shape = object.shape || object.oriented?.kind;
|
||||||
contour = document.createElementNS("http://www.w3.org/2000/svg", shape === "line" ? "line" : shape === "ellipse" ? "ellipse" : shape === "rectangle" ? "rect" : "path");
|
contour = document.createElementNS("http://www.w3.org/2000/svg", shape === "line" ? "line" : shape === "ellipse" ? "ellipse" : shape === "rectangle" ? "rect" : "path");
|
||||||
if (!shape) contour.setAttribute("d", object.outline);
|
if (!shape) contour.setAttribute("d", object.outline);
|
||||||
@@ -312,7 +312,7 @@ export class CanvasEditor {
|
|||||||
this.nodes.set(node, item);
|
this.nodes.set(node, item);
|
||||||
node.addEventListener("focus", () => this.select(item));
|
node.addEventListener("focus", () => this.select(item));
|
||||||
const handles = !canEditObject(object, "transform") ? [] : lineShape(object.shape) ? ["start", "end"]
|
const handles = !canEditObject(object, "transform") ? [] : lineShape(object.shape) ? ["start", "end"]
|
||||||
: object.shape || object.oriented || object.type === "ImageObject" && !object.imageBorder ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"]
|
: object.shape || object.oriented || canEditObject(object, "stretch") ? ["nw", "n", "ne", "e", "se", "s", "sw", "w"]
|
||||||
: object.type === "TextObject" && canEditObject(object, "reflow") && canEditObject(object, "layoutKnown")
|
: object.type === "TextObject" && canEditObject(object, "reflow") && canEditObject(object, "layoutKnown")
|
||||||
? ["nw", "n", "ne", "e", "se", "s", "sw", "w"] : ["nw", "ne", "sw", "se"];
|
? ["nw", "n", "ne", "e", "se", "s", "sw", "w"] : ["nw", "ne", "sw", "se"];
|
||||||
for (const corner of handles) {
|
for (const corner of handles) {
|
||||||
@@ -629,7 +629,7 @@ export class CanvasEditor {
|
|||||||
Object.assign(drag.item.node.style, { left: `${drag.box.x * PX_PER_MM}px`, width: `${drag.box.width * PX_PER_MM}px` });
|
Object.assign(drag.item.node.style, { left: `${drag.box.x * PX_PER_MM}px`, width: `${drag.box.width * PX_PER_MM}px` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (drag.item.type === "ImageObject" && !drag.item.imageBorder && drag.corner.length === 1) {
|
if (canEditObject(drag.item, "stretch") && drag.corner.length === 1) {
|
||||||
drag.box = reshapeBox({ geometry: drag.item }, to.x - from.x, to.y - from.y, drag.corner, false);
|
drag.box = reshapeBox({ geometry: drag.item }, to.x - from.x, to.y - from.y, drag.corner, false);
|
||||||
const sx = drag.box.width / drag.item.width, sy = drag.box.height / drag.item.height;
|
const sx = drag.box.width / drag.item.width, sy = drag.box.height / drag.item.height;
|
||||||
Object.assign(drag.item.node.style, { left: `${drag.box.x * PX_PER_MM}px`, top: `${drag.box.y * PX_PER_MM}px`,
|
Object.assign(drag.item.node.style, { left: `${drag.box.x * PX_PER_MM}px`, top: `${drag.box.y * PX_PER_MM}px`,
|
||||||
@@ -763,7 +763,7 @@ export class CanvasEditor {
|
|||||||
this.viewer.releasePointerCapture(drag.pointerID);
|
this.viewer.releasePointerCapture(drag.pointerID);
|
||||||
const geometry = drag.item.oriented?.box || drag.item.geometry || (drag.item.textFrame ? { x: drag.item.x, width: drag.item.textFrame.width } : drag.item);
|
const geometry = drag.item.oriented?.box || drag.item.geometry || (drag.item.textFrame ? { x: drag.item.x, width: drag.item.textFrame.width } : drag.item);
|
||||||
const changed = Object.keys(drag.box).some((key) => drag.box[key] !== geometry[key]);
|
const changed = Object.keys(drag.box).some((key) => drag.box[key] !== geometry[key]);
|
||||||
const apply = drag.item.shape || drag.item.oriented ? this.options.onReshape : drag.item.type === "ImageObject" ? this.options.onResize : this.options.onTextWidth;
|
const apply = drag.item.shape || drag.item.oriented ? this.options.onReshape : canEditObject(drag.item, "stretch") ? this.options.onResize : this.options.onTextWidth;
|
||||||
Promise.resolve(changed && apply(drag.item, drag.box)).finally(() => {
|
Promise.resolve(changed && apply(drag.item, drag.box)).finally(() => {
|
||||||
drag.preview?.remove();
|
drag.preview?.remove();
|
||||||
this.place(drag.item);
|
this.place(drag.item);
|
||||||
|
|||||||
+17
-5
@@ -841,7 +841,7 @@ func editorObjects(index int, text *ofdgo.PageText) ([]any, error) {
|
|||||||
// 返回: map[string]any 前端能力
|
// 返回: map[string]any 前端能力
|
||||||
func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[string]any {
|
func editorCapabilities(capability ofdgo.ObjectCapabilities, kind string) map[string]any {
|
||||||
result := map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode),
|
result := map[string]any{"update": capability.Update, "paint": capability.Paint, "replaceFont": capability.ReplaceFont, "reflow": capability.Reflow, "layoutKnown": capability.LayoutKnown, "transform": capability.Transform, "arrange": capability.Arrange, "copy": capability.Copy, "delete": capability.Delete, "order": capability.Order, "reason": capability.Reason, "reasonCode": string(capability.ReasonCode),
|
||||||
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "resetCrop": capability.ResetCrop, "ungroup": capability.Ungroup, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
"replaceImage": capability.ReplaceImage, "cropImage": capability.CropImage, "fitImage": capability.FitImage, "resetCrop": capability.ResetCrop, "ungroup": capability.Ungroup, "stretch": capability.Stretch, "enter": capability.Transform && (kind == "CompositeObject" || kind == "CompositeGraphicUnit")}
|
||||||
if missing := capability.MissingGlyphs; missing != nil {
|
if missing := capability.MissingGlyphs; missing != nil {
|
||||||
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
result["missingGlyphs"] = map[string]any{"fontID": missing.FontID, "characters": missing.Characters}
|
||||||
}
|
}
|
||||||
@@ -991,6 +991,18 @@ func changeCompositeObjects(args []js.Value) (any, error) {
|
|||||||
page := args[0].Int()
|
page := args[0].Int()
|
||||||
operation := args[3].String()
|
operation := args[3].String()
|
||||||
switch operation {
|
switch operation {
|
||||||
|
case "group":
|
||||||
|
if args[4].Bool() {
|
||||||
|
if len(indexes) != 1 {
|
||||||
|
return fmt.Errorf("ungrouping requires one member")
|
||||||
|
}
|
||||||
|
selected, err = currentEditor.UngroupCompositeObject(page, path, indexes[0])
|
||||||
|
} else {
|
||||||
|
var index int
|
||||||
|
index, err = currentEditor.GroupCompositeObjects(page, path, indexes)
|
||||||
|
selected = []int{index}
|
||||||
|
}
|
||||||
|
return err
|
||||||
case "pasteStyle":
|
case "pasteStyle":
|
||||||
if copiedStyle == nil {
|
if copiedStyle == nil {
|
||||||
return fmt.Errorf("style clipboard is empty")
|
return fmt.Errorf("style clipboard is empty")
|
||||||
@@ -1161,19 +1173,19 @@ func changeCompositeText(page int, path ofdgo.ObjectPath, indexes []int, operati
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
members, err := currentEditor.CompositeObjects(page, path)
|
members, err := currentEditor.CompositeObjectsAt(page, path, indexes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var text strings.Builder
|
var text strings.Builder
|
||||||
for _, index := range indexes {
|
for _, member := range members {
|
||||||
if index >= len(members) || !members[index].Capabilities.ReplaceFont {
|
if !member.Capabilities.ReplaceFont {
|
||||||
return fmt.Errorf("composite font replacement is not supported")
|
return fmt.Errorf("composite font replacement is not supported")
|
||||||
}
|
}
|
||||||
if value != nil {
|
if value != nil {
|
||||||
text.WriteString(*value)
|
text.WriteString(*value)
|
||||||
} else {
|
} else {
|
||||||
text.WriteString(members[index].Object.TextObject.Text())
|
text.WriteString(member.Object.TextObject.Text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := checkFontGlyphs(data, text.String()); err != nil {
|
if err := checkFontGlyphs(data, text.String()); err != nil {
|
||||||
|
|||||||
+38
-17
@@ -162,30 +162,48 @@ func (e *Editor) CompositeObjects(page int, path ObjectPath) ([]CompositeMember,
|
|||||||
// 入参: page 页面索引, path 父复合路径, index 成员序号
|
// 入参: page 页面索引, path 父复合路径, index 成员序号
|
||||||
// 返回: CompositeMember 独立成员快照, error 错误信息
|
// 返回: CompositeMember 独立成员快照, error 错误信息
|
||||||
func (e *Editor) CompositeObject(page int, path ObjectPath, index int) (CompositeMember, error) {
|
func (e *Editor) CompositeObject(page int, path ObjectPath, index int) (CompositeMember, error) {
|
||||||
reader, renderer, _, nodes, err := e.compositeScope(page, path)
|
members, err := e.CompositeObjectsAt(page, path, []int{index})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CompositeMember{}, err
|
return CompositeMember{}, err
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
return members[0], nil
|
||||||
if index < 0 || index >= len(nodes) {
|
}
|
||||||
return CompositeMember{}, fmt.Errorf("composite member index %d out of range", index)
|
|
||||||
|
// CompositeObjectsAt 按输入顺序读取指定成员,一次解析范围且仅度量选区
|
||||||
|
// 入参: page 页面索引, path 父复合路径, indexes 不重复的成员序号
|
||||||
|
// 返回: []CompositeMember 独立成员快照, error 错误信息
|
||||||
|
func (e *Editor) CompositeObjectsAt(page int, path ObjectPath, indexes []int) ([]CompositeMember, error) {
|
||||||
|
reader, renderer, _, nodes, err := e.compositeScope(page, path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
member := e.measureCompositeMembers(renderer, nodes[index:index+1])[0]
|
defer reader.Close()
|
||||||
position := ObjectPosition{}
|
selected := make([]*editorCompositeNode, len(indexes))
|
||||||
for i, node := range nodes {
|
seen := make(map[int]bool, len(indexes))
|
||||||
if node.span.parent != nodes[index].span.parent {
|
for i, index := range indexes {
|
||||||
continue
|
if index < 0 || index >= len(nodes) || seen[index] {
|
||||||
|
return nil, fmt.Errorf("invalid composite member index %d", index)
|
||||||
}
|
}
|
||||||
|
seen[index] = true
|
||||||
|
selected[i] = nodes[index]
|
||||||
|
}
|
||||||
|
positions := make([]ObjectPosition, len(nodes))
|
||||||
|
containers := make(map[*editorXML]ObjectPosition)
|
||||||
|
for i, node := range nodes {
|
||||||
|
position := containers[node.span.parent]
|
||||||
if position.Count == 0 {
|
if position.Count == 0 {
|
||||||
position.Container = fmt.Sprint(i)
|
position.Container = fmt.Sprint(i)
|
||||||
}
|
}
|
||||||
if i == index {
|
positions[i] = ObjectPosition{Container: position.Container, Index: position.Count}
|
||||||
position.Index = position.Count
|
|
||||||
}
|
|
||||||
position.Count++
|
position.Count++
|
||||||
|
containers[node.span.parent] = position
|
||||||
}
|
}
|
||||||
member.Position = position
|
members := e.measureCompositeMembers(renderer, selected)
|
||||||
return member, nil
|
for i, index := range indexes {
|
||||||
|
members[i].Position = positions[index]
|
||||||
|
members[i].Position.Count = containers[nodes[index].span.parent].Count
|
||||||
|
}
|
||||||
|
return members, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransformCompositeObjects 在页面坐标中等比缩放并平移内部选区,只隔离被修改实例的资源
|
// TransformCompositeObjects 在页面坐标中等比缩放并平移内部选区,只隔离被修改实例的资源
|
||||||
@@ -225,7 +243,7 @@ func (e *Editor) FlipCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
return e.changeCompositeObjects(page, path, indexes, func(box Box) Matrix { return compositeOrientation(box, m) })
|
return e.changeCompositeObjects(page, path, indexes, func(box Box) Matrix { return compositeOrientation(box, m) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResizeCompositeObjects 缩放内部选区到目标范围,无边框图片支持独立宽高,其他对象保持比例
|
// ResizeCompositeObjects 按内部选区范围计算变换,图片及纯图片资源支持独立宽高,描边遵循OFD变换规则
|
||||||
// 入参: page 页面索引, path 父复合对象路径, indexes 成员序号, box 页面毫米目标范围
|
// 入参: page 页面索引, path 父复合对象路径, indexes 成员序号, box 页面毫米目标范围
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) ResizeCompositeObjects(page int, path ObjectPath, indexes []int, box Box) error {
|
func (e *Editor) ResizeCompositeObjects(page int, path ObjectPath, indexes []int, box Box) error {
|
||||||
@@ -247,8 +265,8 @@ func (e *Editor) ResizeCompositeObjects(page int, path ObjectPath, indexes []int
|
|||||||
uniform := math.Abs(sx-sy) <= 1e-9*math.Max(sx, sy)
|
uniform := math.Abs(sx-sy) <= 1e-9*math.Max(sx, sy)
|
||||||
if !uniform {
|
if !uniform {
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
if node.object.Type != "ImageObject" || node.object.ImageObject.Border != nil {
|
if !e.objectStretchable(node.object, make(map[string]bool)) {
|
||||||
return fmt.Errorf("nonuniform resizing requires images without borders")
|
return fmt.Errorf("nonuniform resizing requires images or image-only groups")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -504,6 +522,8 @@ func (e *Editor) measureCompositeMembers(renderer *Renderer, nodes []*editorComp
|
|||||||
transform := node.transformable()
|
transform := node.transformable()
|
||||||
capability := ObjectCapabilities{Transform: transform, Arrange: transform && bounds.box.W > 0 && bounds.box.H > 0,
|
capability := ObjectCapabilities{Transform: transform, Arrange: transform && bounds.box.W > 0 && bounds.box.H > 0,
|
||||||
Delete: true, Copy: transform && editorXMLCopyable(node.node), Order: orderable[node.span.parent], ReplaceImage: node.object.Type == "ImageObject"}
|
Delete: true, Copy: transform && editorXMLCopyable(node.node), Order: orderable[node.span.parent], ReplaceImage: node.object.Type == "ImageObject"}
|
||||||
|
capability.Ungroup = transform && capability.Order && e.compositeUngroupable(node.node) && (node.object.CompositeGraphicUnit.ResourceID != "" || len(node.object.CompositeGraphicUnit.Objects) != 0)
|
||||||
|
capability.Stretch = transform && e.objectStretchable(node.object, make(map[string]bool))
|
||||||
style, err := e.compositeMemberStyle(node)
|
style, err := e.compositeMemberStyle(node)
|
||||||
if !transform {
|
if !transform {
|
||||||
capability.ReasonCode, capability.Reason = EditUnsupportedContainer, "composite member cannot be transformed"
|
capability.ReasonCode, capability.Reason = EditUnsupportedContainer, "composite member cannot be transformed"
|
||||||
@@ -779,6 +799,7 @@ func (e *Editor) transformCompositeMember(renderer *Renderer, n *editorComposite
|
|||||||
}
|
}
|
||||||
local := inverse.Multiply(matrix).Multiply(n.parent)
|
local := inverse.Multiply(matrix).Multiply(n.parent)
|
||||||
if object.Type == "ImageObject" && object.ImageObject.Border != nil {
|
if object.Type == "ImageObject" && object.ImageObject.Border != nil {
|
||||||
|
object.state = n.states[editorObjectID(object)]
|
||||||
origin := &editorObjectOrigin{data: n.data, node: n.node, object: n.object}
|
origin := &editorObjectOrigin{data: n.data, node: n.node, object: n.object}
|
||||||
object, err = e.transformBorderedImage(object, local, origin, renderer)
|
object, err = e.transformBorderedImage(object, local, origin, renderer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package ofdgo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GroupCompositeObjects 组合内部同一容器的连续成员,保留标识、绘制顺序及共享资源
|
||||||
|
// 入参: page 页面索引, path 父路径, indexes 至少两个成员序号
|
||||||
|
// 返回: int 新组合序号, error 错误信息
|
||||||
|
func (e *Editor) GroupCompositeObjects(page int, path ObjectPath, indexes []int) (int, error) {
|
||||||
|
if len(indexes) < 2 {
|
||||||
|
return 0, fmt.Errorf("grouping requires at least two objects")
|
||||||
|
}
|
||||||
|
var result int
|
||||||
|
err := e.editCompositeObjects(page, path, indexes, func(_ *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
for _, member := range members {
|
||||||
|
if !member.Capabilities.Copy || !member.Capabilities.Order {
|
||||||
|
return fmt.Errorf("composite member cannot be grouped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.SortFunc(nodes, func(a, b *editorCompositeNode) int { return a.index - b.index })
|
||||||
|
siblings := nodes[0].siblings()
|
||||||
|
first := slices.Index(siblings, nodes[0])
|
||||||
|
var content []byte
|
||||||
|
for i, node := range nodes {
|
||||||
|
if first+i >= len(siblings) || siblings[first+i] != node {
|
||||||
|
return fmt.Errorf("grouping requires consecutive members in the same container")
|
||||||
|
}
|
||||||
|
content = append(content, bytes.TrimPrefix(node.data, []byte(xml.Header))...)
|
||||||
|
}
|
||||||
|
content, err := editorXMLContainer("Content", nil, content)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content, err = editorXMLContainer("CompositeObject", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: e.nextID()}, {Name: xml.Name{Local: "Boundary"}, Value: "0 0 1 1"}}, bytes.TrimPrefix(content, []byte(xml.Header)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, node := range nodes {
|
||||||
|
var replacement []byte
|
||||||
|
if i == 0 {
|
||||||
|
replacement = bytes.TrimPrefix(content, []byte(xml.Header))
|
||||||
|
}
|
||||||
|
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, replacement})
|
||||||
|
}
|
||||||
|
result = nodes[0].index
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UngroupCompositeObject 展开内部组合,隔离引用资源并保留父容器的样式和裁剪
|
||||||
|
// 入参: page 页面索引, path 父路径, index 组合序号
|
||||||
|
// 返回: []int 展开后的成员序号, error 错误信息
|
||||||
|
func (e *Editor) UngroupCompositeObject(page int, path ObjectPath, index int) ([]int, error) {
|
||||||
|
var result []int
|
||||||
|
err := e.editCompositeObjects(page, path, []int{index}, func(renderer *Renderer, nodes []*editorCompositeNode, members []CompositeMember) error {
|
||||||
|
node := nodes[0]
|
||||||
|
if !members[0].Capabilities.Order {
|
||||||
|
return fmt.Errorf("composite container cannot be reordered")
|
||||||
|
}
|
||||||
|
return e.ungroupCompositeNode(renderer, node, func(objects []GraphicObject) error {
|
||||||
|
var content []byte
|
||||||
|
for i, object := range objects {
|
||||||
|
content = append(content, object.origin.data...)
|
||||||
|
maps.Copy(node.owner.states, object.CompositeGraphicUnit.states)
|
||||||
|
node.owner.states[editorObjectID(object)] = object.state
|
||||||
|
result = append(result, index+i)
|
||||||
|
}
|
||||||
|
delete(node.owner.states, editorObjectID(node.object))
|
||||||
|
node.owner.patches = append(node.owner.patches, editorXMLPatch{node.span.start, node.span.end, content})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ungroupCompositeNode 将组合展开至所在坐标系,祖先透明度和裁剪仅由原父级应用一次
|
||||||
|
// 入参: renderer 当前读取器, node 待展开组合, replace 原子替换回调
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) ungroupCompositeNode(renderer *Renderer, node *editorCompositeNode, replace func([]GraphicObject) error) error {
|
||||||
|
if !e.compositeUngroupable(node.node) {
|
||||||
|
return fmt.Errorf("group container cannot be removed without losing attributes")
|
||||||
|
}
|
||||||
|
copy, err := newEditorCompositeNode(node.data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copy.setStates(node.states)
|
||||||
|
if !node.boundaryInCTM {
|
||||||
|
if err := copy.convertCoordinates(node.parent, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
copy.parent, copy.boundaryInCTM, copy.visible = IdentityMatrix, true, true
|
||||||
|
copy.defaults, copy.drawParams = node.defaults, node.drawParams
|
||||||
|
members, err := e.compositeMembers(copy, renderer.Reader, renderer, make(map[string]bool))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(members) == 0 {
|
||||||
|
return fmt.Errorf("group has no objects")
|
||||||
|
}
|
||||||
|
for _, member := range members {
|
||||||
|
if !member.transformable() || !editorXMLCopyable(member.node) {
|
||||||
|
return fmt.Errorf("group member cannot be expanded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.pasteCompositeSelection(&CompositeSelection{editor: e, nodes: members}, func(objects []GraphicObject) error {
|
||||||
|
ids := make(map[string]string)
|
||||||
|
if copy.ref != nil {
|
||||||
|
for _, object := range objects {
|
||||||
|
if err := e.collectCompositeIDs(object.origin.node, ids); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, object := range objects {
|
||||||
|
data := object.origin.data
|
||||||
|
if len(ids) != 0 {
|
||||||
|
data, err = editorXMLRemapIDs(data, object.origin.node, ids)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
member, err := newEditorCompositeNode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
states := maps.Clone(object.CompositeGraphicUnit.states)
|
||||||
|
if states == nil {
|
||||||
|
states = make(map[string]editorCompositeState)
|
||||||
|
}
|
||||||
|
states[editorObjectID(object)] = object.state
|
||||||
|
if len(ids) != 0 {
|
||||||
|
states = remapCompositeStates(states, ids)
|
||||||
|
}
|
||||||
|
if object.Type == "CompositeObject" || object.Type == "CompositeGraphicUnit" {
|
||||||
|
if err := member.convertCoordinates(IdentityMatrix, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !node.boundaryInCTM {
|
||||||
|
if err := member.convertCoordinates(node.parent, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
member.node.parent = &editorXML{name: xml.Name{Local: "Layer"}, parent: &editorXML{name: xml.Name{Local: "Content"}}}
|
||||||
|
member.object.state = states[editorObjectID(member.object)]
|
||||||
|
member.object.TextObject.layout = member.object.state.layout
|
||||||
|
member.object.CompositeGraphicUnit.states = states
|
||||||
|
member.object.origin = &editorObjectOrigin{editor: e, data: member.data, node: member.node, object: member.object}
|
||||||
|
objects[i] = member.object
|
||||||
|
}
|
||||||
|
return replace(objects)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositeUngroupable 检查待移除的容器及引用链,子对象原文不展开重写
|
||||||
|
// 入参: node 组合节点
|
||||||
|
// 返回: bool 是否可解组
|
||||||
|
func (e *Editor) compositeUngroupable(node *editorXML) bool {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for {
|
||||||
|
if !editorUngroupable(node) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
id := node.attr("ResourceID")
|
||||||
|
if id == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if seen[id] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
seen[id] = true
|
||||||
|
resource, err := e.compositeDefinition(id)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
node = resource.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compositeDefinition 读取复合资源原文,不加载图片或字体数据
|
||||||
|
// 入参: id 资源标识
|
||||||
|
// 返回: *editorCompositeNode 定义节点, error 错误信息
|
||||||
|
func (e *Editor) compositeDefinition(id string) (*editorCompositeNode, error) {
|
||||||
|
var data []byte
|
||||||
|
for _, resource := range e.resources {
|
||||||
|
if resource.composite == id {
|
||||||
|
data = resource.data
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if data == nil && e.source != nil {
|
||||||
|
var err error
|
||||||
|
data, err = e.source.reader.readFile(e.source.reader.resourceFiles[id])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if units := root.child("CompositeGraphicUnits"); units != nil {
|
||||||
|
for _, unit := range units.children {
|
||||||
|
if unit.name.Local == "CompositeGraphicUnit" && unit.attr("ID") == id {
|
||||||
|
fragment, err := editorXMLStandalone(data[unit.start:unit.end], unit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newEditorCompositeNode(fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("composite resource %q not found", id)
|
||||||
|
}
|
||||||
+8
-19
@@ -103,36 +103,25 @@ func (e *Editor) GroupObjects(page int, ids []string) (string, error) {
|
|||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UngroupObject 展开内联组合,保留页面外观与裁剪,含容器行为或扩展的组合不拆解
|
// UngroupObject 展开组合,保留页面外观与裁剪,资源引用分配独立标识,含容器行为或扩展的组合不拆解
|
||||||
// 入参: page 页面索引, id 组合标识
|
// 入参: page 页面索引, id 组合标识
|
||||||
// 返回: []string 展开后的对象标识, error 错误信息
|
// 返回: []string 展开后的对象标识, error 错误信息
|
||||||
func (e *Editor) UngroupObject(page int, id string) ([]string, error) {
|
func (e *Editor) UngroupObject(page int, id string) ([]string, error) {
|
||||||
objects, indexes, err := e.selectedObjects(page, []string{id})
|
_, indexes, err := e.selectedObjects(page, []string{id})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
origin := e.objectOrigin(id)
|
origin := e.objectOrigin(id)
|
||||||
if origin == nil || !editorUngroupable(origin.node) || !editorContainerOrderable(origin.node.parent) || objects[0].CompositeGraphicUnit.ResourceID != "" {
|
if origin == nil || !editorContainerOrderable(origin.node.parent) {
|
||||||
return nil, fmt.Errorf("group container cannot be removed without losing attributes")
|
return nil, fmt.Errorf("group container cannot be removed without losing attributes")
|
||||||
}
|
}
|
||||||
reader, _, _, nodes, err := e.compositeScope(page, ObjectPath{ID: id})
|
reader, renderer, root, _, err := e.compositeScope(page, ObjectPath{ID: id})
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
reader.Close()
|
|
||||||
if len(nodes) == 0 {
|
|
||||||
return nil, fmt.Errorf("group has no objects")
|
|
||||||
}
|
|
||||||
members := make([]int, len(nodes))
|
|
||||||
for i := range members {
|
|
||||||
members[i] = i
|
|
||||||
}
|
|
||||||
selection, err := e.CaptureCompositeObjects(page, ObjectPath{ID: id}, members)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer reader.Close()
|
||||||
var result []string
|
var result []string
|
||||||
err = e.pasteCompositeSelection(selection, func(objects []GraphicObject) error {
|
err = e.ungroupCompositeNode(renderer, root, func(objects []GraphicObject) error {
|
||||||
origins := make(map[string]*editorObjectOrigin, len(objects))
|
origins := make(map[string]*editorObjectOrigin, len(objects))
|
||||||
for i := range objects {
|
for i := range objects {
|
||||||
object := &objects[i]
|
object := &objects[i]
|
||||||
@@ -149,14 +138,14 @@ func (e *Editor) UngroupObject(page int, id string) ([]string, error) {
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorUngroupable 判断移除内联容器是否会丢失对象行为或未知属性
|
// editorUngroupable 判断移除容器是否会丢失对象行为或未知属性
|
||||||
// 入参: node 容器节点
|
// 入参: node 容器节点
|
||||||
// 返回: bool 是否可解组
|
// 返回: bool 是否可解组
|
||||||
func editorUngroupable(node *editorXML) bool {
|
func editorUngroupable(node *editorXML) bool {
|
||||||
if node.name.Local != "CompositeObject" && node.name.Local != "CompositeGraphicUnit" {
|
if node.name.Local != "CompositeObject" && node.name.Local != "CompositeGraphicUnit" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !editorXMLAttributes(node, "ID Boundary CTM DrawParam Alpha Visible") {
|
if !editorXMLAttributes(node, "ID Boundary CTM DrawParam Alpha Visible ResourceID Width Height") {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ type editorSourcePage struct {
|
|||||||
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
// Paint表示可独立修改纯色填充和描边,不要求重新排版文字
|
||||||
// ReplaceImage表示可替换图片数据,CropImage表示可裁剪图片;内部裁剪与原裁剪取交集
|
// ReplaceImage表示可替换图片数据,CropImage表示可裁剪图片;内部裁剪与原裁剪取交集
|
||||||
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
// MissingGlyphs提供缺字导致操作受限时的结构化诊断
|
||||||
// Ungroup表示可移除内联组合容器并保留内部内容
|
// Ungroup表示可移除组合容器并保留内部内容,Stretch表示图片及纯图片组合可独立调整宽高
|
||||||
type ObjectCapabilities struct {
|
type ObjectCapabilities struct {
|
||||||
Update bool
|
Update bool
|
||||||
Paint bool
|
Paint bool
|
||||||
@@ -84,6 +84,7 @@ type ObjectCapabilities struct {
|
|||||||
Delete bool
|
Delete bool
|
||||||
Order bool
|
Order bool
|
||||||
Ungroup bool
|
Ungroup bool
|
||||||
|
Stretch bool
|
||||||
Reason string
|
Reason string
|
||||||
ReasonCode EditReason
|
ReasonCode EditReason
|
||||||
MissingGlyphs *MissingGlyphError
|
MissingGlyphs *MissingGlyphError
|
||||||
@@ -366,6 +367,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
all.ResetCrop = object.Type == "ImageObject" && object.state.crop != nil
|
all.ResetCrop = object.Type == "ImageObject" && object.state.crop != nil
|
||||||
origin := e.objectOrigin(id)
|
origin := e.objectOrigin(id)
|
||||||
if origin == nil {
|
if origin == nil {
|
||||||
|
all.Stretch = object.Type == "ImageObject"
|
||||||
all.ReplaceImage = object.Type == "ImageObject"
|
all.ReplaceImage = object.Type == "ImageObject"
|
||||||
all.CropImage = all.ReplaceImage
|
all.CropImage = all.ReplaceImage
|
||||||
all.FitImage = all.ReplaceImage && axisAlignedMatrix(NewMatrix(object.ImageObject.CTM))
|
all.FitImage = all.ReplaceImage && axisAlignedMatrix(NewMatrix(object.ImageObject.CTM))
|
||||||
@@ -385,7 +387,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
return ObjectCapabilities{Reason: err.Error(), ReasonCode: EditInvalidObject}, nil
|
return ObjectCapabilities{Reason: err.Error(), ReasonCode: EditInvalidObject}, nil
|
||||||
}
|
}
|
||||||
all.Order = editorContainerOrderable(node.parent)
|
all.Order = editorContainerOrderable(node.parent)
|
||||||
all.Ungroup = all.Order && editorUngroupable(node) && len(object.CompositeGraphicUnit.Objects) != 0
|
all.Ungroup = all.Order && e.compositeUngroupable(node) && (object.CompositeGraphicUnit.ResourceID != "" || len(object.CompositeGraphicUnit.Objects) != 0)
|
||||||
all.Copy = editorXMLCopyable(node) && (origin.reason == nil || object.Type == "PathObject" && editReason(origin.reason) == EditUnsupportedColor)
|
all.Copy = editorXMLCopyable(node) && (origin.reason == nil || object.Type == "PathObject" && editReason(origin.reason) == EditUnsupportedColor)
|
||||||
if origin.reason != nil {
|
if origin.reason != nil {
|
||||||
all.Update, all.Reflow, all.ReplaceFont, all.Paint = false, false, false, false
|
all.Update, all.Reflow, all.ReplaceFont, all.Paint = false, false, false, false
|
||||||
@@ -412,6 +414,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
all.Reflow = false
|
all.Reflow = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
all.Stretch = all.Transform && e.objectStretchable(object, make(map[string]bool))
|
||||||
return all, nil
|
return all, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func (e *Editor) FlipObjects(page int, ids []string, axis string) error {
|
|||||||
return e.orientObjects(page, ids, m)
|
return e.orientObjects(page, ids, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResizeObjects 将选区可见范围移动并缩放到指定矩形,文字、路径和带边框图片保持比例,无边框图片支持独立宽高
|
// ResizeObjects 按选区可见范围计算目标矩形的变换,图片及纯图片资源支持独立宽高,描边遵循OFD变换规则
|
||||||
// 不重排文字或重采样图片,一次撤销恢复全部;基本图形独立调整宽高可使用PathObject.Reshape
|
// 不重排文字或重采样图片,一次撤销恢复全部;基本图形独立调整宽高可使用PathObject.Reshape
|
||||||
// 入参: page 页面索引, ids 对象标识, box 页面毫米坐标中的目标范围
|
// 入参: page 页面索引, ids 对象标识, box 页面毫米坐标中的目标范围
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
@@ -96,10 +96,10 @@ func (e *Editor) ResizeObjects(page int, ids []string, box Box) error {
|
|||||||
return e.transformObjects(page, objects, func(object GraphicObject) (GraphicObject, error) {
|
return e.transformObjects(page, objects, func(object GraphicObject) (GraphicObject, error) {
|
||||||
if uniform {
|
if uniform {
|
||||||
return e.transformObject(object, matrix.e, matrix.f, sx)
|
return e.transformObject(object, matrix.e, matrix.f, sx)
|
||||||
} else if object.Type == "ImageObject" && object.ImageObject.Border == nil {
|
} else if e.objectStretchable(object, make(map[string]bool)) {
|
||||||
return e.transformMatrix(object, matrix)
|
return e.transformMatrix(object, matrix)
|
||||||
}
|
}
|
||||||
return GraphicObject{}, fmt.Errorf("nonuniform resizing requires images without borders")
|
return GraphicObject{}, fmt.Errorf("nonuniform resizing requires images or image-only groups")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,9 +177,56 @@ func (e *Editor) transformMatrix(object GraphicObject, matrix Matrix) (GraphicOb
|
|||||||
}
|
}
|
||||||
origin := e.objectOrigin(object.ImageObject.ID)
|
origin := e.objectOrigin(object.ImageObject.ID)
|
||||||
if origin == nil {
|
if origin == nil {
|
||||||
return GraphicObject{}, fmt.Errorf("bordered image requires an original object")
|
return GraphicObject{}, fmt.Errorf("image source is unavailable")
|
||||||
}
|
}
|
||||||
return e.transformBorderedImage(object, matrix, origin, NewRenderer(e.source.reader, WithFontFS(e.fontFS...)))
|
reader, err := e.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return GraphicObject{}, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
return e.transformBorderedImage(object, matrix, origin, NewRenderer(reader, WithFontFS(e.fontFS...)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// objectStretchable 允许图片及纯图片资源独立调整宽高,内联组合仍保持比例
|
||||||
|
// 入参: object 对象, visiting 当前资源链
|
||||||
|
// 返回: bool 是否支持独立宽高
|
||||||
|
func (e *Editor) objectStretchable(object GraphicObject, visiting map[string]bool) bool {
|
||||||
|
if object.Type != "ImageObject" && (object.CompositeGraphicUnit.ResourceID == "" || len(object.CompositeGraphicUnit.Objects) != 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return e.objectImagesOnly(object, visiting)
|
||||||
|
}
|
||||||
|
|
||||||
|
// objectImagesOnly 检查资源链中是否只有图片,保留原边框与裁剪语义
|
||||||
|
// 入参: object 对象, visiting 当前资源链
|
||||||
|
// 返回: bool 是否只包含图片
|
||||||
|
func (e *Editor) objectImagesOnly(object GraphicObject, visiting map[string]bool) bool {
|
||||||
|
if object.Type == "ImageObject" {
|
||||||
|
return object.ImageObject.Border == nil || len(object.ImageObject.Actions) == 0
|
||||||
|
}
|
||||||
|
if object.Type != "CompositeObject" && object.Type != "CompositeGraphicUnit" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
group := object.CompositeGraphicUnit
|
||||||
|
found := len(group.Objects) != 0
|
||||||
|
if id := group.ResourceID; id != "" {
|
||||||
|
if visiting[id] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
visiting[id] = true
|
||||||
|
resource, err := e.compositeDefinition(id)
|
||||||
|
if err != nil || !e.objectImagesOnly(resource.object, visiting) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
delete(visiting, id)
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
for _, child := range group.Objects {
|
||||||
|
if !e.objectImagesOnly(child, visiting) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
// transformBorderedImage 将原文图片封装为复合资源,保留边框和裁剪
|
// transformBorderedImage 将原文图片封装为复合资源,保留边框和裁剪
|
||||||
@@ -231,6 +278,7 @@ func (e *Editor) transformBorderedImage(object GraphicObject, matrix Matrix, ori
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return GraphicObject{}, err
|
return GraphicObject{}, err
|
||||||
}
|
}
|
||||||
|
resource.states = map[string]editorCompositeState{child.ImageObject.ID: object.state}
|
||||||
e.resources = append(e.resources, resource)
|
e.resources = append(e.resources, resource)
|
||||||
object = GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{ID: object.ImageObject.ID, ResourceID: id, Boundary: editorBoxString(extent)}}
|
object = GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{ID: object.ImageObject.ID, ResourceID: id, Boundary: editorBoxString(extent)}}
|
||||||
return transformEditorMatrix(object, matrix), nil
|
return transformEditorMatrix(object, matrix), nil
|
||||||
|
|||||||
@@ -180,7 +180,13 @@ func (r *boundsRenderer) addContour(path *canvas.Path, evenOdd bool) {
|
|||||||
rect := path.Bounds()
|
rect := path.Bounds()
|
||||||
r.box = unionTextBox(r.box, Box{X: rect.X0, Y: -rect.Y1, W: rect.W(), H: rect.H()})
|
r.box = unionTextBox(r.box, Box{X: rect.X0, Y: -rect.Y1, W: rect.W(), H: rect.H()})
|
||||||
if r.collect {
|
if r.collect {
|
||||||
r.contours = append(r.contours, ObjectContour{Path: path.Copy().Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}}).ToSVG(), EvenOdd: evenOdd})
|
outline := &canvas.Path{}
|
||||||
|
for _, subpath := range path.Split() {
|
||||||
|
subpath = subpath.Copy()
|
||||||
|
subpath.Close()
|
||||||
|
outline = outline.Append(subpath)
|
||||||
|
}
|
||||||
|
r.contours = append(r.contours, ObjectContour{Path: outline.Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}}).ToSVG(), EvenOdd: evenOdd})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user