mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
1 parent
71e67be9c1
commit
1914b5329a
25 files changed
+744
-250
No files matched your search
@@ -93,7 +93,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="editor-group editor-erase">
|
<div class="editor-group editor-erase">
|
||||||
<button id="eraseObjectButton" class="button" type="button" title="整对象擦除" aria-label="整对象擦除" aria-pressed="false">对象</button>
|
<button id="eraseObjectButton" class="button" type="button" title="整对象擦除" aria-label="整对象擦除" aria-pressed="false">对象</button>
|
||||||
<button id="eraseRegionButton" class="button" type="button" title="区域擦除(视觉裁剪)" aria-label="区域擦除" aria-pressed="false">区域</button>
|
<button id="eraseRegionButton" class="button" type="button" title="矩形擦除(视觉裁剪)" aria-label="矩形擦除" aria-pressed="false">矩形</button>
|
||||||
|
<button id="erasePathButton" class="button" type="button" title="套索擦除(视觉裁剪)" aria-label="套索擦除" aria-pressed="false">套索</button>
|
||||||
<span>擦除</span>
|
<span>擦除</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-group">
|
<div class="editor-group">
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ a:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.editor-file,
|
.editor-file,
|
||||||
|
.editor-erase,
|
||||||
.editor-paragraph,
|
.editor-paragraph,
|
||||||
.editor-image,
|
.editor-image,
|
||||||
.editor-group:has(#textFont),
|
.editor-group:has(#textFont),
|
||||||
@@ -119,11 +120,15 @@ a:focus-visible {
|
|||||||
grid-template-columns: repeat(2, max-content);
|
grid-template-columns: repeat(2, max-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-transform,
|
.editor-transform {
|
||||||
.editor-erase {
|
|
||||||
grid-template-columns: max-content;
|
grid-template-columns: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#eraseObjectButton {
|
||||||
|
grid-row: span 2;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-image > select {
|
.editor-image > select {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-13
@@ -120,6 +120,7 @@ const el = {
|
|||||||
drawEllipseButton: document.querySelector("#drawEllipseButton"),
|
drawEllipseButton: document.querySelector("#drawEllipseButton"),
|
||||||
eraseObjectButton: document.querySelector("#eraseObjectButton"),
|
eraseObjectButton: document.querySelector("#eraseObjectButton"),
|
||||||
eraseRegionButton: document.querySelector("#eraseRegionButton"),
|
eraseRegionButton: document.querySelector("#eraseRegionButton"),
|
||||||
|
erasePathButton: document.querySelector("#erasePathButton"),
|
||||||
shapeFill: document.querySelector("#shapeFill"),
|
shapeFill: document.querySelector("#shapeFill"),
|
||||||
shapeFillColor: document.querySelector("#shapeFillColor"),
|
shapeFillColor: document.querySelector("#shapeFillColor"),
|
||||||
shapeStroke: document.querySelector("#shapeStroke"),
|
shapeStroke: document.querySelector("#shapeStroke"),
|
||||||
@@ -277,10 +278,11 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
||||||
},
|
},
|
||||||
onDelete: (item) => changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
|
onDelete: (item) => changeDocument(item.items ? "ofdgoDeleteObjects" : "ofdgoDeleteObject", item),
|
||||||
onErase: (items, box) => {
|
onErase: (items, box, points) => {
|
||||||
const blocked = items.find(item => !canEditObject(item, box ? "arrange" : "delete"));
|
const blocked = items.find(item => !canEditObject(item, box ? "arrange" : "delete"));
|
||||||
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
|
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
|
||||||
const item = { index: items[0].index, id: items.map(item => item.id) };
|
const item = { index: items[0].index, id: items.map(item => item.id) };
|
||||||
|
if (points) return changeDocument("ofdgoEraseObjectsPath", item, points);
|
||||||
return box ? changeDocument("ofdgoEraseObjects", item, box.x, box.y, box.width, box.height)
|
return box ? changeDocument("ofdgoEraseObjects", item, box.x, box.y, box.width, box.height)
|
||||||
: changeDocument("ofdgoDeleteObjects", item);
|
: changeDocument("ofdgoDeleteObjects", item);
|
||||||
},
|
},
|
||||||
@@ -437,7 +439,7 @@ el.resetCropButton.addEventListener("click", () => {
|
|||||||
editorClick(el.undoButton, () => changeDocument("ofdgoUndo"));
|
editorClick(el.undoButton, () => changeDocument("ofdgoUndo"));
|
||||||
editorClick(el.redoButton, () => changeDocument("ofdgoRedo"));
|
editorClick(el.redoButton, () => changeDocument("ofdgoRedo"));
|
||||||
for (const [button, tool] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"],
|
for (const [button, tool] of [[el.drawLineButton, "line"], [el.drawRectangleButton, "rectangle"], [el.drawEllipseButton, "ellipse"],
|
||||||
[el.eraseObjectButton, "erase-object"], [el.eraseRegionButton, "erase-region"]]) {
|
[el.eraseObjectButton, "erase-object"], [el.eraseRegionButton, "erase-region"], [el.erasePathButton, "erase-path"]]) {
|
||||||
editorClick(button, () => {
|
editorClick(button, () => {
|
||||||
if (!state.editing || document.body.hasAttribute("aria-busy")) {
|
if (!state.editing || document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
@@ -976,7 +978,7 @@ async function beginCanvasText(index, box, surface, page) {
|
|||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const data = font.embedded ? (await callWASM("ofdgoEditorFont", font.id.slice(9))).bytes : await fontManager.read(font);
|
const data = await readTextFont(font);
|
||||||
const face = new FontFace("ofdgo-draft", data);
|
const face = new FontFace("ofdgo-draft", data);
|
||||||
await face.load();
|
await face.load();
|
||||||
if (openSeq !== state.openSeq || !surface.isConnected) return;
|
if (openSeq !== state.openSeq || !surface.isConnected) return;
|
||||||
@@ -991,7 +993,7 @@ async function beginCanvasText(index, box, surface, page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function editorFonts(item) {
|
function editorFonts(item) {
|
||||||
const fonts = [...fontManager.userFonts.filter((font) => font.enabled), ...fontManager.catalog];
|
const fonts = fontManager.editorFonts();
|
||||||
if (item) {
|
if (item) {
|
||||||
fonts.unshift({ id: `embedded:${item.font}`, name: item.fontName || item.font, embedded: true, disabled: !canEditObject(item, "update") });
|
fonts.unshift({ id: `embedded:${item.font}`, name: item.fontName || item.font, embedded: true, disabled: !canEditObject(item, "update") });
|
||||||
}
|
}
|
||||||
@@ -999,22 +1001,31 @@ function editorFonts(item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadEditorFonts() {
|
async function loadEditorFonts() {
|
||||||
if (!fontManager.canReadLocal() || fontManager.catalogLoaded || fontManager.permission === "denied" || state.fontCatalogLoading) return;
|
const local = fontManager.canReadLocal() && !fontManager.catalogLoaded && fontManager.permission !== "denied";
|
||||||
|
if (state.fontCatalogLoading || !local && !fontManager.userFonts.some(font => font.enabled && !font.faces)) return;
|
||||||
state.fontCatalogLoading = true;
|
state.fontCatalogLoading = true;
|
||||||
try {
|
try {
|
||||||
await fontManager.queryLocal();
|
if (local) {
|
||||||
|
try { await fontManager.queryLocal(); }
|
||||||
|
catch (err) { setStatus(err?.name === "NotAllowedError" ? "字体尚未授权" : String(err.message || err)); }
|
||||||
|
}
|
||||||
|
await fontManager.loadFaces(data => callWASM("ofdgoFontFaces", data));
|
||||||
const open = fontPicker.open;
|
const open = fontPicker.open;
|
||||||
const selected = state.textFonts[Number(fontPicker.value)];
|
const selected = state.textFonts[Number(fontPicker.value)];
|
||||||
const query = el.textFont.value === (selected?.fullName || selected?.name || "") ? "" : el.textFont.value;
|
const query = el.textFont.value === (selected?.fullName || selected?.name || "") ? "" : el.textFont.value;
|
||||||
updateTextFonts(canvasEditor.selected, true);
|
updateTextFonts(canvasEditor.selected, true);
|
||||||
if (open && !el.textFont.disabled) { if (query) el.textFont.value = query; fontPicker.show(query); }
|
if (open && !el.textFont.disabled) { if (query) el.textFont.value = query; fontPicker.show(query); }
|
||||||
} catch (err) {
|
|
||||||
setStatus(err?.name === "NotAllowedError" ? "字体尚未授权" : String(err.message || err));
|
|
||||||
} finally {
|
} finally {
|
||||||
state.fontCatalogLoading = false;
|
state.fontCatalogLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readTextFont(font) {
|
||||||
|
if (font.embedded) return (await callWASM("ofdgoEditorFont", font.id.slice(9))).bytes;
|
||||||
|
if (font.file) return (await callWASM("ofdgoFontFace", await fontManager.read(font.file), font.index)).bytes;
|
||||||
|
return fontManager.read(font);
|
||||||
|
}
|
||||||
|
|
||||||
function updateTextFonts(item, refresh = false) {
|
function updateTextFonts(item, refresh = false) {
|
||||||
const id = item?.type === "TextObject" && !item.draft ? item.font : null;
|
const id = item?.type === "TextObject" && !item.draft ? item.font : null;
|
||||||
if (refresh || state.textFontID !== id) {
|
if (refresh || state.textFontID !== id) {
|
||||||
@@ -1039,7 +1050,7 @@ async function changeTextFont() {
|
|||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const data = font.embedded ? (await callWASM("ofdgoEditorFont", font.id.slice(9))).bytes : await fontManager.read(font);
|
const data = await readTextFont(font);
|
||||||
const face = new FontFace("ofdgo-edit-font", data);
|
const face = new FontFace("ofdgo-edit-font", data);
|
||||||
await face.load();
|
await face.load();
|
||||||
if (openSeq !== state.openSeq || canvasEditor.input !== editing) return;
|
if (openSeq !== state.openSeq || canvasEditor.input !== editing) return;
|
||||||
@@ -1056,7 +1067,7 @@ async function changeTextFont() {
|
|||||||
}
|
}
|
||||||
} else if (item?.type === "TextObject") {
|
} else if (item?.type === "TextObject") {
|
||||||
if (!font.embedded && canEditObject(item, "replaceFont")) {
|
if (!font.embedded && canEditObject(item, "replaceFont")) {
|
||||||
await changeDocument("ofdgoUpdateText", item, canEditObject(item, "layoutKnown") ? item.text : null, fontManager.read(font), item.size, null);
|
await changeDocument("ofdgoUpdateText", item, canEditObject(item, "layoutKnown") ? item.text : null, readTextFont(font), item.size, null);
|
||||||
updateTextFonts(canvasEditor.selected, true);
|
updateTextFonts(canvasEditor.selected, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1221,7 +1232,7 @@ function updateDrawingControls() {
|
|||||||
button.disabled = disabled || !pageCan("insert");
|
button.disabled = disabled || !pageCan("insert");
|
||||||
button.setAttribute("aria-pressed", String(tool === name));
|
button.setAttribute("aria-pressed", String(tool === name));
|
||||||
}
|
}
|
||||||
for (const [button, name] of [[el.eraseObjectButton, "erase-object"], [el.eraseRegionButton, "erase-region"]]) {
|
for (const [button, name] of [[el.eraseObjectButton, "erase-object"], [el.eraseRegionButton, "erase-region"], [el.erasePathButton, "erase-path"]]) {
|
||||||
button.disabled = disabled;
|
button.disabled = disabled;
|
||||||
button.setAttribute("aria-pressed", String(tool === name));
|
button.setAttribute("aria-pressed", String(tool === name));
|
||||||
}
|
}
|
||||||
@@ -1256,7 +1267,7 @@ async function changeDocument(name, item, ...args) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
setEditorInfo(doc);
|
setEditorInfo(doc);
|
||||||
if (!item || name === "ofdgoDeleteObject" || name === "ofdgoDeleteObjects" || name === "ofdgoEraseObjects") {
|
if (!item || name === "ofdgoDeleteObject" || name === "ofdgoDeleteObjects" || name === "ofdgoEraseObjects" || name === "ofdgoEraseObjectsPath") {
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
}
|
}
|
||||||
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||||
@@ -3021,7 +3032,7 @@ async function pasteEditorContent(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const style = currentTextStyle();
|
const style = currentTextStyle();
|
||||||
const data = font.embedded ? callWASM("ofdgoEditorFont", font.id.slice(9)).then(result => result.bytes) : fontManager.read(font);
|
const data = readTextFont(font);
|
||||||
await changeDocument("ofdgoInsertText", null, index, value, data, x, y, style.size, style.color,
|
await changeDocument("ofdgoInsertText", null, index, value, data, x, y, style.size, style.color,
|
||||||
page.width - x * 2, style.wrap, style.align || "left", style.paragraphHeight || 0, style.letterSpacing || 0);
|
page.width - x * 2, style.wrap, style.align || "left", style.paragraphHeight || 0, style.letterSpacing || 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -505,7 +505,7 @@ export class CanvasEditor {
|
|||||||
? drag.items.filter(item => item.x < drag.box.x + drag.box.width && item.x + item.width > drag.box.x
|
? drag.items.filter(item => item.x < drag.box.x + drag.box.width && item.x + item.width > drag.box.x
|
||||||
&& item.y < drag.box.y + drag.box.height && item.y + item.height > drag.box.y) : [];
|
&& item.y < drag.box.y + drag.box.height && item.y + item.height > drag.box.y) : [];
|
||||||
this.cancel();
|
this.cancel();
|
||||||
if (items.length) this.options.onErase(items, drag.erase === "erase-region" ? drag.box : null);
|
if (items.length && (!drag.points || drag.points.length >= 3)) this.options.onErase(items, drag.box, drag.points);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (drag.crop) {
|
if (drag.crop) {
|
||||||
@@ -591,6 +591,11 @@ export class CanvasEditor {
|
|||||||
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
||||||
if (this.tool === "erase-region") Object.assign(this.drag, this.createPreview(surface, page, "rectangle",
|
if (this.tool === "erase-region") Object.assign(this.drag, this.createPreview(surface, page, "rectangle",
|
||||||
{ fill: true, fillColor: "var(--accent-soft)", stroke: true, strokeColor: "var(--accent)", lineWidth: 0.2 }));
|
{ fill: true, fillColor: "var(--accent-soft)", stroke: true, strokeColor: "var(--accent)", lineWidth: 0.2 }));
|
||||||
|
if (this.tool === "erase-path") {
|
||||||
|
Object.assign(this.drag, this.createPreview(surface, page, "path",
|
||||||
|
{ fill: true, fillColor: "var(--accent-soft)", stroke: true, strokeColor: "var(--accent)", lineWidth: 0.2 }), { points: [] });
|
||||||
|
this.drag.node.setAttribute("fill-rule", "evenodd");
|
||||||
|
}
|
||||||
this.viewer.setPointerCapture(event.pointerId);
|
this.viewer.setPointerCapture(event.pointerId);
|
||||||
this.moveErase(event, this.drag);
|
this.moveErase(event, this.drag);
|
||||||
}
|
}
|
||||||
@@ -598,6 +603,18 @@ export class CanvasEditor {
|
|||||||
moveErase(event, drag) {
|
moveErase(event, drag) {
|
||||||
if (drag.erase === "erase-region") { this.moveShape(event, drag); return; }
|
if (drag.erase === "erase-region") { this.moveShape(event, drag); return; }
|
||||||
const point = pagePoint(event.clientX, event.clientY, drag.rect, drag.page, drag.rotation);
|
const point = pagePoint(event.clientX, event.clientY, drag.rect, drag.page, drag.rotation);
|
||||||
|
if (drag.points) {
|
||||||
|
const last = drag.lastPointer;
|
||||||
|
if (last && Math.hypot(event.clientX - last.x, event.clientY - last.y) < 2) return;
|
||||||
|
drag.lastPointer = { x: event.clientX, y: event.clientY };
|
||||||
|
drag.points.push(point);
|
||||||
|
const box = drag.box || { x: point.x, y: point.y, width: 0, height: 0 };
|
||||||
|
const right = Math.max(box.x + box.width, point.x), bottom = Math.max(box.y + box.height, point.y);
|
||||||
|
drag.box = { x: Math.min(box.x, point.x), y: Math.min(box.y, point.y) };
|
||||||
|
Object.assign(drag.box, { width: right - drag.box.x, height: bottom - drag.box.y });
|
||||||
|
drag.node.setAttribute("d", drag.points.map((point, index) => `${index ? "L" : "M"}${point.x} ${point.y}`).join(" ") + " Z");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const item = [...drag.items].reverse().find(item => point.x >= item.x && point.x <= item.x + item.width
|
const item = [...drag.items].reverse().find(item => point.x >= item.x && point.x <= item.x + item.width
|
||||||
&& point.y >= item.y && point.y <= item.y + item.height);
|
&& point.y >= item.y && point.y <= item.y + item.height);
|
||||||
if (item) { drag.erased.add(item); item.node.classList.add("erasing"); }
|
if (item) { drag.erased.add(item); item.node.classList.add("erasing"); }
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export class FontPicker {
|
|||||||
const normalize = value => value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/g, " ").trim();
|
const normalize = value => value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/g, " ").trim();
|
||||||
const terms = normalize(query).split(" ").filter(Boolean);
|
const terms = normalize(query).split(" ").filter(Boolean);
|
||||||
this.matches = this.fonts.map((font, index) => ({ font, index })).filter(({ font }) => {
|
this.matches = this.fonts.map((font, index) => ({ font, index })).filter(({ font }) => {
|
||||||
const name = normalize([font.fullName, font.name, font.family, font.postscriptName].filter(Boolean).join(" "));
|
const name = normalize([font.fullName, font.name, font.family, font.style, font.postscriptName, ...(font.names || [])].filter(Boolean).join(" "));
|
||||||
return terms.every(term => name.includes(term));
|
return terms.every(term => name.includes(term));
|
||||||
});
|
});
|
||||||
this.list.replaceChildren();
|
this.list.replaceChildren();
|
||||||
@@ -105,8 +105,10 @@ export class FontPicker {
|
|||||||
|
|
||||||
commitExact() {
|
commitExact() {
|
||||||
const value = this.input.value.trim().toLocaleLowerCase();
|
const value = this.input.value.trim().toLocaleLowerCase();
|
||||||
const index = this.fonts.findIndex(font => !font.disabled
|
const matches = font => font && !font.disabled
|
||||||
&& [font.fullName, font.name, font.postscriptName].some(name => name?.toLocaleLowerCase() === value));
|
&& [font.fullName, font.name, font.postscriptName, ...(font.names || [])].some(name => name?.toLocaleLowerCase() === value);
|
||||||
|
const selected = Number(this.value);
|
||||||
|
const index = matches(this.fonts[selected]) ? selected : this.fonts.findIndex(matches);
|
||||||
if (index >= 0) this.choose(index);
|
if (index >= 0) this.choose(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +285,22 @@ export class FontManager {
|
|||||||
return font.data;
|
return font.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editorFonts() {
|
||||||
|
return [...this.userFonts.filter(font => font.enabled).flatMap(file => {
|
||||||
|
if (!file.faces) return [file];
|
||||||
|
if (!file.faces.length) return [{ ...file, disabled: true }];
|
||||||
|
return file.faces.map(face => ({ ...face, id: face.index ? `${file.id}:${face.index}` : file.id, name: file.name, file }));
|
||||||
|
}), ...this.catalog];
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadFaces(inspect) {
|
||||||
|
for (const file of this.userFonts) {
|
||||||
|
if (!file.enabled || file.faces) continue;
|
||||||
|
try { file.faces = await inspect(await this.read(file)); }
|
||||||
|
catch { file.faces = []; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async add(fonts) {
|
async add(fonts) {
|
||||||
let saved = true;
|
let saved = true;
|
||||||
try {
|
try {
|
||||||
@@ -320,6 +338,7 @@ export class FontManager {
|
|||||||
const fonts = stored.map((font) => ({
|
const fonts = stored.map((font) => ({
|
||||||
...font,
|
...font,
|
||||||
data: previous.get(font.id)?.checksum === font.checksum ? previous.get(font.id).data : font.data,
|
data: previous.get(font.id)?.checksum === font.checksum ? previous.get(font.id).data : font.data,
|
||||||
|
faces: previous.get(font.id)?.checksum === font.checksum ? previous.get(font.id).faces : undefined,
|
||||||
source: "stored",
|
source: "stored",
|
||||||
}));
|
}));
|
||||||
fonts.push(...this.userFonts.filter((font) => font.source === "upload"));
|
fonts.push(...this.userFonts.filter((font) => font.source === "upload"));
|
||||||
|
|||||||
+65
-22
@@ -97,6 +97,8 @@ func RunWASM() {
|
|||||||
registerExportCallback("ofdgoExportDocument", exportDocument)
|
registerExportCallback("ofdgoExportDocument", exportDocument)
|
||||||
registerExportCallback("ofdgoExportAttachment", exportAttachment)
|
registerExportCallback("ofdgoExportAttachment", exportAttachment)
|
||||||
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
||||||
|
registerCallback("ofdgoFontFaces", fontFaces)
|
||||||
|
registerCallback("ofdgoFontFace", fontFace)
|
||||||
registerCallback("ofdgoCreateDocument", createDocument)
|
registerCallback("ofdgoCreateDocument", createDocument)
|
||||||
registerCallback("ofdgoEditDocument", editDocument)
|
registerCallback("ofdgoEditDocument", editDocument)
|
||||||
registerCallback("ofdgoChangePage", changePage)
|
registerCallback("ofdgoChangePage", changePage)
|
||||||
@@ -115,6 +117,7 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoAlignObjects", alignObjects)
|
registerCallback("ofdgoAlignObjects", alignObjects)
|
||||||
registerCallback("ofdgoDeleteObjects", deleteObjects)
|
registerCallback("ofdgoDeleteObjects", deleteObjects)
|
||||||
registerCallback("ofdgoEraseObjects", eraseObjects)
|
registerCallback("ofdgoEraseObjects", eraseObjects)
|
||||||
|
registerCallback("ofdgoEraseObjectsPath", eraseObjectsPath)
|
||||||
registerCallback("ofdgoCopyObjects", copyObjects)
|
registerCallback("ofdgoCopyObjects", copyObjects)
|
||||||
registerCallback("ofdgoCaptureObjects", captureObjects)
|
registerCallback("ofdgoCaptureObjects", captureObjects)
|
||||||
registerCallback("ofdgoPasteObjects", pasteObjects)
|
registerCallback("ofdgoPasteObjects", pasteObjects)
|
||||||
@@ -467,6 +470,32 @@ func matchFontFiles(args []js.Value) (any, error) {
|
|||||||
return currentSession.Reader.MatchFontFiles(stringsFromJS(args[0]))
|
return currentSession.Reader.MatchFontFiles(stringsFromJS(args[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fontFaces 读取上传字体的名称和集合索引
|
||||||
|
// 入参: args 字体数据
|
||||||
|
// 返回: any 字体列表, error 错误信息
|
||||||
|
func fontFaces(args []js.Value) (any, error) {
|
||||||
|
data, err := bytesFromJS(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return (ofdgo.FontFile{Data: data}).Faces()
|
||||||
|
}
|
||||||
|
|
||||||
|
// fontFace 提取编辑时选定的字体,供输入预览和保存共用
|
||||||
|
// 入参: args 字体数据和零起始索引
|
||||||
|
// 返回: any 独立字体数据, error 错误信息
|
||||||
|
func fontFace(args []js.Value) (any, error) {
|
||||||
|
data, err := bytesFromJS(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err = (ofdgo.FontFile{Data: data}).Face(args[1].Int())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return successResult(map[string]any{"bytes": bytesToJS(data)}), nil
|
||||||
|
}
|
||||||
|
|
||||||
// bytesFromJS 从浏览器值读取二进制数据
|
// bytesFromJS 从浏览器值读取二进制数据
|
||||||
// 入参: value 浏览器值
|
// 入参: value 浏览器值
|
||||||
// 返回: []byte 二进制数据, error 错误信息
|
// 返回: []byte 二进制数据, error 错误信息
|
||||||
@@ -535,13 +564,13 @@ func encodeResult(result apiResult) string {
|
|||||||
// currentEditor 当前编辑文档
|
// currentEditor 当前编辑文档
|
||||||
var currentEditor *ofdgo.Editor
|
var currentEditor *ofdgo.Editor
|
||||||
|
|
||||||
// editorClipboard 当前编辑文档中的对象快照与剪贴板标识。
|
// editorClipboard 当前编辑文档中的对象快照与剪贴板标识
|
||||||
type editorClipboard struct {
|
type editorClipboard struct {
|
||||||
token string
|
token string
|
||||||
objects []ofdgo.GraphicObject
|
objects []ofdgo.GraphicObject
|
||||||
}
|
}
|
||||||
|
|
||||||
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据。
|
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据
|
||||||
var copiedObjects *editorClipboard
|
var copiedObjects *editorClipboard
|
||||||
|
|
||||||
// editorInfo 编辑文档信息与操作状态
|
// editorInfo 编辑文档信息与操作状态
|
||||||
@@ -560,7 +589,7 @@ type editorPageInfo struct {
|
|||||||
PageIndex int `json:"pageIndex"`
|
PageIndex int `json:"pageIndex"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorSelectionInfo 对象复制结果及新选区。
|
// editorSelectionInfo 对象复制结果及新选区
|
||||||
type editorSelectionInfo struct {
|
type editorSelectionInfo struct {
|
||||||
editorInfo
|
editorInfo
|
||||||
SelectedIDs []string `json:"selectedIDs"`
|
SelectedIDs []string `json:"selectedIDs"`
|
||||||
@@ -851,7 +880,7 @@ func copyObjects(args []js.Value) (any, error) {
|
|||||||
return copyEditorObjects(args[0].Int(), objects, args[2].Float(), args[3].Float())
|
return copyEditorObjects(args[0].Int(), objects, args[2].Float(), args[3].Float())
|
||||||
}
|
}
|
||||||
|
|
||||||
// captureObjects 保存独立选区快照,不修改文档或历史。
|
// captureObjects 保存独立选区快照,不修改文档或历史
|
||||||
// 入参: args 页面索引、对象标识数组和剪贴板标识
|
// 入参: args 页面索引、对象标识数组和剪贴板标识
|
||||||
// 返回: any 空结果, error 错误信息
|
// 返回: any 空结果, error 错误信息
|
||||||
func captureObjects(args []js.Value) (any, error) {
|
func captureObjects(args []js.Value) (any, error) {
|
||||||
@@ -866,7 +895,7 @@ func captureObjects(args []js.Value) (any, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pasteObjects 将当前文档的对象快照粘贴到目标页。
|
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
||||||
// 入参: args 目标页、剪贴板标识和横纵位移
|
// 入参: args 目标页、剪贴板标识和横纵位移
|
||||||
// 返回: any 文档信息及新选区, error 错误信息
|
// 返回: any 文档信息及新选区, error 错误信息
|
||||||
func pasteObjects(args []js.Value) (any, error) {
|
func pasteObjects(args []js.Value) (any, error) {
|
||||||
@@ -876,7 +905,7 @@ func pasteObjects(args []js.Value) (any, error) {
|
|||||||
return copyEditorObjects(args[0].Int(), copiedObjects.objects, args[2].Float(), args[3].Float())
|
return copyEditorObjects(args[0].Int(), copiedObjects.objects, args[2].Float(), args[3].Float())
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyEditorObjects 复制快照并更新预览,返回新对象标识。
|
// copyEditorObjects 复制快照并更新预览,返回新对象标识
|
||||||
// 入参: page 目标页, objects 对象快照, dx、dy 毫米位移
|
// 入参: page 目标页, objects 对象快照, dx、dy 毫米位移
|
||||||
// 返回: any 文档信息及新选区, error 错误信息
|
// 返回: any 文档信息及新选区, error 错误信息
|
||||||
func copyEditorObjects(page int, objects []ofdgo.GraphicObject, dx, dy float64) (any, error) {
|
func copyEditorObjects(page int, objects []ofdgo.GraphicObject, dx, dy float64) (any, error) {
|
||||||
@@ -900,7 +929,7 @@ func orderObjects(args []js.Value) (any, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// distributeObjects 按指定轴等距分布选区。
|
// distributeObjects 按指定轴等距分布选区
|
||||||
// 入参: args 页面索引、对象标识数组和分布轴
|
// 入参: args 页面索引、对象标识数组和分布轴
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func distributeObjects(args []js.Value) (any, error) {
|
func distributeObjects(args []js.Value) (any, error) {
|
||||||
@@ -978,7 +1007,7 @@ func createDocument(args []js.Value) (any, error) {
|
|||||||
return previewEditor(editor, args[3].Bool())
|
return previewEditor(editor, args[3].Bool())
|
||||||
}
|
}
|
||||||
|
|
||||||
// editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置。
|
// editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置
|
||||||
// 入参: args 浏览器参数
|
// 入参: args 浏览器参数
|
||||||
// 返回: any 编辑状态, error 错误信息
|
// 返回: any 编辑状态, error 错误信息
|
||||||
func editDocument(args []js.Value) (any, error) {
|
func editDocument(args []js.Value) (any, error) {
|
||||||
@@ -1241,7 +1270,7 @@ func insertText(args []js.Value) (any, error) {
|
|||||||
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
||||||
}
|
}
|
||||||
|
|
||||||
// layoutText 调整文字框和段落排版,保留软换行前的原文。
|
// layoutText 调整文字框和段落排版,保留软换行前的原文
|
||||||
// 入参: args 页码、对象标识、本地左侧偏移、宽度、折行、对齐、行距和字距
|
// 入参: args 页码、对象标识、本地左侧偏移、宽度、折行、对齐、行距和字距
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func layoutText(args []js.Value) (any, error) {
|
func layoutText(args []js.Value) (any, error) {
|
||||||
@@ -1276,14 +1305,14 @@ func layoutText(args []js.Value) (any, error) {
|
|||||||
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
return previewEditor(currentEditor, currentSession.Renderer.RenderAnnotations)
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorBox 将库层毫米边界传递给画布交互层。
|
// editorBox 将库层毫米边界传递给画布交互层
|
||||||
// 入参: box 毫米坐标中的矩形范围
|
// 入参: box 毫米坐标中的矩形范围
|
||||||
// 返回: map[string]any 可直接传递给JavaScript的横纵坐标和宽高
|
// 返回: map[string]any 可直接传递给JavaScript的横纵坐标和宽高
|
||||||
func editorBox(box ofdgo.Box) map[string]any {
|
func editorBox(box ofdgo.Box) map[string]any {
|
||||||
return map[string]any{"x": box.X, "y": box.Y, "width": box.W, "height": box.H}
|
return map[string]any{"x": box.X, "y": box.Y, "width": box.W, "height": box.H}
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorMatrix 将标准CTM转换为浏览器仿射矩阵分量。
|
// editorMatrix 将标准CTM转换为浏览器仿射矩阵分量
|
||||||
// 入参: matrix 库层仿射矩阵
|
// 入参: matrix 库层仿射矩阵
|
||||||
// 返回: []any 可直接传递给JavaScript的a、b、c、d、e、f分量,位移单位为毫米
|
// 返回: []any 可直接传递给JavaScript的a、b、c、d、e、f分量,位移单位为毫米
|
||||||
func editorMatrix(matrix ofdgo.Matrix) []any {
|
func editorMatrix(matrix ofdgo.Matrix) []any {
|
||||||
@@ -1293,7 +1322,7 @@ func editorMatrix(matrix ofdgo.Matrix) []any {
|
|||||||
return []any{ax - x, ay - y, bx - x, by - y, x, y}
|
return []any{ax - x, ay - y, bx - x, by - y, x, y}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rotateObjects 旋转同页选区,通用几何与历史记录由库负责。
|
// rotateObjects 旋转同页选区,通用几何与历史记录由库负责
|
||||||
// 入参: args 页面索引、对象标识数组和顺时针角度
|
// 入参: args 页面索引、对象标识数组和顺时针角度
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func rotateObjects(args []js.Value) (any, error) {
|
func rotateObjects(args []js.Value) (any, error) {
|
||||||
@@ -1302,7 +1331,7 @@ func rotateObjects(args []js.Value) (any, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// flipObjects 镜像同页选区。
|
// flipObjects 镜像同页选区
|
||||||
// 入参: args 页面索引、对象标识数组和镜像轴
|
// 入参: args 页面索引、对象标识数组和镜像轴
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func flipObjects(args []js.Value) (any, error) {
|
func flipObjects(args []js.Value) (any, error) {
|
||||||
@@ -1311,7 +1340,7 @@ func flipObjects(args []js.Value) (any, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// cropImage 设置图片裁剪范围,保留原始图片数据。
|
// cropImage 设置图片裁剪范围,保留原始图片数据
|
||||||
// 入参: args 页面索引、对象标识及页面毫米坐标中的横纵坐标和宽高
|
// 入参: args 页面索引、对象标识及页面毫米坐标中的横纵坐标和宽高
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func cropImage(args []js.Value) (any, error) {
|
func cropImage(args []js.Value) (any, error) {
|
||||||
@@ -1320,7 +1349,7 @@ func cropImage(args []js.Value) (any, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// fitImage 按原始比例适应或填充图片框。
|
// fitImage 按原始比例适应或填充图片框
|
||||||
// 入参: args 页面索引、对象标识和适应方式
|
// 入参: args 页面索引、对象标识和适应方式
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func fitImage(args []js.Value) (any, error) {
|
func fitImage(args []js.Value) (any, error) {
|
||||||
@@ -1329,7 +1358,7 @@ func fitImage(args []js.Value) (any, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// previewImage 在页面副本中显示指定图片的完整内容,不改变正文、历史或缓存。
|
// previewImage 在页面副本中显示指定图片的完整内容,不改变正文、历史或缓存
|
||||||
// 入参: args 页面索引和图片对象标识
|
// 入参: args 页面索引和图片对象标识
|
||||||
// 返回: any 页面SVG和分离的图片资源, error 错误信息
|
// 返回: any 页面SVG和分离的图片资源, error 错误信息
|
||||||
func previewImage(args []js.Value) (any, error) {
|
func previewImage(args []js.Value) (any, error) {
|
||||||
@@ -1362,7 +1391,7 @@ func previewImage(args []js.Value) (any, error) {
|
|||||||
return successResult(map[string]any{"svg": out.String(), "images": svgImagesToJS(resources.Images)}), nil
|
return successResult(map[string]any{"svg": out.String(), "images": svgImagesToJS(resources.Images)}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// svgImagesToJS 将图片资源以二进制数组传给前端,不使用base64。
|
// svgImagesToJS 将图片资源以二进制数组传给前端,不使用base64
|
||||||
// 入参: images SVG引用的图片资源
|
// 入参: images SVG引用的图片资源
|
||||||
// 返回: []any 图片标识、类型和数据
|
// 返回: []any 图片标识、类型和数据
|
||||||
func svgImagesToJS(images []ofdgo.SVGImage) []any {
|
func svgImagesToJS(images []ofdgo.SVGImage) []any {
|
||||||
@@ -1373,7 +1402,7 @@ func svgImagesToJS(images []ofdgo.SVGImage) []any {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
// transformObjects 统一移动或缩放选区,生成一次撤销记录。
|
// transformObjects 统一移动或缩放选区,生成一次撤销记录
|
||||||
// 入参: args 页面索引、对象标识数组、横纵位移和正缩放比例
|
// 入参: args 页面索引、对象标识数组、横纵位移和正缩放比例
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func transformObjects(args []js.Value) (any, error) {
|
func transformObjects(args []js.Value) (any, error) {
|
||||||
@@ -1382,7 +1411,7 @@ func transformObjects(args []js.Value) (any, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// alignObjects 对齐选区中的对象。
|
// alignObjects 对齐选区中的对象
|
||||||
// 入参: args 页面索引、对象标识数组和对齐方式
|
// 入参: args 页面索引、对象标识数组和对齐方式
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func alignObjects(args []js.Value) (any, error) {
|
func alignObjects(args []js.Value) (any, error) {
|
||||||
@@ -1391,7 +1420,7 @@ func alignObjects(args []js.Value) (any, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteObjects 一次删除选区中的对象。
|
// deleteObjects 一次删除选区中的对象
|
||||||
// 入参: args 页面索引和对象标识数组
|
// 入参: args 页面索引和对象标识数组
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func deleteObjects(args []js.Value) (any, error) {
|
func deleteObjects(args []js.Value) (any, error) {
|
||||||
@@ -1400,7 +1429,21 @@ func deleteObjects(args []js.Value) (any, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// eraseObjects 按页面矩形范围擦除对象,保留局部裁剪之外的内容。
|
// eraseObjectsPath 按页面闭合折线范围擦除对象,保留局部裁剪之外的内容
|
||||||
|
// 入参: args 页面索引、对象标识数组和坐标点数组
|
||||||
|
// 返回: any 文档信息, error 错误信息
|
||||||
|
func eraseObjectsPath(args []js.Value) (any, error) {
|
||||||
|
return changeObjects(func() error {
|
||||||
|
points := make([]ofdgo.Point, args[2].Length())
|
||||||
|
for i := range points {
|
||||||
|
point := args[2].Index(i)
|
||||||
|
points[i] = ofdgo.Point{X: point.Get("x").Float(), Y: point.Get("y").Float()}
|
||||||
|
}
|
||||||
|
return currentEditor.EraseObjectsPath(args[0].Int(), stringsFromJS(args[1]), points)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// eraseObjects 按页面矩形范围擦除对象,保留局部裁剪之外的内容
|
||||||
// 入参: args 页面索引、对象标识数组和毫米坐标范围
|
// 入参: args 页面索引、对象标识数组和毫米坐标范围
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func eraseObjects(args []js.Value) (any, error) {
|
func eraseObjects(args []js.Value) (any, error) {
|
||||||
@@ -1411,7 +1454,7 @@ func eraseObjects(args []js.Value) (any, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// changeObjects 提交库层批量操作,无修改时复用当前预览。
|
// changeObjects 提交库层批量操作,无修改时复用当前预览
|
||||||
// 入参: apply 待执行的编辑操作
|
// 入参: apply 待执行的编辑操作
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
func changeObjects(apply func() error) (any, error) {
|
func changeObjects(apply func() error) (any, error) {
|
||||||
|
|||||||
+8
-16
@@ -278,9 +278,6 @@ func (e *Editor) nextID() string {
|
|||||||
// 入参: file 字体文件, index 集合内字体索引,单字体为0
|
// 入参: file 字体文件, index 集合内字体索引,单字体为0
|
||||||
// 返回: string 字体资源标识, error 错误信息
|
// 返回: string 字体资源标识, error 错误信息
|
||||||
func (e *Editor) AddFont(file FontFile, index int) (string, error) {
|
func (e *Editor) AddFont(file FontFile, index int) (string, error) {
|
||||||
if index < 0 {
|
|
||||||
return "", fmt.Errorf("font index must not be negative")
|
|
||||||
}
|
|
||||||
data, err := font.ToSFNT(file.Data)
|
data, err := font.ToSFNT(file.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -289,16 +286,11 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) {
|
|||||||
if id, ok := e.resourceID[key]; ok {
|
if id, ok := e.resourceID[key]; ok {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
if bytes.HasPrefix(data, []byte("ttcf")) {
|
data, err = (FontFile{Data: data}).Face(index)
|
||||||
data, err = extractCollectionFont(data, index)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
index = 0
|
sfnt, err := font.ParseSFNT(data, 0)
|
||||||
} else {
|
|
||||||
data = bytes.Clone(data)
|
|
||||||
}
|
|
||||||
sfnt, err := font.ParseSFNT(data, index)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -348,7 +340,7 @@ func editorFontName(sfnt *font.SFNT, names ...font.NameID) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddImage 注册PNG或JPEG图片,重复资源复用标识,引用后写入文档
|
// AddImage 注册PNG或JPEG图片,重复资源复用标识,引用后写入文档
|
||||||
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码。
|
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
|
||||||
// 入参: data 图片数据
|
// 入参: data 图片数据
|
||||||
// 返回: string 图片资源标识, error 错误信息
|
// 返回: string 图片资源标识, error 错误信息
|
||||||
func (e *Editor) AddImage(data []byte) (string, error) {
|
func (e *Editor) AddImage(data []byte) (string, error) {
|
||||||
@@ -389,7 +381,7 @@ func (e *Editor) AddImage(data []byte) (string, error) {
|
|||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorBinaryImage 尝试纯黑白无损编码,不二值化,不改变透明度,无体积收益时保留原图。
|
// editorBinaryImage 尝试纯黑白无损编码,不二值化,不改变透明度,无体积收益时保留原图
|
||||||
// 入参: data PNG图片数据
|
// 入参: data PNG图片数据
|
||||||
// 返回: []byte 更小的JBIG2数据,不适用时为nil
|
// 返回: []byte 更小的JBIG2数据,不适用时为nil
|
||||||
func editorBinaryImage(data []byte) []byte {
|
func editorBinaryImage(data []byte) []byte {
|
||||||
@@ -416,8 +408,8 @@ func (e *Editor) AddObject(page int, object GraphicObject) (string, error) {
|
|||||||
return ids[0], nil
|
return ids[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object 获取对象的独立副本,修改副本不影响文档。
|
// Object 获取对象的独立副本,修改副本不影响文档
|
||||||
// 可编辑的继承样式解析为直接属性;仅在对象修改或复制时写入独立样式。
|
// 可编辑的继承样式解析为直接属性;仅在对象修改或复制时写入独立样式
|
||||||
// 入参: page 页面索引, id 对象标识
|
// 入参: page 页面索引, id 对象标识
|
||||||
// 返回: GraphicObject 对象内容, error 错误信息
|
// 返回: GraphicObject 对象内容, error 错误信息
|
||||||
func (e *Editor) Object(page int, id string) (GraphicObject, error) {
|
func (e *Editor) Object(page int, id string) (GraphicObject, error) {
|
||||||
@@ -718,7 +710,7 @@ func (e *Editor) TransformObject(page int, id string, dx, dy, scale float64) err
|
|||||||
return e.TransformObjects(page, []string{id}, dx, dy, scale)
|
return e.TransformObjects(page, []string{id}, dx, dy, scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
// transformEditorObject 变换独立对象副本,保留绘制属性和编辑中的段落信息。
|
// transformEditorObject 变换独立对象副本,保留绘制属性和编辑中的段落信息
|
||||||
// 入参: object 对象副本, dx 横向位移, dy 纵向位移, scale 正缩放比例
|
// 入参: object 对象副本, dx 横向位移, dy 纵向位移, scale 正缩放比例
|
||||||
// 返回: GraphicObject 变换后的对象, error 错误信息
|
// 返回: GraphicObject 变换后的对象, error 错误信息
|
||||||
func transformEditorObject(object GraphicObject, dx, dy, scale float64) (GraphicObject, error) {
|
func transformEditorObject(object GraphicObject, dx, dy, scale float64) (GraphicObject, error) {
|
||||||
@@ -774,7 +766,7 @@ func transformEditorObject(object GraphicObject, dx, dy, scale float64) (Graphic
|
|||||||
return object, nil
|
return object, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReplaceImage 替换图片资源,保留对象标识和绘制顺序,一次撤销恢复资源及布局。
|
// ReplaceImage 替换图片资源,保留对象标识和绘制顺序,一次撤销恢复资源及布局
|
||||||
// 入参: page 页面索引, id 图片对象标识, data PNG或JPEG数据, fit 为contain、cover或空,空值保留原变换和裁剪
|
// 入参: page 页面索引, id 图片对象标识, data PNG或JPEG数据, fit 为contain、cover或空,空值保留原变换和裁剪
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) error {
|
func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) error {
|
||||||
|
|||||||
+20
-20
@@ -24,13 +24,13 @@ import (
|
|||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
)
|
)
|
||||||
|
|
||||||
// editorObjectPosition 对象所属图层及层内索引。
|
// editorObjectPosition 对象所属图层及层内索引
|
||||||
type editorObjectPosition struct {
|
type editorObjectPosition struct {
|
||||||
layer int
|
layer int
|
||||||
index int
|
index int
|
||||||
}
|
}
|
||||||
|
|
||||||
// compareEditorPosition 比较对象在页面结构中的先后顺序。
|
// compareEditorPosition 比较对象在页面结构中的先后顺序
|
||||||
// 入参: a、b 对象位置
|
// 入参: a、b 对象位置
|
||||||
// 返回: int 比较结果
|
// 返回: int 比较结果
|
||||||
func compareEditorPosition(a, b editorObjectPosition) int {
|
func compareEditorPosition(a, b editorObjectPosition) int {
|
||||||
@@ -40,7 +40,7 @@ func compareEditorPosition(a, b editorObjectPosition) int {
|
|||||||
return cmp.Compare(a.index, b.index)
|
return cmp.Compare(a.index, b.index)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Objects 按绘制顺序获取选区的独立快照,保留编辑中的段落信息。
|
// Objects 按绘制顺序获取选区的独立快照,保留编辑中的段落信息
|
||||||
// 入参: page 页面索引, ids 对象标识,不得重复
|
// 入参: page 页面索引, ids 对象标识,不得重复
|
||||||
// 返回: []GraphicObject 独立对象副本, error 错误信息
|
// 返回: []GraphicObject 独立对象副本, error 错误信息
|
||||||
func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) {
|
func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) {
|
||||||
@@ -67,8 +67,8 @@ func (e *Editor) Objects(page int, ids []string) ([]GraphicObject, error) {
|
|||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyObjects 将对象快照按输入顺序复制到目标页并平移,分配新ID,提交一次撤销记录。
|
// CopyObjects 将对象快照按输入顺序复制到目标页并平移,分配新ID,提交一次撤销记录
|
||||||
// 对象引用当前Editor已注册的资源,保留段落信息,不修改输入快照。
|
// 对象引用当前Editor已注册的资源,保留段落信息,不修改输入快照
|
||||||
// 入参: page 目标页面索引, objects 对象快照, dx、dy 毫米位移
|
// 入参: page 目标页面索引, objects 对象快照, dx、dy 毫米位移
|
||||||
// 返回: []string 按绘制顺序排列的新对象标识, error 错误信息
|
// 返回: []string 按绘制顺序排列的新对象标识, error 错误信息
|
||||||
func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64) ([]string, error) {
|
func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64) ([]string, error) {
|
||||||
@@ -132,7 +132,7 @@ func (e *Editor) CopyObjects(page int, objects []GraphicObject, dx, dy float64)
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectPosition 对象在图层或页块直接成员中的位置,Index从0开始,Count不含嵌套页块的对象。
|
// ObjectPosition 对象在图层或页块直接成员中的位置,Index从0开始,Count不含嵌套页块的对象
|
||||||
type ObjectPosition struct {
|
type ObjectPosition struct {
|
||||||
Layer string
|
Layer string
|
||||||
Container string
|
Container string
|
||||||
@@ -140,7 +140,7 @@ type ObjectPosition struct {
|
|||||||
Count int
|
Count int
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectPosition 获取对象的直接容器及排序位置。
|
// ObjectPosition 获取对象的直接容器及排序位置
|
||||||
// 入参: page 页面索引, id 对象标识
|
// 入参: page 页面索引, id 对象标识
|
||||||
// 返回: ObjectPosition 对象位置, error 错误信息
|
// 返回: ObjectPosition 对象位置, error 错误信息
|
||||||
func (e *Editor) ObjectPosition(page int, id string) (ObjectPosition, error) {
|
func (e *Editor) ObjectPosition(page int, id string) (ObjectPosition, error) {
|
||||||
@@ -162,7 +162,7 @@ func (e *Editor) ObjectPosition(page int, id string) (ObjectPosition, error) {
|
|||||||
return position, nil
|
return position, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// objectOrderIndexes 获取同一直接容器的对象索引,不跨越图层或页块。
|
// objectOrderIndexes 获取同一直接容器的对象索引,不跨越图层或页块
|
||||||
// 入参: page 页面索引, layer 图层, id 对象标识
|
// 入参: page 页面索引, layer 图层, id 对象标识
|
||||||
// 返回: *editorXML 原始容器, []int 对象索引
|
// 返回: *editorXML 原始容器, []int 对象索引
|
||||||
func (e *Editor) objectOrderIndexes(page int, layer *Layer, id string) (*editorXML, []int) {
|
func (e *Editor) objectOrderIndexes(page int, layer *Layer, id string) (*editorXML, []int) {
|
||||||
@@ -184,7 +184,7 @@ func (e *Editor) objectOrderIndexes(page int, layer *Layer, id string) (*editorX
|
|||||||
return parent, indexes
|
return parent, indexes
|
||||||
}
|
}
|
||||||
|
|
||||||
// OrderObjects 调整同一图层或页块内选区的绘制顺序,保留各组选中及未选中对象的相对顺序。
|
// OrderObjects 调整同一图层或页块内选区的绘制顺序,保留各组选中及未选中对象的相对顺序
|
||||||
// 入参: page 页面索引, ids 对象标识, order 为up、down、top或bottom
|
// 入参: page 页面索引, ids 对象标识, order 为up、down、top或bottom
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
||||||
@@ -248,8 +248,8 @@ func (e *Editor) OrderObjects(page int, ids []string, order string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DistributeObjects 按可见范围等距分布同页对象,固定两端对象,保留绘制顺序。
|
// DistributeObjects 按可见范围等距分布同页对象,固定两端对象,保留绘制顺序
|
||||||
// 不足三个对象时不修改;允许负间距,无法保持位置顺序的重叠布局返回错误。
|
// 不足三个对象时不修改;允许负间距,无法保持位置顺序的重叠布局返回错误
|
||||||
// 入参: page 页面索引, ids 对象标识, axis 为horizontal或vertical
|
// 入参: page 页面索引, ids 对象标识, axis 为horizontal或vertical
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
|
func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
|
||||||
@@ -317,7 +317,7 @@ func (e *Editor) DistributeObjects(page int, ids []string, axis string) error {
|
|||||||
return e.updateObjects(page, updates, true)
|
return e.updateObjects(page, updates, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作。
|
// replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作
|
||||||
// 入参: page 页面索引, layers 新图层列表
|
// 入参: page 页面索引, layers 新图层列表
|
||||||
func (e *Editor) replaceLayers(page int, layers []Layer) {
|
func (e *Editor) replaceLayers(page int, layers []Layer) {
|
||||||
before := copyEditorPage(e.pages[page]).Content.Layer
|
before := copyEditorPage(e.pages[page]).Content.Layer
|
||||||
@@ -335,14 +335,14 @@ func (e *Editor) replaceLayers(page int, layers []Layer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateObjects 原子替换同页对象,按各对象ID定位,全部校验通过后提交一次历史记录。
|
// UpdateObjects 原子替换同页对象,按各对象ID定位,全部校验通过后提交一次历史记录
|
||||||
// 入参: page 页面索引, objects 新对象内容,ID不得重复
|
// 入参: page 页面索引, objects 新对象内容,ID不得重复
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) UpdateObjects(page int, objects []GraphicObject) error {
|
func (e *Editor) UpdateObjects(page int, objects []GraphicObject) error {
|
||||||
return e.updateObjects(page, objects, false)
|
return e.updateObjects(page, objects, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateObjects 校验内容更新或纯几何变换,原子提交跨图层选区。
|
// updateObjects 校验内容更新或纯几何变换,原子提交跨图层选区
|
||||||
// 入参: page 页面索引, objects 新对象, geometry 是否保留原内容的几何操作
|
// 入参: page 页面索引, objects 新对象, geometry 是否保留原内容的几何操作
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) updateObjects(page int, objects []GraphicObject, geometry bool) error {
|
func (e *Editor) updateObjects(page int, objects []GraphicObject, geometry bool) error {
|
||||||
@@ -399,7 +399,7 @@ func (e *Editor) updateObjects(page int, objects []GraphicObject, geometry bool)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransformObjects 同页对象以页面原点等比缩放后统一平移,保持相对位置,一次撤销恢复全部。
|
// TransformObjects 同页对象以页面原点等比缩放后统一平移,保持相对位置,一次撤销恢复全部
|
||||||
// 入参: page 页面索引, ids 对象标识, dx、dy 位移, scale 正缩放比例
|
// 入参: page 页面索引, ids 对象标识, dx、dy 位移, scale 正缩放比例
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64) error {
|
func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64) error {
|
||||||
@@ -434,7 +434,7 @@ func (e *Editor) TransformObjects(page int, ids []string, dx, dy, scale float64)
|
|||||||
return e.updateObjects(page, objects, true)
|
return e.updateObjects(page, objects, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AlignObjects 单对象对齐页面,多对象相互对齐至选区边界,文字采用实际字形范围。
|
// AlignObjects 单对象对齐页面,多对象相互对齐至选区边界,文字采用实际字形范围
|
||||||
// 入参: page 页面索引, ids 对象标识, alignment 为left、center、right、top、middle或bottom
|
// 入参: page 页面索引, ids 对象标识, alignment 为left、center、right、top、middle或bottom
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
|
func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
|
||||||
@@ -484,7 +484,7 @@ func (e *Editor) AlignObjects(page int, ids []string, alignment string) error {
|
|||||||
return e.updateObjects(page, objects, true)
|
return e.updateObjects(page, objects, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部。
|
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部
|
||||||
// 入参: page 页面索引, ids 对象标识
|
// 入参: page 页面索引, ids 对象标识
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) DeleteObjects(page int, ids []string) error {
|
func (e *Editor) DeleteObjects(page int, ids []string) error {
|
||||||
@@ -511,7 +511,7 @@ func (e *Editor) DeleteObjects(page int, ids []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectedObjects 校验同页选择,返回内部只读对象与对应索引。
|
// selectedObjects 校验同页选择,返回内部只读对象与对应索引
|
||||||
// 入参: page 页面索引, ids 对象标识,不得重复
|
// 入参: page 页面索引, ids 对象标识,不得重复
|
||||||
// 返回: []GraphicObject 所选对象, []editorObjectPosition 图层和对象索引, error 错误信息
|
// 返回: []GraphicObject 所选对象, []editorObjectPosition 图层和对象索引, error 错误信息
|
||||||
func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []editorObjectPosition, error) {
|
func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []editorObjectPosition, error) {
|
||||||
@@ -541,7 +541,7 @@ func (e *Editor) selectedObjects(page int, ids []string) ([]GraphicObject, []edi
|
|||||||
return objects, indexes, nil
|
return objects, indexes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorObjectID 获取可创作对象的标准标识。
|
// editorObjectID 获取可创作对象的标准标识
|
||||||
// 入参: object 图形对象
|
// 入参: object 图形对象
|
||||||
// 返回: string 对象标识,不支持的类型返回空字符串
|
// 返回: string 对象标识,不支持的类型返回空字符串
|
||||||
func editorObjectID(object GraphicObject) string {
|
func editorObjectID(object GraphicObject) string {
|
||||||
@@ -558,7 +558,7 @@ func editorObjectID(object GraphicObject) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// objectBounds 批量获取对齐范围,复用同一渲染器的资源和字形缓存。
|
// objectBounds 批量获取对齐范围,复用同一渲染器的资源和字形缓存
|
||||||
// 入参: page 页面索引, objects 待度量的对象
|
// 入参: page 页面索引, objects 待度量的对象
|
||||||
// 返回: []Box 对象范围, error 错误信息
|
// 返回: []Box 对象范围, error 错误信息
|
||||||
func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error) {
|
func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error) {
|
||||||
|
|||||||
+70
-7
@@ -19,17 +19,55 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tdewolff/canvas"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EraseObjects 擦除同页对象与矩形区域重叠的可见部分,一次撤销恢复全部。
|
// EraseObjects 擦除同页对象与矩形区域重叠的可见部分,一次撤销恢复全部
|
||||||
// 全部覆盖时删除对象;局部覆盖使用OFD路径裁剪,保留原文字和资源,不用于敏感信息脱敏。
|
// 全部覆盖时删除对象;局部覆盖使用OFD路径裁剪,保留原文字和资源,不用于敏感信息脱敏
|
||||||
// 不改变未相交对象,无法保真编辑时返回错误且不提交修改。
|
// 不改变未相交对象,无法保真编辑时返回错误且不提交修改
|
||||||
// 入参: page 页面索引, ids 对象标识, box 页面毫米坐标中的擦除范围
|
// 入参: page 页面索引, ids 对象标识, box 页面毫米坐标中的擦除范围
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
||||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
if _, err := creationBox(editorBoxString(box)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return e.eraseObjects(page, ids, box, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EraseObjectsPath 擦除同页对象与闭合折线区域重叠的可见部分,末点自动连接起点
|
||||||
|
// 自交区域使用奇偶规则;复用矩形擦除的裁剪、原子修改和撤销逻辑,不用于敏感信息脱敏
|
||||||
|
// 入参: page 页面索引, ids 对象标识, points 页面毫米坐标,至少三个点
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) EraseObjectsPath(page int, ids []string, points []Point) error {
|
||||||
|
if len(points) < 3 {
|
||||||
|
return fmt.Errorf("erase path requires at least three points")
|
||||||
|
}
|
||||||
|
region := &canvas.Path{}
|
||||||
|
for i, point := range points {
|
||||||
|
if !finite(point.X) || !finite(point.Y) {
|
||||||
|
return fmt.Errorf("invalid erase path point %d", i)
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
region.MoveTo(point.X, point.Y)
|
||||||
|
} else {
|
||||||
|
region.LineTo(point.X, point.Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
region.Close()
|
||||||
|
region = region.Settle(canvas.EvenOdd)
|
||||||
|
if region.Empty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bounds := region.FastBounds()
|
||||||
|
return e.eraseObjects(page, ids, Box{X: bounds.X0, Y: bounds.Y0, W: bounds.W(), H: bounds.H()}, points, region)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eraseObjects 按矩形或闭合路径裁剪对象,只在全部对象处理成功后提交修改
|
||||||
|
// 入参: page 页面索引, ids 对象标识, box 区域边界, points 折线点, region 奇偶填充后的路径
|
||||||
|
// 返回: error 错误信息
|
||||||
|
func (e *Editor) eraseObjects(page int, ids []string, box Box, points []Point, region *canvas.Path) error {
|
||||||
objects, indexes, err := e.selectedObjects(page, ids)
|
objects, indexes, err := e.selectedObjects(page, ids)
|
||||||
if err != nil || len(objects) == 0 {
|
if err != nil || len(objects) == 0 {
|
||||||
return err
|
return err
|
||||||
@@ -48,7 +86,15 @@ func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
|||||||
if right <= x || bottom <= y {
|
if right <= x || bottom <= y {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if x == bounds.X && y == bounds.Y && right == bounds.X+bounds.W && bottom == bounds.Y+bounds.H {
|
covered := x == bounds.X && y == bounds.Y && right == bounds.X+bounds.W && bottom == bounds.Y+bounds.H
|
||||||
|
if region != nil {
|
||||||
|
path := canvas.Rectangle(bounds.W, bounds.H).Translate(bounds.X, bounds.Y)
|
||||||
|
if path.And(region).Empty() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
covered = path.Not(region).Empty()
|
||||||
|
}
|
||||||
|
if covered {
|
||||||
removed[ids[i]] = true
|
removed[ids[i]] = true
|
||||||
changed = true
|
changed = true
|
||||||
continue
|
continue
|
||||||
@@ -79,12 +125,16 @@ func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
clip := Clip{}
|
clip := Clip{}
|
||||||
for _, area := range []Box{
|
areas := []Box{
|
||||||
{X: outer.X, Y: outer.Y, W: outer.W, H: y - outer.Y},
|
{X: outer.X, Y: outer.Y, W: outer.W, H: y - outer.Y},
|
||||||
{X: outer.X, Y: bottom, W: outer.W, H: outer.Y + outer.H - bottom},
|
{X: outer.X, Y: bottom, W: outer.W, H: outer.Y + outer.H - bottom},
|
||||||
{X: outer.X, Y: y, W: x - outer.X, H: bottom - y},
|
{X: outer.X, Y: y, W: x - outer.X, H: bottom - y},
|
||||||
{X: right, Y: y, W: outer.X + outer.W - right, H: bottom - y},
|
{X: right, Y: y, W: outer.X + outer.W - right, H: bottom - y},
|
||||||
} {
|
}
|
||||||
|
if region != nil {
|
||||||
|
areas = []Box{unionTextBox(outer, box)}
|
||||||
|
}
|
||||||
|
for _, area := range areas {
|
||||||
if area.W <= 0 || area.H <= 0 {
|
if area.W <= 0 || area.H <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -95,6 +145,19 @@ func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
|||||||
}
|
}
|
||||||
fill, stroke := true, false
|
fill, stroke := true, false
|
||||||
path.Fill, path.Stroke = &fill, &stroke
|
path.Fill, path.Stroke = &fill, &stroke
|
||||||
|
if region != nil {
|
||||||
|
var data strings.Builder
|
||||||
|
data.WriteString(path.AbbreviatedData)
|
||||||
|
for i, point := range points {
|
||||||
|
command := "L"
|
||||||
|
if i == 0 {
|
||||||
|
command = "M"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&data, " %s %g %g", command, point.X-frame.X-area.X, point.Y-frame.Y-area.Y)
|
||||||
|
}
|
||||||
|
data.WriteString(" C")
|
||||||
|
path.AbbreviatedData, path.Rule = data.String(), "Even-Odd"
|
||||||
|
}
|
||||||
clip.Area = append(clip.Area, ClipArea{CTM: matrix.String(), Path: []PathObject{path}})
|
clip.Area = append(clip.Area, ClipArea{CTM: matrix.String(), Path: []PathObject{path}})
|
||||||
}
|
}
|
||||||
if slices.ContainsFunc((*clips).Clip, func(previous Clip) bool { return reflect.DeepEqual(previous, clip) }) {
|
if slices.ContainsFunc((*clips).Clip, func(previous Clip) bool { return reflect.DeepEqual(previous, clip) }) {
|
||||||
@@ -120,7 +183,7 @@ func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorObjectClips 获取基本对象的裁剪字段。
|
// editorObjectClips 获取基本对象的裁剪字段
|
||||||
// 入参: object 文字、路径或图片对象
|
// 入参: object 文字、路径或图片对象
|
||||||
// 返回: **Clips 裁剪字段地址,不支持的对象返回nil
|
// 返回: **Clips 裁剪字段地址,不支持的对象返回nil
|
||||||
func editorObjectClips(object *GraphicObject) **Clips {
|
func editorObjectClips(object *GraphicObject) **Clips {
|
||||||
|
|||||||
+156
-2
@@ -16,13 +16,167 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/tdewolff/font"
|
"github.com/tdewolff/font"
|
||||||
)
|
)
|
||||||
|
|
||||||
// subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,不修改编辑资源或原文档字体。
|
// editorFontUsage 汇总原文档字体的字符、显式字形及无法确定的引用
|
||||||
|
type editorFontUsage struct {
|
||||||
|
chars map[rune]bool
|
||||||
|
glyphs map[uint16]bool
|
||||||
|
unsafe bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// newEditorFontUsage 创建字体用字记录
|
||||||
|
// 返回: *editorFontUsage 用字记录
|
||||||
|
func newEditorFontUsage() *editorFontUsage {
|
||||||
|
return &editorFontUsage{chars: make(map[rune]bool), glyphs: make(map[uint16]bool)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fontUsage 获取规范化资源标识对应的字体用字记录
|
||||||
|
// 入参: id 字体资源标识
|
||||||
|
// 返回: *editorFontUsage 用字记录
|
||||||
|
func (r *editorResourceRefs) fontUsage(id string) *editorFontUsage {
|
||||||
|
if r.fonts == nil {
|
||||||
|
r.fonts = make(map[string]*editorFontUsage)
|
||||||
|
}
|
||||||
|
id = editorResourceID(id)
|
||||||
|
if r.fonts[id] == nil {
|
||||||
|
r.fonts[id] = newEditorFontUsage()
|
||||||
|
}
|
||||||
|
return r.fonts[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
// text 收集TextCode字符和CGTransform显式字形,异常编号使该字体保持原样
|
||||||
|
// 入参: key 元素名称, value 元素文本
|
||||||
|
func (u *editorFontUsage) text(key, value string) {
|
||||||
|
switch key {
|
||||||
|
case "TextCode":
|
||||||
|
for _, char := range textCodeRunes(value) {
|
||||||
|
u.chars[char] = true
|
||||||
|
}
|
||||||
|
case "Glyphs":
|
||||||
|
for _, value := range strings.Fields(strings.ReplaceAll(value, ",", " ")) {
|
||||||
|
id, err := strconv.ParseUint(value, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
u.unsafe = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.glyphs[uint16(id)] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge 合并指向同一字体文件的全部资源用字
|
||||||
|
// 入参: other 用字记录, unsafe 是否存在无法确定的引用
|
||||||
|
func (u *editorFontUsage) merge(other *editorFontUsage, unsafe bool) {
|
||||||
|
u.unsafe = u.unsafe || unsafe
|
||||||
|
if other != nil {
|
||||||
|
u.unsafe = u.unsafe || other.unsafe
|
||||||
|
maps.Copy(u.chars, other.chars)
|
||||||
|
maps.Copy(u.glyphs, other.glyphs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// subsetSourceFont 裁剪静态TrueType原有字体,保留字形编号、复合依赖、度量及提示指令
|
||||||
|
// 集合、字形替换、可变、彩色和未知表保持原样,不对不完整的引用或异常字体猜测修复
|
||||||
|
// 入参: data 字体数据, usage 全包用字记录
|
||||||
|
// 返回: []byte 更小的字体子集,无确定收益时为空
|
||||||
|
func subsetSourceFont(data []byte, usage *editorFontUsage) []byte {
|
||||||
|
if usage.unsafe || len(usage.chars)+len(usage.glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
sfnt, err := font.ParseSFNT(data, 0)
|
||||||
|
if err != nil || !sfnt.IsTrueType {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for tag := range sfnt.Tables {
|
||||||
|
switch tag {
|
||||||
|
case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "DSIG", "FFTM":
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var unicodeTables []uint16
|
||||||
|
subtables := make(map[uint32]uint16)
|
||||||
|
for i, record := range sfnt.Cmap.EncodingRecords {
|
||||||
|
if record.Format == 14 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
offset := binary.BigEndian.Uint32(sfnt.Tables["cmap"][8+8*i:])
|
||||||
|
index, ok := subtables[offset]
|
||||||
|
if !ok {
|
||||||
|
index = uint16(len(subtables))
|
||||||
|
subtables[offset] = index
|
||||||
|
}
|
||||||
|
if record.PlatformID == 0 || record.PlatformID == 3 && (record.EncodingID == 1 || record.EncodingID == 10) {
|
||||||
|
unicodeTables = append(unicodeTables, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(unicodeTables) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
glyphs := maps.Clone(usage.glyphs)
|
||||||
|
glyphs[0] = true
|
||||||
|
mapping := make(map[rune]uint16, len(usage.chars))
|
||||||
|
for char := range usage.chars {
|
||||||
|
id := sfnt.GlyphIndex(char)
|
||||||
|
for _, index := range unicodeTables {
|
||||||
|
if alternate, ok := sfnt.Cmap.Subtables[index].Get(char); ok && alternate != id {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glyphs[id] = true
|
||||||
|
if id != 0 {
|
||||||
|
mapping[char] = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id := range glyphs {
|
||||||
|
if id >= sfnt.NumGlyphs() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dependencies, err := sfnt.Glyf.Dependencies(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, dependency := range dependencies {
|
||||||
|
glyphs[dependency] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var glyf, loca []byte
|
||||||
|
for id := 0; id <= int(sfnt.NumGlyphs()); id++ {
|
||||||
|
if sfnt.Head.IndexToLocFormat == 0 {
|
||||||
|
loca = binary.BigEndian.AppendUint16(loca, uint16(len(glyf)/2))
|
||||||
|
} else {
|
||||||
|
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
|
||||||
|
}
|
||||||
|
if id < int(sfnt.NumGlyphs()) && glyphs[uint16(id)] {
|
||||||
|
glyf = append(glyf, sfnt.Glyf.Get(uint16(id))...)
|
||||||
|
if len(glyf)%2 != 0 {
|
||||||
|
glyf = append(glyf, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tables := maps.Clone(sfnt.Tables)
|
||||||
|
tables["glyf"], tables["loca"] = glyf, loca
|
||||||
|
tables["cmap"] = buildCmapTable(uint16(sfnt.NumGlyphs()), mapping)
|
||||||
|
tables["head"] = bytes.Clone(tables["head"])
|
||||||
|
clear(tables["head"][8:12])
|
||||||
|
delete(tables, "DSIG")
|
||||||
|
result, err := serializeOTF(tables)
|
||||||
|
if err != nil || len(result) >= len(data) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,不修改编辑资源或原文档字体
|
||||||
// 返回: map[string][]byte 包内字体子集, error 错误信息
|
// 返回: map[string][]byte 包内字体子集, error 错误信息
|
||||||
func (e *Editor) subsetFonts() (map[string][]byte, error) {
|
func (e *Editor) subsetFonts() (map[string][]byte, error) {
|
||||||
used := make(map[string]map[uint16]bool)
|
used := make(map[string]map[uint16]bool)
|
||||||
@@ -77,7 +231,7 @@ func (e *Editor) subsetFonts() (map[string][]byte, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// subsetEditorFont 裁剪TrueType字形及复合依赖,保留字符映射、名称、度量和提示指令。
|
// subsetEditorFont 裁剪TrueType字形及复合依赖,保留字符映射、名称、度量和提示指令
|
||||||
// 入参: data 字体数据, glyphs 已排序的字形编号,包含0
|
// 入参: data 字体数据, glyphs 已排序的字形编号,包含0
|
||||||
// 返回: []byte 字体子集, error 错误信息
|
// 返回: []byte 字体子集, error 错误信息
|
||||||
func subsetEditorFont(data []byte, glyphs []uint16) ([]byte, error) {
|
func subsetEditorFont(data []byte, glyphs []uint16) ([]byte, error) {
|
||||||
|
|||||||
+30
-30
@@ -28,7 +28,7 @@ import (
|
|||||||
"github.com/tdewolff/font"
|
"github.com/tdewolff/font"
|
||||||
)
|
)
|
||||||
|
|
||||||
// editorSource 保留输入包及按需加载的原始页面。
|
// editorSource 保留输入包及按需加载的原始页面
|
||||||
type editorSource struct {
|
type editorSource struct {
|
||||||
reader *Reader
|
reader *Reader
|
||||||
document *Document
|
document *Document
|
||||||
@@ -39,7 +39,7 @@ type editorSource struct {
|
|||||||
idsReady bool
|
idsReady bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用。
|
// editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用
|
||||||
type editorObjectOrigin struct {
|
type editorObjectOrigin struct {
|
||||||
page *editorSourcePage
|
page *editorSourcePage
|
||||||
node *editorXML
|
node *editorXML
|
||||||
@@ -47,7 +47,7 @@ type editorObjectOrigin struct {
|
|||||||
reason string
|
reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorSourcePage 保存页面原文、解析快照及图层和页块内的对象位置。
|
// editorSourcePage 保存页面原文、解析快照及图层和页块内的对象位置
|
||||||
type editorSourcePage struct {
|
type editorSourcePage struct {
|
||||||
ref Page
|
ref Page
|
||||||
data []byte
|
data []byte
|
||||||
@@ -56,9 +56,9 @@ type editorSourcePage struct {
|
|||||||
nodes map[string]*editorXML
|
nodes map[string]*editorXML
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因。
|
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因
|
||||||
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块。
|
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
|
||||||
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复。
|
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
|
||||||
type ObjectCapabilities struct {
|
type ObjectCapabilities struct {
|
||||||
Update bool
|
Update bool
|
||||||
ReplaceFont bool
|
ReplaceFont bool
|
||||||
@@ -72,7 +72,7 @@ type ObjectCapabilities struct {
|
|||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageCapabilities 页面可执行的操作,Insert表示可以添加RGB对象;新增空白页面始终可用。
|
// PageCapabilities 页面可执行的操作,Insert表示可以添加RGB对象;新增空白页面始终可用
|
||||||
type PageCapabilities struct {
|
type PageCapabilities struct {
|
||||||
Insert bool
|
Insert bool
|
||||||
Copy bool
|
Copy bool
|
||||||
@@ -81,9 +81,9 @@ type PageCapabilities struct {
|
|||||||
Resize bool
|
Resize bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Editor 打开主文档进行保真编辑,不修改输入包,不以权限声明或签名限制编辑。
|
// Editor 打开主文档进行保真编辑,不修改输入包,不以权限声明或签名限制编辑
|
||||||
// 原权限和签名数据保留,修改受保护内容会使原签名失效,不重新签名。
|
// 原权限和签名数据保留,修改受保护内容会使原签名失效,不重新签名
|
||||||
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader。
|
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader
|
||||||
// 返回: *Editor 编辑器, error 错误信息
|
// 返回: *Editor 编辑器, error 错误信息
|
||||||
func (r *Reader) Editor() (*Editor, error) {
|
func (r *Reader) Editor() (*Editor, error) {
|
||||||
if r.Zip != nil {
|
if r.Zip != nil {
|
||||||
@@ -129,7 +129,7 @@ func (r *Reader) Editor() (*Editor, error) {
|
|||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorDirectoryExists 判断包内目录是否已经使用,避免覆盖原始资源。
|
// editorDirectoryExists 判断包内目录是否已经使用,避免覆盖原始资源
|
||||||
// 入参: reader 输入包, directory 目录路径
|
// 入参: reader 输入包, directory 目录路径
|
||||||
// 返回: bool 是否存在
|
// 返回: bool 是否存在
|
||||||
func editorDirectoryExists(reader *Reader, directory string) bool {
|
func editorDirectoryExists(reader *Reader, directory string) bool {
|
||||||
@@ -146,7 +146,7 @@ func editorDirectoryExists(reader *Reader, directory string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepareSourceIDs 首次新增内容前流式核对原包标识,不依赖可能缺失或过期的MaxUnitID。
|
// prepareSourceIDs 首次新增内容前流式核对原包标识,不依赖可能缺失或过期的MaxUnitID
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) prepareSourceIDs() error {
|
func (e *Editor) prepareSourceIDs() error {
|
||||||
if e.source == nil || e.source.idsReady {
|
if e.source == nil || e.source.idsReady {
|
||||||
@@ -180,7 +180,7 @@ func (e *Editor) prepareSourceIDs() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLMaxID 流式读取OFD结构中的最大标识,跳过非OFD的XML附件。
|
// editorXMLMaxID 流式读取OFD结构中的最大标识,跳过非OFD的XML附件
|
||||||
// 入参: input XML输入流
|
// 入参: input XML输入流
|
||||||
// 返回: int 最大标识, error 错误信息
|
// 返回: int 最大标识, error 错误信息
|
||||||
func editorXMLMaxID(input io.Reader) (int, error) {
|
func editorXMLMaxID(input io.Reader) (int, error) {
|
||||||
@@ -217,7 +217,7 @@ func editorXMLMaxID(input io.Reader) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// resourceDirectory 获取新增二进制资源的目录。
|
// resourceDirectory 获取新增二进制资源的目录
|
||||||
// 返回: string 包内路径
|
// 返回: string 包内路径
|
||||||
func (e *Editor) resourceDirectory() string {
|
func (e *Editor) resourceDirectory() string {
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
@@ -226,14 +226,14 @@ func (e *Editor) resourceDirectory() string {
|
|||||||
return "Doc_0/Res"
|
return "Doc_0/Res"
|
||||||
}
|
}
|
||||||
|
|
||||||
// originalPage 判断索引是否指向原文档页面。
|
// originalPage 判断索引是否指向原文档页面
|
||||||
// 入参: index 页面索引
|
// 入参: index 页面索引
|
||||||
// 返回: bool 是否为原始页面
|
// 返回: bool 是否为原始页面
|
||||||
func (e *Editor) originalPage(index int) bool {
|
func (e *Editor) originalPage(index int) bool {
|
||||||
return e.source != nil && index >= 0 && index < len(e.pages) && e.source.pages[e.pages[index].ID] != nil
|
return e.source != nil && index >= 0 && index < len(e.pages) && e.source.pages[e.pages[index].ID] != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageCapabilities 获取页面操作范围;原始页面暂不复制、删除,避免破坏模板与外部引用。
|
// PageCapabilities 获取页面操作范围;原始页面暂不复制、删除,避免破坏模板与外部引用
|
||||||
// 入参: index 页面索引
|
// 入参: index 页面索引
|
||||||
// 返回: PageCapabilities 操作能力, error 错误信息
|
// 返回: PageCapabilities 操作能力, error 错误信息
|
||||||
func (e *Editor) PageCapabilities(index int) (PageCapabilities, error) {
|
func (e *Editor) PageCapabilities(index int) (PageCapabilities, error) {
|
||||||
@@ -244,7 +244,7 @@ func (e *Editor) PageCapabilities(index int) (PageCapabilities, error) {
|
|||||||
return PageCapabilities{Insert: e.sourceRGB(), Copy: !original, Delete: !original, Move: true, Resize: true}, nil
|
return PageCapabilities{Insert: e.sourceRGB(), Copy: !original, Delete: !original, Move: true, Resize: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceRGB 判断新建RGB对象能否直接使用文档默认颜色空间。
|
// sourceRGB 判断新建RGB对象能否直接使用文档默认颜色空间
|
||||||
// 返回: bool 是否为8位RGB文档
|
// 返回: bool 是否为8位RGB文档
|
||||||
func (e *Editor) sourceRGB() bool {
|
func (e *Editor) sourceRGB() bool {
|
||||||
if e.source == nil || e.source.document.CommonData.DefaultCS == 0 {
|
if e.source == nil || e.source.document.CommonData.DefaultCS == 0 {
|
||||||
@@ -254,7 +254,7 @@ func (e *Editor) sourceRGB() bool {
|
|||||||
return space != nil && space.Type == "RGB" && (space.BitsPerComponent == 0 || space.BitsPerComponent == 8) && len(space.Palette) == 0
|
return space != nil && space.Type == "RGB" && (space.BitsPerComponent == 0 || space.BitsPerComponent == 8) && len(space.Palette) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadSourcePage 首次访问时解析原始页面,不加载图片或字体二进制数据。
|
// loadSourcePage 首次访问时解析原始页面,不加载图片或字体二进制数据
|
||||||
// 入参: index 页面索引
|
// 入参: index 页面索引
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) loadSourcePage(index int) error {
|
func (e *Editor) loadSourcePage(index int) error {
|
||||||
@@ -310,9 +310,9 @@ func (e *Editor) loadSourcePage(index int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectCapabilities 获取对象可执行的操作,不支持的内容保持原文,不强制转换。
|
// ObjectCapabilities 获取对象可执行的操作,不支持的内容保持原文,不强制转换
|
||||||
// 字体缺失或子集不包含原文时仍可移动、删除,不允许重新排版或复制。
|
// 字体缺失或子集不包含原文时仍可移动、删除,不允许重新排版或复制
|
||||||
// 显式提供可用字体后可通过UpdateObject替换;原文布局未知时不自动推断段落选项。
|
// 显式提供可用字体后可通过UpdateObject替换;原文布局未知时不自动推断段落选项
|
||||||
// 入参: page 页面索引, id 对象标识
|
// 入参: page 页面索引, id 对象标识
|
||||||
// 返回: ObjectCapabilities 操作能力, error 错误信息
|
// 返回: ObjectCapabilities 操作能力, error 错误信息
|
||||||
func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, error) {
|
func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, error) {
|
||||||
@@ -365,7 +365,7 @@ func (e *Editor) ObjectCapabilities(page int, id string) (ObjectCapabilities, er
|
|||||||
return all, nil
|
return all, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// objectOrigin 获取原对象或其副本的保真来源。
|
// objectOrigin 获取原对象或其副本的保真来源
|
||||||
// 入参: id 对象标识
|
// 入参: id 对象标识
|
||||||
// 返回: *editorObjectOrigin 原始对象,无来源时为nil
|
// 返回: *editorObjectOrigin 原始对象,无来源时为nil
|
||||||
func (e *Editor) objectOrigin(id string) *editorObjectOrigin {
|
func (e *Editor) objectOrigin(id string) *editorObjectOrigin {
|
||||||
@@ -375,7 +375,7 @@ func (e *Editor) objectOrigin(id string) *editorObjectOrigin {
|
|||||||
return e.source.origins[id]
|
return e.source.origins[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorContainerOrderable 判断容器的直接对象能否在保留页块的前提下排序。
|
// editorContainerOrderable 判断容器的直接对象能否在保留页块的前提下排序
|
||||||
// 入参: container 图层或页块
|
// 入参: container 图层或页块
|
||||||
// 返回: bool 是否可排序
|
// 返回: bool 是否可排序
|
||||||
func editorContainerOrderable(container *editorXML) bool {
|
func editorContainerOrderable(container *editorXML) bool {
|
||||||
@@ -387,7 +387,7 @@ func editorContainerOrderable(container *editorXML) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorGeometry 获取基本对象的边界与变换。
|
// editorGeometry 获取基本对象的边界与变换
|
||||||
// 入参: object 对象
|
// 入参: object 对象
|
||||||
// 返回: string 边界, string 变换矩阵
|
// 返回: string 边界, string 变换矩阵
|
||||||
func editorGeometry(object GraphicObject) (string, string) {
|
func editorGeometry(object GraphicObject) (string, string) {
|
||||||
@@ -402,7 +402,7 @@ func editorGeometry(object GraphicObject) (string, string) {
|
|||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorFont 按需解析原文档内嵌字体,不用系统回退替换原字体。
|
// editorFont 按需解析原文档内嵌字体,不用系统回退替换原字体
|
||||||
// 入参: id 字体资源标识
|
// 入参: id 字体资源标识
|
||||||
// 返回: *font.SFNT 字体, error 错误信息
|
// 返回: *font.SFNT 字体, error 错误信息
|
||||||
func (e *Editor) editorFont(id string) (*font.SFNT, error) {
|
func (e *Editor) editorFont(id string) (*font.SFNT, error) {
|
||||||
@@ -431,7 +431,7 @@ func (e *Editor) editorFont(id string) (*font.SFNT, error) {
|
|||||||
return nil, fmt.Errorf("embedded font %q not found", id)
|
return nil, fmt.Errorf("embedded font %q not found", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorImage 按需读取原图片尺寸,不解码完整像素。
|
// editorImage 按需读取原图片尺寸,不解码完整像素
|
||||||
// 入参: id 图片资源标识
|
// 入参: id 图片资源标识
|
||||||
// 返回: image.Point 图片尺寸, error 错误信息
|
// 返回: image.Point 图片尺寸, error 错误信息
|
||||||
func (e *Editor) editorImage(id string) (image.Point, error) {
|
func (e *Editor) editorImage(id string) (image.Point, error) {
|
||||||
@@ -456,14 +456,14 @@ func (e *Editor) editorImage(id string) (image.Point, error) {
|
|||||||
return size, nil
|
return size, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// cloneEditorData 深复制编辑快照,未导出的排版信息保持不可变共享。
|
// cloneEditorData 深复制编辑快照,未导出的排版信息保持不可变共享
|
||||||
// 入参: value 原值
|
// 入参: value 原值
|
||||||
// 返回: T 独立副本
|
// 返回: T 独立副本
|
||||||
func cloneEditorData[T any](value T) T {
|
func cloneEditorData[T any](value T) T {
|
||||||
return cloneEditorValue(reflect.ValueOf(value)).Interface().(T)
|
return cloneEditorValue(reflect.ValueOf(value)).Interface().(T)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cloneEditorValue 复制结构体中的可变指针和切片。
|
// cloneEditorValue 复制结构体中的可变指针和切片
|
||||||
// 入参: value 原值
|
// 入参: value 原值
|
||||||
// 返回: reflect.Value 独立副本
|
// 返回: reflect.Value 独立副本
|
||||||
func cloneEditorValue(value reflect.Value) reflect.Value {
|
func cloneEditorValue(value reflect.Value) reflect.Value {
|
||||||
@@ -495,7 +495,7 @@ func cloneEditorValue(value reflect.Value) reflect.Value {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorObjectXML 编码可编辑对象,包含独立命名空间声明。
|
// editorObjectXML 编码可编辑对象,包含独立命名空间声明
|
||||||
// 入参: object 对象
|
// 入参: object 对象
|
||||||
// 返回: []byte XML片段, error 错误信息
|
// 返回: []byte XML片段, error 错误信息
|
||||||
func editorObjectXML(object GraphicObject) ([]byte, error) {
|
func editorObjectXML(object GraphicObject) ([]byte, error) {
|
||||||
@@ -503,7 +503,7 @@ func editorObjectXML(object GraphicObject) ([]byte, error) {
|
|||||||
return bytes.TrimPrefix(data, []byte(xml.Header)), err
|
return bytes.TrimPrefix(data, []byte(xml.Header)), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateEditorGeometry 校验几何操作产生的数值,不重新排版或替换字体。
|
// validateEditorGeometry 校验几何操作产生的数值,不重新排版或替换字体
|
||||||
// 入参: object 变换后的对象
|
// 入参: object 变换后的对象
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func validateEditorGeometry(object GraphicObject) error {
|
func validateEditorGeometry(object GraphicObject) error {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sourceParts 生成编辑过的XML和新增资源,不读取未修改页面或原始二进制资源。
|
// sourceParts 生成编辑过的XML和新增资源,不读取未修改页面或原始二进制资源
|
||||||
// 返回: map[string][]byte 替换及新增条目, error 错误信息
|
// 返回: map[string][]byte 替换及新增条目, error 错误信息
|
||||||
func (e *Editor) sourceParts() (map[string][]byte, error) {
|
func (e *Editor) sourceParts() (map[string][]byte, error) {
|
||||||
parts := make(map[string][]byte)
|
parts := make(map[string][]byte)
|
||||||
@@ -171,7 +171,7 @@ func (e *Editor) sourceParts() (map[string][]byte, error) {
|
|||||||
return parts, nil
|
return parts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourcePageXML 仅改写已修改的对象、页面尺寸和新增图层,保留原页块层级。
|
// sourcePageXML 仅改写已修改的对象、页面尺寸和新增图层,保留原页块层级
|
||||||
// 入参: index 页面索引, source 原页面
|
// 入参: index 页面索引, source 原页面
|
||||||
// 返回: []byte 页面XML, error 错误信息
|
// 返回: []byte 页面XML, error 错误信息
|
||||||
func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, error) {
|
func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, error) {
|
||||||
@@ -290,7 +290,7 @@ func (e *Editor) sourcePageXML(index int, source *editorSourcePage) ([]byte, err
|
|||||||
return editorPatchXML(data, patches), nil
|
return editorPatchXML(data, patches), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceLayerXML 写出新增图层,对复制对象保留原始定位和显式默认值。
|
// sourceLayerXML 写出新增图层,对复制对象保留原始定位和显式默认值
|
||||||
// 入参: layer 图层
|
// 入参: layer 图层
|
||||||
// 返回: []byte 图层XML, error 错误信息
|
// 返回: []byte 图层XML, error 错误信息
|
||||||
func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) {
|
func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) {
|
||||||
@@ -314,7 +314,7 @@ func (e *Editor) sourceLayerXML(layer Layer) ([]byte, error) {
|
|||||||
return editorXMLContainer("Layer", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: layer.ID}, {Name: xml.Name{Local: "Type"}, Value: layer.Type}}, content)
|
return editorXMLContainer("Layer", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: layer.ID}, {Name: xml.Name{Local: "Type"}, Value: layer.Type}}, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceNewPageXML 编码新增页面,同时保留复制对象的原文语义。
|
// sourceNewPageXML 编码新增页面,同时保留复制对象的原文语义
|
||||||
// 入参: page 页面
|
// 入参: page 页面
|
||||||
// 返回: []byte 页面XML, error 错误信息
|
// 返回: []byte 页面XML, error 错误信息
|
||||||
func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
|
func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
|
||||||
@@ -337,7 +337,7 @@ func (e *Editor) sourceNewPageXML(page PageContent) ([]byte, error) {
|
|||||||
return editorXMLContainer("Page", nil, append(area, content...))
|
return editorXMLContainer("Page", nil, append(area, content...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceInfoXML 更新公开元数据,保留原创建程序和未识别字段。
|
// sourceInfoXML 更新公开元数据,保留原创建程序和未识别字段
|
||||||
// 返回: []byte 根索引XML, error 错误信息
|
// 返回: []byte 根索引XML, error 错误信息
|
||||||
func (e *Editor) sourceInfoXML() ([]byte, error) {
|
func (e *Editor) sourceInfoXML() ([]byte, error) {
|
||||||
data, err := e.source.reader.readFile("OFD.xml")
|
data, err := e.source.reader.readFile("OFD.xml")
|
||||||
@@ -373,7 +373,7 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
|
|||||||
return editorXMLSetText(data, info, values), nil
|
return editorXMLSetText(data, info, values), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本。
|
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本
|
||||||
// 入参: writer 输出流, fonts 新增字体子集
|
// 入参: writer 输出流, fonts 新增字体子集
|
||||||
// 返回: int64 写入字节数, error 错误信息
|
// 返回: int64 写入字节数, error 错误信息
|
||||||
func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte) (int64, error) {
|
func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte) (int64, error) {
|
||||||
@@ -382,7 +382,7 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte) (int64,
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
maps.Copy(parts, fonts)
|
maps.Copy(parts, fonts)
|
||||||
removed, err := e.pruneSourceResources(parts)
|
removed, err := e.compactSourceResources(parts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -445,7 +445,7 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte) (int64,
|
|||||||
return output.count, err
|
return output.count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourceReader 生成原包与修改条目组成的独立预览快照,不进行ZIP压缩。
|
// sourceReader 生成原包与修改条目组成的独立预览快照,不进行ZIP压缩
|
||||||
// 返回: *Reader 阅读器, error 错误信息
|
// 返回: *Reader 阅读器, error 错误信息
|
||||||
func (e *Editor) sourceReader() (*Reader, error) {
|
func (e *Editor) sourceReader() (*Reader, error) {
|
||||||
parts, err := e.sourceParts()
|
parts, err := e.sourceParts()
|
||||||
|
|||||||
+63
-11
@@ -26,18 +26,19 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活。
|
// editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活
|
||||||
type editorResourceRefs struct {
|
type editorResourceRefs struct {
|
||||||
ids map[string]bool
|
ids map[string]bool
|
||||||
files map[string]bool
|
files map[string]bool
|
||||||
|
fonts map[string]*editorFontUsage
|
||||||
}
|
}
|
||||||
|
|
||||||
// pruneSourceResources 在保存副本中清理无引用字体和图片,保留其他资源及原始编辑状态。
|
// compactSourceResources 在保存副本中清理无引用字体和图片,并裁剪可完整确认用字的原有字体
|
||||||
// 按GB/T 33190-2016附录A检查标识和路径引用,包括模板、注释、底纹、裁剪、版本和其他文档。
|
// 按GB/T 33190-2016附录A检查标识和路径引用,包括模板、注释、底纹、裁剪、版本和其他文档
|
||||||
// 全包检查保留孤立XML中的引用;未知命名空间、扩展数据或无法解析的XML使本次清理跳过。
|
// 全包检查保留孤立XML中的引用;未知命名空间、扩展数据或无法解析的XML使本次清理跳过
|
||||||
// 入参: parts 已修改和新增的包内条目
|
// 入参: parts 已修改和新增的包内条目
|
||||||
// 返回: map[string]bool 可移除的二进制条目, error 读取错误
|
// 返回: map[string]bool 可移除的二进制条目, error 读取错误
|
||||||
func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool, error) {
|
func (e *Editor) compactSourceResources(parts map[string][]byte) (map[string]bool, error) {
|
||||||
if len(parts) == 0 {
|
if len(parts) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool,
|
|||||||
for name := range parts {
|
for name := range parts {
|
||||||
names[name] = true
|
names[name] = true
|
||||||
}
|
}
|
||||||
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool)}
|
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
|
||||||
var resources []string
|
var resources []string
|
||||||
for _, name := range slices.Sorted(maps.Keys(names)) {
|
for _, name := range slices.Sorted(maps.Keys(names)) {
|
||||||
if strings.HasSuffix(name, "/") {
|
if strings.HasSuffix(name, "/") {
|
||||||
@@ -80,6 +81,7 @@ func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool,
|
|||||||
removed := make(map[string]bool)
|
removed := make(map[string]bool)
|
||||||
usedFiles := maps.Clone(refs.files)
|
usedFiles := maps.Clone(refs.files)
|
||||||
updates := make(map[string][]byte)
|
updates := make(map[string][]byte)
|
||||||
|
fontFiles := make(map[string]*editorFontUsage)
|
||||||
for _, name := range resources {
|
for _, name := range resources {
|
||||||
data, ok := parts[name]
|
data, ok := parts[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -114,9 +116,21 @@ func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool,
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if location := editorResourceLocation(name, root.attr("BaseLoc"), value); location != "" {
|
if location := editorResourceLocation(name, root.attr("BaseLoc"), value); location != "" {
|
||||||
|
alternate := strings.ToLower(cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value)))
|
||||||
|
if field == "FontFile" {
|
||||||
|
usage := refs.fonts[id]
|
||||||
|
charset := node.attr("Charset")
|
||||||
|
unsafe := id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate
|
||||||
|
for _, file := range []string{location, alternate} {
|
||||||
|
if fontFiles[file] == nil {
|
||||||
|
fontFiles[file] = newEditorFontUsage()
|
||||||
|
}
|
||||||
|
fontFiles[file].merge(usage, unsafe || keep && usage == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
if keep {
|
if keep {
|
||||||
usedFiles[location] = true
|
usedFiles[location] = true
|
||||||
usedFiles[strings.ToLower(cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value)))] = true
|
usedFiles[alternate] = true
|
||||||
} else {
|
} else {
|
||||||
removed[location] = true
|
removed[location] = true
|
||||||
}
|
}
|
||||||
@@ -139,6 +153,20 @@ func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool,
|
|||||||
for name := range usedFiles {
|
for name := range usedFiles {
|
||||||
delete(removed, name)
|
delete(removed, name)
|
||||||
}
|
}
|
||||||
|
for name := range names {
|
||||||
|
key := strings.ToLower(cleanPackagePath(name))
|
||||||
|
usage := fontFiles[key]
|
||||||
|
if usage == nil || usage.unsafe || refs.files[key] || removed[key] || parts[name] != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data, err := reader.readFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if subset := subsetSourceFont(data, usage); subset != nil {
|
||||||
|
updates[name] = subset
|
||||||
|
}
|
||||||
|
}
|
||||||
maps.Copy(parts, updates)
|
maps.Copy(parts, updates)
|
||||||
for name := range parts {
|
for name := range parts {
|
||||||
if removed[strings.ToLower(cleanPackagePath(name))] {
|
if removed[strings.ToLower(cleanPackagePath(name))] {
|
||||||
@@ -148,7 +176,7 @@ func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool,
|
|||||||
return removed, nil
|
return removed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// scan 流式读取包内XML引用,二进制条目只探测文件头,不展开页面对象或图片像素。
|
// scan 流式读取包内XML引用,二进制条目只探测文件头,不展开页面对象或图片像素
|
||||||
// 入参: input 文件流, name 包内路径
|
// 入参: input 文件流, name 包内路径
|
||||||
// 返回: bool 是否资源索引, bool 是否可完整判断引用
|
// 返回: bool 是否资源索引, bool 是否可完整判断引用
|
||||||
func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
||||||
@@ -166,6 +194,7 @@ func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
|||||||
}
|
}
|
||||||
decoder := xml.NewDecoder(buffer)
|
decoder := xml.NewDecoder(buffer)
|
||||||
var stack []string
|
var stack []string
|
||||||
|
var fonts []*editorFontUsage
|
||||||
var text strings.Builder
|
var text strings.Builder
|
||||||
resource, base := false, ""
|
resource, base := false, ""
|
||||||
for {
|
for {
|
||||||
@@ -194,6 +223,13 @@ func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var usage *editorFontUsage
|
||||||
|
if len(fonts) != 0 {
|
||||||
|
usage = fonts[len(fonts)-1]
|
||||||
|
if usage != nil && (stack[len(stack)-1] == "TextCode" || stack[len(stack)-1] == "Glyphs") {
|
||||||
|
usage.unsafe = true
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, attr := range token.Attr {
|
for _, attr := range token.Attr {
|
||||||
if attr.Name.Space == "xmlns" || attr.Name.Local == "xmlns" || attr.Name.Space == "http://www.w3.org/XML/1998/namespace" {
|
if attr.Name.Space == "xmlns" || attr.Name.Local == "xmlns" || attr.Name.Space == "http://www.w3.org/XML/1998/namespace" {
|
||||||
continue
|
continue
|
||||||
@@ -204,14 +240,30 @@ func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
|||||||
if attr.Name.Local != "ID" {
|
if attr.Name.Local != "ID" {
|
||||||
r.reference(name, base, attr.Name.Local, attr.Value, resource)
|
r.reference(name, base, attr.Name.Local, attr.Value, resource)
|
||||||
}
|
}
|
||||||
|
if attr.Name.Local == "Font" {
|
||||||
|
usage = r.fontUsage(attr.Value)
|
||||||
|
if token.Name.Local != "TextObject" && token.Name.Local != "Text" {
|
||||||
|
usage.unsafe = true
|
||||||
|
}
|
||||||
|
} else if attr.Name.Local == "Substitution" {
|
||||||
|
r.fontUsage(attr.Value).unsafe = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
stack = append(stack, token.Name.Local)
|
stack = append(stack, token.Name.Local)
|
||||||
|
fonts = append(fonts, usage)
|
||||||
text.Reset()
|
text.Reset()
|
||||||
case xml.CharData:
|
case xml.CharData:
|
||||||
text.Write(token)
|
text.Write(token)
|
||||||
case xml.EndElement:
|
case xml.EndElement:
|
||||||
r.reference(name, base, token.Name.Local, text.String(), resource)
|
r.reference(name, base, token.Name.Local, text.String(), resource)
|
||||||
|
if (token.Name.Local == "Substitution" || token.Name.Local == "Font") && strings.TrimSpace(text.String()) != "" {
|
||||||
|
r.fontUsage(text.String()).unsafe = true
|
||||||
|
}
|
||||||
|
if usage := fonts[len(fonts)-1]; usage != nil {
|
||||||
|
usage.text(token.Name.Local, text.String())
|
||||||
|
}
|
||||||
stack = stack[:len(stack)-1]
|
stack = stack[:len(stack)-1]
|
||||||
|
fonts = fonts[:len(fonts)-1]
|
||||||
text.Reset()
|
text.Reset()
|
||||||
case xml.Directive:
|
case xml.Directive:
|
||||||
return false, false
|
return false, false
|
||||||
@@ -219,7 +271,7 @@ func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// reference 记录标准资源标识和路径引用,不将普通数字或资源自身ID视为引用。
|
// reference 记录标准资源标识和路径引用,不将普通数字或资源自身ID视为引用
|
||||||
// 入参: name 当前文件, base 资源目录, key 字段, value 值, resource 是否资源索引
|
// 入参: name 当前文件, base 资源目录, key 字段, value 值, resource 是否资源索引
|
||||||
func (r *editorResourceRefs) reference(name, base, key, value string, resource bool) {
|
func (r *editorResourceRefs) reference(name, base, key, value string, resource bool) {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
@@ -248,7 +300,7 @@ func (r *editorResourceRefs) reference(name, base, key, value string, resource b
|
|||||||
r.files[strings.ToLower(cleanPackagePath(resolveResourcePath(name, base, value)))] = true
|
r.files[strings.ToLower(cleanPackagePath(resolveResourcePath(name, base, value)))] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorResourceID 规范化ST_ID与ST_RefID的十进制表示。
|
// editorResourceID 规范化ST_ID与ST_RefID的十进制表示
|
||||||
// 入参: value 标识文本
|
// 入参: value 标识文本
|
||||||
// 返回: string 标准十进制标识,无效时为空
|
// 返回: string 标准十进制标识,无效时为空
|
||||||
func editorResourceID(value string) string {
|
func editorResourceID(value string) string {
|
||||||
@@ -259,7 +311,7 @@ func editorResourceID(value string) string {
|
|||||||
return strconv.FormatUint(id, 10)
|
return strconv.FormatUint(id, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorResourceLocation 按ST_Loc与Res.BaseLoc解析路径,大小写折叠用于保守保留共享文件。
|
// editorResourceLocation 按ST_Loc与Res.BaseLoc解析路径,大小写折叠用于保守保留共享文件
|
||||||
// 入参: name 所在XML, base 资源目录, value 路径值
|
// 入参: name 所在XML, base 资源目录, value 路径值
|
||||||
// 返回: string 包内路径
|
// 返回: string 包内路径
|
||||||
func editorResourceLocation(name, base, value string) string {
|
func editorResourceLocation(name, base, value string) string {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// editorDrawParam 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照。
|
// editorDrawParam 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照
|
||||||
// 入参: id 绘制参数标识, visited 已访问标识
|
// 入参: id 绘制参数标识, visited 已访问标识
|
||||||
// 返回: *DrawParam 合并参数, error 错误信息
|
// 返回: *DrawParam 合并参数, error 错误信息
|
||||||
func (e *Editor) editorDrawParam(id string, visited map[string]bool) (*DrawParam, error) {
|
func (e *Editor) editorDrawParam(id string, visited map[string]bool) (*DrawParam, error) {
|
||||||
@@ -35,7 +35,7 @@ func (e *Editor) editorDrawParam(id string, visited map[string]bool) (*DrawParam
|
|||||||
return mergeDrawParam(*base, dp), nil
|
return mergeDrawParam(*base, dp), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveEditorStyle 为对象生成独立的有效样式,不改写共享资源或原XML。
|
// resolveEditorStyle 为对象生成独立的有效样式,不改写共享资源或原XML
|
||||||
// 入参: object 原对象, layer 图层绘制参数标识
|
// 入参: object 原对象, layer 图层绘制参数标识
|
||||||
// 返回: GraphicObject 编辑快照, error 错误信息
|
// 返回: GraphicObject 编辑快照, error 错误信息
|
||||||
func (e *Editor) resolveEditorStyle(object GraphicObject, layer string) (GraphicObject, error) {
|
func (e *Editor) resolveEditorStyle(object GraphicObject, layer string) (GraphicObject, error) {
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import (
|
|||||||
"github.com/go-text/typesetting/segmenter"
|
"github.com/go-text/typesetting/segmenter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TextLayout 本地横向段落选项,Wrap按CTM变换前的边界宽度折行,Align为left、center、right或justify。
|
// TextLayout 本地横向段落选项,Wrap按CTM变换前的边界宽度折行,Align为left、center、right或justify
|
||||||
// LineHeight为毫米单位的基线间距,0使用字体度量;LetterSpacing为字素间的附加毫米间距,可为负。
|
// LineHeight为毫米单位的基线间距,0使用字体度量;LetterSpacing为字素间的附加毫米间距,可为负
|
||||||
// 零值保持显式换行和左对齐。
|
// 零值保持显式换行和左对齐
|
||||||
type TextLayout struct {
|
type TextLayout struct {
|
||||||
Wrap bool
|
Wrap bool
|
||||||
Align string
|
Align string
|
||||||
@@ -30,14 +30,14 @@ type TextLayout struct {
|
|||||||
LetterSpacing float64
|
LetterSpacing float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// textLayout 保存编辑中的原文与选项,不写入OFD,也不参与渲染。
|
// textLayout 保存编辑中的原文与选项,不写入OFD,也不参与渲染
|
||||||
type textLayout struct {
|
type textLayout struct {
|
||||||
value string
|
value string
|
||||||
options TextLayout
|
options TextLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextLayout 获取最近一次排版的原文与选项,区分软换行和显式换行。
|
// TextLayout 获取最近一次排版的原文与选项,区分软换行和显式换行
|
||||||
// 信息仅在当前编辑过程保留,读取OFD时返回定位后的文字和默认选项。
|
// 信息仅在当前编辑过程保留,读取OFD时返回定位后的文字和默认选项
|
||||||
// 返回: string 原文, TextLayout 排版选项
|
// 返回: string 原文, TextLayout 排版选项
|
||||||
func (obj TextObject) TextLayout() (string, TextLayout) {
|
func (obj TextObject) TextLayout() (string, TextLayout) {
|
||||||
if obj.layout != nil {
|
if obj.layout != nil {
|
||||||
@@ -46,7 +46,7 @@ func (obj TextObject) TextLayout() (string, TextLayout) {
|
|||||||
return obj.Text(), TextLayout{}
|
return obj.Text(), TextLayout{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// breakTextLines 优先使用Unicode断行机会,过长词仅在字素边界折行,不丢弃空白。
|
// breakTextLines 优先使用Unicode断行机会,过长词仅在字素边界折行,不丢弃空白
|
||||||
// 入参: runes 段落字符, advances 含字距的字符步进, width 本地排版宽度, wrap 是否自动折行, spacing 附加字距
|
// 入参: runes 段落字符, advances 含字距的字符步进, width 本地排版宽度, wrap 是否自动折行, spacing 附加字距
|
||||||
// 返回: [][2]int 各行的字符起止索引,左闭右开
|
// 返回: [][2]int 各行的字符起止索引,左闭右开
|
||||||
func breakTextLines(runes []rune, advances []float64, width float64, wrap bool, spacing float64) [][2]int {
|
func breakTextLines(runes []rune, advances []float64, width float64, wrap bool, spacing float64) [][2]int {
|
||||||
@@ -106,7 +106,7 @@ func breakTextLines(runes []rune, advances []float64, width float64, wrap bool,
|
|||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
// alignTextLine 将段落对齐转换为标准X和DeltaX,两端对齐保留段落末行左对齐。
|
// alignTextLine 将段落对齐转换为标准X和DeltaX,两端对齐保留段落末行左对齐
|
||||||
// 入参: runes 行内字符, advances 含字距的字符步进, width 本地排版宽度, alignment 对齐方式, justify 是否允许本行两端对齐, spacing 附加字距
|
// 入参: runes 行内字符, advances 含字距的字符步进, width 本地排版宽度, alignment 对齐方式, justify 是否允许本行两端对齐, spacing 附加字距
|
||||||
// 返回: float64 行首X坐标, string 字符间的DeltaX序列
|
// 返回: float64 行首X坐标, string 字符间的DeltaX序列
|
||||||
func alignTextLine(runes []rune, advances []float64, width float64, alignment string, justify bool, spacing float64) (float64, string) {
|
func alignTextLine(runes []rune, advances []float64, width float64, alignment string, justify bool, spacing float64) (float64, string) {
|
||||||
@@ -150,7 +150,7 @@ func alignTextLine(runes []rune, advances []float64, width float64, alignment st
|
|||||||
return x, strings.Join(deltas, " ")
|
return x, strings.Join(deltas, " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// spaceTextAdvances 将字距加入各字素末尾,不拆散组合字符。
|
// spaceTextAdvances 将字距加入各字素末尾,不拆散组合字符
|
||||||
// 入参: runes 段落字符, advances 待更新的字符步进, spacing 附加毫米字距
|
// 入参: runes 段落字符, advances 待更新的字符步进, spacing 附加毫米字距
|
||||||
func spaceTextAdvances(runes []rune, advances []float64, spacing float64) {
|
func spaceTextAdvances(runes []rune, advances []float64, spacing float64) {
|
||||||
if spacing == 0 {
|
if spacing == 0 {
|
||||||
|
|||||||
+20
-20
@@ -20,15 +20,15 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RotateObject 绕对象可见范围中心旋转。
|
// RotateObject 绕对象可见范围中心旋转
|
||||||
// 入参: page 页面索引, id 对象标识, degrees 顺时针角度
|
// 入参: page 页面索引, id 对象标识, degrees 顺时针角度
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) RotateObject(page int, id string, degrees int) error {
|
func (e *Editor) RotateObject(page int, id string, degrees int) error {
|
||||||
return e.RotateObjects(page, []string{id}, degrees)
|
return e.RotateObjects(page, []string{id}, degrees)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RotateObjects 绕同页选区的可见范围中心旋转。
|
// RotateObjects 绕同页选区的可见范围中心旋转
|
||||||
// 保留相对位置及资源,一次撤销恢复全部。
|
// 保留相对位置及资源,一次撤销恢复全部
|
||||||
// 入参: page 页面索引, ids 对象标识, degrees 顺时针角度,仅支持90度的整数倍
|
// 入参: page 页面索引, ids 对象标识, degrees 顺时针角度,仅支持90度的整数倍
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) RotateObjects(page int, ids []string, degrees int) error {
|
func (e *Editor) RotateObjects(page int, ids []string, degrees int) error {
|
||||||
@@ -40,15 +40,15 @@ func (e *Editor) RotateObjects(page int, ids []string, degrees int) error {
|
|||||||
return e.orientObjects(page, ids, m)
|
return e.orientObjects(page, ids, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlipObject 绕对象可见范围中心镜像。
|
// FlipObject 绕对象可见范围中心镜像
|
||||||
// 入参: page 页面索引, id 对象标识, axis 为horizontal或vertical
|
// 入参: page 页面索引, id 对象标识, axis 为horizontal或vertical
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) FlipObject(page int, id, axis string) error {
|
func (e *Editor) FlipObject(page int, id, axis string) error {
|
||||||
return e.FlipObjects(page, []string{id}, axis)
|
return e.FlipObjects(page, []string{id}, axis)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlipObjects 以同页选区中心镜像,保留资源及绘制顺序,一次撤销恢复全部。
|
// FlipObjects 以同页选区中心镜像,保留资源及绘制顺序,一次撤销恢复全部
|
||||||
// 以对象可见范围计算选区。
|
// 以对象可见范围计算选区
|
||||||
// 入参: page 页面索引, ids 对象标识, axis 为horizontal或vertical
|
// 入参: page 页面索引, ids 对象标识, axis 为horizontal或vertical
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) FlipObjects(page int, ids []string, axis string) error {
|
func (e *Editor) FlipObjects(page int, ids []string, axis string) error {
|
||||||
@@ -64,7 +64,7 @@ func (e *Editor) FlipObjects(page int, ids []string, axis string) error {
|
|||||||
return e.orientObjects(page, ids, m)
|
return e.orientObjects(page, ids, m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// orientObjects 更新Boundary与CTM,不重排文字或重采样图片。
|
// orientObjects 更新Boundary与CTM,不重排文字或重采样图片
|
||||||
// 入参: page 页面索引, ids 对象标识, matrix 直角旋转或镜像矩阵
|
// 入参: page 页面索引, ids 对象标识, matrix 直角旋转或镜像矩阵
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) orientObjects(page int, ids []string, matrix Matrix) error {
|
func (e *Editor) orientObjects(page int, ids []string, matrix Matrix) error {
|
||||||
@@ -110,7 +110,7 @@ func (e *Editor) orientObjects(page int, ids []string, matrix Matrix) error {
|
|||||||
return e.updateObjects(page, objects, true)
|
return e.updateObjects(page, objects, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// transformObjectClips 变换裁剪区域,不修改原始裁剪数据。
|
// transformObjectClips 变换裁剪区域,不修改原始裁剪数据
|
||||||
// 入参: clips 裁剪集合, matrix 对象边界坐标中的变换
|
// 入参: clips 裁剪集合, matrix 对象边界坐标中的变换
|
||||||
// 返回: *Clips 变换后的裁剪集合
|
// 返回: *Clips 变换后的裁剪集合
|
||||||
func transformObjectClips(clips *Clips, matrix Matrix) *Clips {
|
func transformObjectClips(clips *Clips, matrix Matrix) *Clips {
|
||||||
@@ -126,7 +126,7 @@ func transformObjectClips(clips *Clips, matrix Matrix) *Clips {
|
|||||||
return &result
|
return &result
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageBounds 返回完整图片经Boundary位移及CTM变换后的轴对齐边界,不应用裁剪或父级变换。
|
// ImageBounds 返回完整图片经Boundary位移及CTM变换后的轴对齐边界,不应用裁剪或父级变换
|
||||||
// 返回: Box 所在坐标系中的完整图片边界, error 错误信息
|
// 返回: Box 所在坐标系中的完整图片边界, error 错误信息
|
||||||
func (obj ImageObject) ImageBounds() (Box, error) {
|
func (obj ImageObject) ImageBounds() (Box, error) {
|
||||||
box, err := creationBox(obj.Boundary)
|
box, err := creationBox(obj.Boundary)
|
||||||
@@ -142,8 +142,8 @@ func (obj ImageObject) ImageBounds() (Box, error) {
|
|||||||
return TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(obj.CTM)).TransformBox(Box{W: 1, H: 1}), nil
|
return TranslationMatrix(box.X, box.Y).Multiply(NewMatrix(obj.CTM)).TransformBox(Box{W: 1, H: 1}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CropImage 按页面毫米坐标重设图片裁剪,保留原始资源及像素,可再次扩大裁剪区域。
|
// CropImage 按页面毫米坐标重设图片裁剪,保留原始资源及像素,可再次扩大裁剪区域
|
||||||
// 使用ImageObject.ImageBounds返回的范围可还原完整图片;提交一次撤销记录。
|
// 使用ImageObject.ImageBounds返回的范围可还原完整图片;提交一次撤销记录
|
||||||
// 入参: page 页面索引, id 图片对象标识, box 页面毫米坐标中的保留范围
|
// 入参: page 页面索引, id 图片对象标识, box 页面毫米坐标中的保留范围
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) CropImage(page int, id string, box Box) error {
|
func (e *Editor) CropImage(page int, id string, box Box) error {
|
||||||
@@ -161,7 +161,7 @@ func (e *Editor) CropImage(page int, id string, box Box) error {
|
|||||||
return e.UpdateObject(page, id, object)
|
return e.UpdateObject(page, id, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cropImageObject 更新图片副本的裁剪和局部坐标,不修改资源。
|
// cropImageObject 更新图片副本的裁剪和局部坐标,不修改资源
|
||||||
// 入参: image 图片对象副本, box 页面毫米坐标中的保留范围
|
// 入参: image 图片对象副本, box 页面毫米坐标中的保留范围
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func cropImageObject(image *ImageObject, box Box) error {
|
func cropImageObject(image *ImageObject, box Box) error {
|
||||||
@@ -200,8 +200,8 @@ func cropImageObject(image *ImageObject, box Box) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FitImage 按原始像素比例居中适应或填充当前Boundary,保留直角旋转及镜像方向。
|
// FitImage 按原始像素比例居中适应或填充当前Boundary,保留直角旋转及镜像方向
|
||||||
// 保留原始图片,不重采样;重设裁剪,一次撤销恢复原布局。
|
// 保留原始图片,不重采样;重设裁剪,一次撤销恢复原布局
|
||||||
// 入参: page 页面索引, id 图片对象标识, mode 为contain(完整显示)或cover(填满裁剪)
|
// 入参: page 页面索引, id 图片对象标识, mode 为contain(完整显示)或cover(填满裁剪)
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) FitImage(page int, id, mode string) error {
|
func (e *Editor) FitImage(page int, id, mode string) error {
|
||||||
@@ -218,7 +218,7 @@ func (e *Editor) FitImage(page int, id, mode string) error {
|
|||||||
return e.UpdateObject(page, id, object)
|
return e.UpdateObject(page, id, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fitImage 将原始像素比例应用到当前图片框,适应时保留空白,填充时裁掉超出部分。
|
// fitImage 将原始像素比例应用到当前图片框,适应时保留空白,填充时裁掉超出部分
|
||||||
// 入参: obj 图片对象副本, mode 为contain或cover
|
// 入参: obj 图片对象副本, mode 为contain或cover
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) fitImage(obj *ImageObject, mode string) error {
|
func (e *Editor) fitImage(obj *ImageObject, mode string) error {
|
||||||
@@ -263,8 +263,8 @@ func (e *Editor) fitImage(obj *ImageObject, mode string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextFrame 将对象Boundary逆变换到本地坐标并返回轴对齐边界。
|
// TextFrame 将对象Boundary逆变换到本地坐标并返回轴对齐边界
|
||||||
// 直角旋转与翻转不改变本地段落宽度。
|
// 直角旋转与翻转不改变本地段落宽度
|
||||||
// 返回: Box 对象本地坐标中的边界, error 错误信息
|
// 返回: Box 对象本地坐标中的边界, error 错误信息
|
||||||
func (obj TextObject) TextFrame() (Box, error) {
|
func (obj TextObject) TextFrame() (Box, error) {
|
||||||
box, err := creationBox(obj.Boundary)
|
box, err := creationBox(obj.Boundary)
|
||||||
@@ -283,8 +283,8 @@ func (obj TextObject) TextFrame() (Box, error) {
|
|||||||
return m.TransformBox(Box{W: box.W, H: box.H}), nil
|
return m.TransformBox(Box{W: box.W, H: box.H}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResizeTextFrame 调整文字本地坐标的左侧偏移及宽度,保留页面方向;重新排版后通过UpdateObject提交。
|
// ResizeTextFrame 调整文字本地坐标的左侧偏移及宽度,保留页面方向;重新排版后通过UpdateObject提交
|
||||||
// 支持轴向缩放、直角旋转与镜像,不支持斜切或其他角度旋转。
|
// 支持轴向缩放、直角旋转与镜像,不支持斜切或其他角度旋转
|
||||||
// 入参: offset 本地左侧位移, width 新的本地排版宽度,单位为毫米
|
// 入参: offset 本地左侧位移, width 新的本地排版宽度,单位为毫米
|
||||||
// 返回: TextObject 调整后的文字对象, error 错误信息
|
// 返回: TextObject 调整后的文字对象, error 错误信息
|
||||||
func (obj TextObject) ResizeTextFrame(offset, width float64) (TextObject, error) {
|
func (obj TextObject) ResizeTextFrame(offset, width float64) (TextObject, error) {
|
||||||
@@ -308,7 +308,7 @@ func (obj TextObject) ResizeTextFrame(offset, width float64) (TextObject, error)
|
|||||||
return obj, nil
|
return obj, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorBoxString 使用统一精度序列化创作边界。
|
// editorBoxString 使用统一精度序列化创作边界
|
||||||
// 入参: box 矩形范围
|
// 入参: box 矩形范围
|
||||||
// 返回: string 标准Boundary属性值
|
// 返回: string 标准Boundary属性值
|
||||||
func editorBoxString(box Box) string {
|
func editorBoxString(box Box) string {
|
||||||
|
|||||||
+17
-17
@@ -23,7 +23,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// editorXML 保存XML节点的原文位置,不重写未修改的内容。
|
// editorXML 保存XML节点的原文位置,不重写未修改的内容
|
||||||
type editorXML struct {
|
type editorXML struct {
|
||||||
name xml.Name
|
name xml.Name
|
||||||
attrs []xml.Attr
|
attrs []xml.Attr
|
||||||
@@ -32,13 +32,13 @@ type editorXML struct {
|
|||||||
parent *editorXML
|
parent *editorXML
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLPatch 替换原文中的连续区间。
|
// editorXMLPatch 替换原文中的连续区间
|
||||||
type editorXMLPatch struct {
|
type editorXMLPatch struct {
|
||||||
start, end int
|
start, end int
|
||||||
data []byte
|
data []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseEditorXML 解析节点位置,保留命名空间、注释和原始格式。
|
// parseEditorXML 解析节点位置,保留命名空间、注释和原始格式
|
||||||
// 入参: data XML原文
|
// 入参: data XML原文
|
||||||
// 返回: *editorXML 根节点, error 错误信息
|
// 返回: *editorXML 根节点, error 错误信息
|
||||||
func parseEditorXML(data []byte) (*editorXML, error) {
|
func parseEditorXML(data []byte) (*editorXML, error) {
|
||||||
@@ -80,7 +80,7 @@ func parseEditorXML(data []byte) (*editorXML, error) {
|
|||||||
return root, nil
|
return root, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// child 查找OFD直接子节点。
|
// child 查找OFD直接子节点
|
||||||
// 入参: name 节点名称
|
// 入参: name 节点名称
|
||||||
// 返回: *editorXML 节点,不存在时为nil
|
// 返回: *editorXML 节点,不存在时为nil
|
||||||
func (n *editorXML) child(name string) *editorXML {
|
func (n *editorXML) child(name string) *editorXML {
|
||||||
@@ -92,7 +92,7 @@ func (n *editorXML) child(name string) *editorXML {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// attr 获取无命名空间的属性值。
|
// attr 获取无命名空间的属性值
|
||||||
// 入参: name 属性名称
|
// 入参: name 属性名称
|
||||||
// 返回: string 属性值
|
// 返回: string 属性值
|
||||||
func (n *editorXML) attr(name string) string {
|
func (n *editorXML) attr(name string) string {
|
||||||
@@ -104,7 +104,7 @@ func (n *editorXML) attr(name string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorPatchXML 按位置应用互不重叠的替换。
|
// editorPatchXML 按位置应用互不重叠的替换
|
||||||
// 入参: data 原文, patches 修改区间
|
// 入参: data 原文, patches 修改区间
|
||||||
// 返回: []byte 修改后的XML
|
// 返回: []byte 修改后的XML
|
||||||
func editorPatchXML(data []byte, patches []editorXMLPatch) []byte {
|
func editorPatchXML(data []byte, patches []editorXMLPatch) []byte {
|
||||||
@@ -125,7 +125,7 @@ func editorPatchXML(data []byte, patches []editorXMLPatch) []byte {
|
|||||||
return result.Bytes()
|
return result.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLContent 替换节点内部内容,兼容自闭合节点。
|
// editorXMLContent 替换节点内部内容,兼容自闭合节点
|
||||||
// 入参: data 原文, node 节点, content 新内容
|
// 入参: data 原文, node 节点, content 新内容
|
||||||
// 返回: editorXMLPatch 修改区间
|
// 返回: editorXMLPatch 修改区间
|
||||||
func editorXMLContent(data []byte, node *editorXML, content []byte) editorXMLPatch {
|
func editorXMLContent(data []byte, node *editorXML, content []byte) editorXMLPatch {
|
||||||
@@ -141,7 +141,7 @@ func editorXMLContent(data []byte, node *editorXML, content []byte) editorXMLPat
|
|||||||
return editorXMLPatch{node.start, node.end, replacement}
|
return editorXMLPatch{node.start, node.end, replacement}
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLText 编码带独立命名空间的文本节点。
|
// editorXMLText 编码带独立命名空间的文本节点
|
||||||
// 入参: name 节点名称, value 文本
|
// 入参: name 节点名称, value 文本
|
||||||
// 返回: []byte XML片段
|
// 返回: []byte XML片段
|
||||||
func editorXMLText(name, value string) []byte {
|
func editorXMLText(name, value string) []byte {
|
||||||
@@ -150,7 +150,7 @@ func editorXMLText(name, value string) []byte {
|
|||||||
return []byte("<ofd:" + name + " xmlns:ofd=\"" + ofdNamespace + "\">" + escaped.String() + "</ofd:" + name + ">")
|
return []byte("<ofd:" + name + " xmlns:ofd=\"" + ofdNamespace + "\">" + escaped.String() + "</ofd:" + name + ">")
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLSetText 更新或追加直接子节点,保留其他节点。
|
// editorXMLSetText 更新或追加直接子节点,保留其他节点
|
||||||
// 入参: data 原文, node 父节点, values 需要修改的字段
|
// 入参: data 原文, node 父节点, values 需要修改的字段
|
||||||
// 返回: []byte 修改后的XML
|
// 返回: []byte 修改后的XML
|
||||||
func editorXMLSetText(data []byte, node *editorXML, values [][2]string) []byte {
|
func editorXMLSetText(data []byte, node *editorXML, values [][2]string) []byte {
|
||||||
@@ -187,7 +187,7 @@ func editorXMLSetText(data []byte, node *editorXML, values [][2]string) []byte {
|
|||||||
return editorPatchXML(data, patches)
|
return editorPatchXML(data, patches)
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLObjects 递归登记图层和页块内的对象位置,保留原容器层级。
|
// editorXMLObjects 递归登记图层和页块内的对象位置,保留原容器层级
|
||||||
// 入参: container 图层或页块, nodes 标识与节点映射
|
// 入参: container 图层或页块, nodes 标识与节点映射
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func editorXMLObjects(container *editorXML, nodes map[string]*editorXML) error {
|
func editorXMLObjects(container *editorXML, nodes map[string]*editorXML) error {
|
||||||
@@ -207,7 +207,7 @@ func editorXMLObjects(container *editorXML, nodes map[string]*editorXML) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLAttributes 判断节点的命名空间与属性是否在编辑器支持范围内。
|
// editorXMLAttributes 判断节点的命名空间与属性是否在编辑器支持范围内
|
||||||
// 入参: node 节点, allowed 允许的属性名称
|
// 入参: node 节点, allowed 允许的属性名称
|
||||||
// 返回: bool 是否支持
|
// 返回: bool 是否支持
|
||||||
func editorXMLAttributes(node *editorXML, allowed string) bool {
|
func editorXMLAttributes(node *editorXML, allowed string) bool {
|
||||||
@@ -225,7 +225,7 @@ func editorXMLAttributes(node *editorXML, allowed string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLSupported 判断原文是否仅包含创建器能够保留的对象字段。
|
// editorXMLSupported 判断原文是否仅包含创建器能够保留的对象字段
|
||||||
// 入参: node 对象或子节点
|
// 入参: node 对象或子节点
|
||||||
// 返回: bool 是否支持完整编辑
|
// 返回: bool 是否支持完整编辑
|
||||||
func editorXMLSupported(node *editorXML) bool {
|
func editorXMLSupported(node *editorXML) bool {
|
||||||
@@ -266,8 +266,8 @@ func editorXMLSupported(node *editorXML) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLObject 修改对象发生变化的属性与内容,保留原文中的显式默认值。
|
// editorXMLObject 修改对象发生变化的属性与内容,保留原文中的显式默认值
|
||||||
// 有效样式快照在修改后展开原绘制参数,避免已清除的属性重新继承。
|
// 有效样式快照在修改后展开原绘制参数,避免已清除的属性重新继承
|
||||||
// 入参: data 页面原文, node 原对象节点, before 原对象, after 新对象
|
// 入参: data 页面原文, node 原对象节点, before 原对象, after 新对象
|
||||||
// 返回: []byte 对象XML, error 错误信息
|
// 返回: []byte 对象XML, error 错误信息
|
||||||
func editorXMLObject(data []byte, node *editorXML, before, after GraphicObject) ([]byte, error) {
|
func editorXMLObject(data []byte, node *editorXML, before, after GraphicObject) ([]byte, error) {
|
||||||
@@ -298,7 +298,7 @@ func editorXMLObject(data []byte, node *editorXML, before, after GraphicObject)
|
|||||||
return editorXMLMerge(data, node, oldXML, newXML)
|
return editorXMLMerge(data, node, oldXML, newXML)
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLMerge 按标准编码的差异更新原节点,递归保留未变化的显式默认值与命名空间。
|
// editorXMLMerge 按标准编码的差异更新原节点,递归保留未变化的显式默认值与命名空间
|
||||||
// 入参: data 原文, node 原节点, oldXML 修改前编码, newXML 修改后编码
|
// 入参: data 原文, node 原节点, oldXML 修改前编码, newXML 修改后编码
|
||||||
// 返回: []byte 更新节点, error 错误信息
|
// 返回: []byte 更新节点, error 错误信息
|
||||||
func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte, error) {
|
func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte, error) {
|
||||||
@@ -397,7 +397,7 @@ func editorXMLMerge(data []byte, node *editorXML, oldXML, newXML []byte) ([]byte
|
|||||||
return result.Bytes(), nil
|
return result.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLContainer 用标准编码器封装已编码的子节点。
|
// editorXMLContainer 用标准编码器封装已编码的子节点
|
||||||
// 入参: name 节点名称, attrs 属性, content 子节点XML
|
// 入参: name 节点名称, attrs 属性, content 子节点XML
|
||||||
// 返回: []byte XML片段, error 错误信息
|
// 返回: []byte XML片段, error 错误信息
|
||||||
func editorXMLContainer(name string, attrs ofdAttrs, content []byte) ([]byte, error) {
|
func editorXMLContainer(name string, attrs ofdAttrs, content []byte) ([]byte, error) {
|
||||||
@@ -410,7 +410,7 @@ func editorXMLContainer(name string, attrs ofdAttrs, content []byte) ([]byte, er
|
|||||||
return output.Bytes(), err
|
return output.Bytes(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorXMLStandalone 补齐原对象继承的命名空间,使跨页复制不依赖原父节点。
|
// editorXMLStandalone 补齐原对象继承的命名空间,使跨页复制不依赖原父节点
|
||||||
// 入参: data 对象XML, source 原节点
|
// 入参: data 对象XML, source 原节点
|
||||||
// 返回: []byte 独立XML片段, error 错误信息
|
// 返回: []byte 独立XML片段, error 错误信息
|
||||||
func editorXMLStandalone(data []byte, source *editorXML) ([]byte, error) {
|
func editorXMLStandalone(data []byte, source *editorXML) ([]byte, error) {
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// 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/binary"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tdewolff/font"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FontFace 字体文件中的字体名称与零起始索引,不依赖文件名
|
||||||
|
type FontFace struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
FullName string `json:"fullName"`
|
||||||
|
Family string `json:"family"`
|
||||||
|
Style string `json:"style"`
|
||||||
|
PostScriptName string `json:"postscriptName"`
|
||||||
|
Names []string `json:"names"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Faces 读取字体名称表,集合文件按原顺序列出各项,不解析字形轮廓
|
||||||
|
// 返回: []FontFace 字体列表, error 格式或名称表错误
|
||||||
|
func (f FontFile) Faces() ([]FontFace, error) {
|
||||||
|
data, err := font.ToSFNT(f.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
count, err := fontFileCount(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
faces := make([]FontFace, 0, count)
|
||||||
|
for index := 0; index < count; index++ {
|
||||||
|
tables, err := fontFileTables(data, index)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
records, values := fontNameValues(tables["name"])
|
||||||
|
name := func(ids ...uint16) string {
|
||||||
|
for _, id := range ids {
|
||||||
|
for _, record := range records {
|
||||||
|
if record.Name == id {
|
||||||
|
if value := strings.TrimSpace(values[[4]uint16{record.Platform, record.Encoding, record.Language, id}]); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
face := FontFace{Index: index, FullName: name(4, 6, 16, 1), Family: name(16, 1), Style: name(17, 2), PostScriptName: name(6), Names: fontNamesFromTable(tables["name"])}
|
||||||
|
if face.FullName == "" {
|
||||||
|
return nil, fmt.Errorf("font %d has no usable name", index)
|
||||||
|
}
|
||||||
|
faces = append(faces, face)
|
||||||
|
}
|
||||||
|
return faces, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Face 提取指定字体,返回独立的OpenType数据,可用于预览或AddFont,且不修改原文件
|
||||||
|
// 入参: index 零起始字体索引,非集合文件只能为0
|
||||||
|
// 返回: []byte 独立字体数据, error 格式或索引错误
|
||||||
|
func (f FontFile) Face(index int) ([]byte, error) {
|
||||||
|
data, err := font.ToSFNT(f.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if bytes.HasPrefix(data, []byte("ttcf")) {
|
||||||
|
return extractCollectionFont(data, index)
|
||||||
|
}
|
||||||
|
if _, err := fontFileTables(data, index); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bytes.Clone(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fontFileCount 校验字体或集合头并读取字体数量
|
||||||
|
// 入参: data OpenType数据
|
||||||
|
// 返回: int 字体数量, error 格式错误
|
||||||
|
func fontFileCount(data []byte) (int, error) {
|
||||||
|
if len(data) < 12 {
|
||||||
|
return 0, fmt.Errorf("invalid font header")
|
||||||
|
}
|
||||||
|
switch string(data[:4]) {
|
||||||
|
case "\x00\x01\x00\x00", "OTTO", "true":
|
||||||
|
return 1, nil
|
||||||
|
case "ttcf":
|
||||||
|
version := binary.BigEndian.Uint32(data[4:])
|
||||||
|
count := uint64(binary.BigEndian.Uint32(data[8:]))
|
||||||
|
if (version != 0x00010000 && version != 0x00020000) || count == 0 || count > uint64(len(data)-12)/4 {
|
||||||
|
return 0, fmt.Errorf("invalid font collection header")
|
||||||
|
}
|
||||||
|
return int(count), nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("invalid OpenType header")
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
-21
@@ -99,17 +99,42 @@ func fontFileNames(file io.ReaderAt) []string {
|
|||||||
// 入参: data 名称表数据
|
// 入参: data 名称表数据
|
||||||
// 返回: []string 字体名称列表
|
// 返回: []string 字体名称列表
|
||||||
func fontNamesFromTable(data []byte) []string {
|
func fontNamesFromTable(data []byte) []string {
|
||||||
|
records, values := fontNameValues(data)
|
||||||
|
var names []string
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, record := range records {
|
||||||
|
key := [4]uint16{record.Platform, record.Encoding, record.Language, record.Name}
|
||||||
|
name := values[key]
|
||||||
|
switch record.Name {
|
||||||
|
case 1, 16:
|
||||||
|
key[3]++
|
||||||
|
if style := values[key]; style != "" && name != "" {
|
||||||
|
name += " " + style
|
||||||
|
}
|
||||||
|
case 4, 6:
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = appendFontName(names, seen, name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// fontNameValues 解码字体名称表的多语言记录
|
||||||
|
// 入参: data 名称表数据
|
||||||
|
// 返回: []fontNameRecord 名称记录, map[[4]uint16]string 平台、编码、语言和名称对应的文本
|
||||||
|
func fontNameValues(data []byte) ([]fontNameRecord, map[[4]uint16]string) {
|
||||||
if len(data) < 6 || binary.BigEndian.Uint16(data) > 1 {
|
if len(data) < 6 || binary.BigEndian.Uint16(data) > 1 {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
count := int(binary.BigEndian.Uint16(data[2:]))
|
count := int(binary.BigEndian.Uint16(data[2:]))
|
||||||
storage := int(binary.BigEndian.Uint16(data[4:]))
|
storage := int(binary.BigEndian.Uint16(data[4:]))
|
||||||
if 6+count*12 > storage || storage > len(data) {
|
if 6+count*12 > storage || storage > len(data) {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
records := make([]fontNameRecord, count)
|
records := make([]fontNameRecord, count)
|
||||||
if err := binary.Read(bytes.NewReader(data[6:]), binary.BigEndian, records); err != nil {
|
if err := binary.Read(bytes.NewReader(data[6:]), binary.BigEndian, records); err != nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
values := make(map[[4]uint16]string)
|
values := make(map[[4]uint16]string)
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
@@ -141,22 +166,5 @@ func fontNamesFromTable(data []byte) []string {
|
|||||||
}
|
}
|
||||||
values[[4]uint16{record.Platform, record.Encoding, record.Language, record.Name}] = name
|
values[[4]uint16{record.Platform, record.Encoding, record.Language, record.Name}] = name
|
||||||
}
|
}
|
||||||
var names []string
|
return records, values
|
||||||
seen := make(map[string]bool)
|
|
||||||
for _, record := range records {
|
|
||||||
key := [4]uint16{record.Platform, record.Encoding, record.Language, record.Name}
|
|
||||||
name := values[key]
|
|
||||||
switch record.Name {
|
|
||||||
case 1, 16:
|
|
||||||
key[3]++
|
|
||||||
if style := values[key]; style != "" && name != "" {
|
|
||||||
name += " " + style
|
|
||||||
}
|
|
||||||
case 4, 6:
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
names = appendFontName(names, seen, name)
|
|
||||||
}
|
|
||||||
return names
|
|
||||||
}
|
}
|
||||||
+28
-17
@@ -25,15 +25,33 @@ import (
|
|||||||
// 入参: data 集合数据, index 字体索引
|
// 入参: data 集合数据, index 字体索引
|
||||||
// 返回: []byte 单字体数据, error 错误信息
|
// 返回: []byte 单字体数据, error 错误信息
|
||||||
func extractCollectionFont(data []byte, index int) ([]byte, error) {
|
func extractCollectionFont(data []byte, index int) ([]byte, error) {
|
||||||
if len(data) < 12 || string(data[:4]) != "ttcf" {
|
tables, err := fontFileTables(data, index)
|
||||||
return nil, fmt.Errorf("invalid font collection header")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
version := binary.BigEndian.Uint32(data[4:])
|
if len(tables["head"]) < 12 {
|
||||||
count := uint64(binary.BigEndian.Uint32(data[8:]))
|
return nil, fmt.Errorf("invalid head font table")
|
||||||
if (version != 0x00010000 && version != 0x00020000) || count > uint64(len(data)-12)/4 || index < 0 || uint64(index) >= count {
|
}
|
||||||
return nil, fmt.Errorf("invalid font collection index or version")
|
tables["head"] = bytes.Clone(tables["head"])
|
||||||
|
clear(tables["head"][8:12])
|
||||||
|
return serializeOTF(tables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fontFileTables 读取单字体或集合指定项的表目录,不复制表数据
|
||||||
|
// 入参: data 字体数据, index 字体索引
|
||||||
|
// 返回: map[string][]byte 字体表, error 错误信息
|
||||||
|
func fontFileTables(data []byte, index int) (map[string][]byte, error) {
|
||||||
|
count, err := fontFileCount(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if index < 0 || index >= count {
|
||||||
|
return nil, fmt.Errorf("invalid font index %d", index)
|
||||||
|
}
|
||||||
|
var offset uint64
|
||||||
|
if string(data[:4]) == "ttcf" {
|
||||||
|
offset = uint64(binary.BigEndian.Uint32(data[12+index*4:]))
|
||||||
}
|
}
|
||||||
offset := uint64(binary.BigEndian.Uint32(data[12+index*4:]))
|
|
||||||
if offset > uint64(len(data)-12) {
|
if offset > uint64(len(data)-12) {
|
||||||
return nil, fmt.Errorf("invalid font offset")
|
return nil, fmt.Errorf("invalid font offset")
|
||||||
}
|
}
|
||||||
@@ -60,12 +78,7 @@ func extractCollectionFont(data []byte, index int) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
tables[tag] = data[start : start+length]
|
tables[tag] = data[start : start+length]
|
||||||
}
|
}
|
||||||
if len(tables["head"]) < 12 {
|
return tables, nil
|
||||||
return nil, fmt.Errorf("invalid head font table")
|
|
||||||
}
|
|
||||||
tables["head"] = bytes.Clone(tables["head"])
|
|
||||||
clear(tables["head"][8:12])
|
|
||||||
return serializeOTF(tables)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// packedGlyphRune 获取包装字体字符
|
// packedGlyphRune 获取包装字体字符
|
||||||
@@ -537,7 +550,7 @@ func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
|
|||||||
return mainBuf.Bytes()
|
return mainBuf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldBuildCmapFormat12 判断字符超出BMP或format 4长度溢出时是否需要format 12
|
// shouldBuildCmapFormat12 判断终止码、补充平面字符或format 4长度溢出时是否需要format 12
|
||||||
// 入参: mapping 字符到字形的映射
|
// 入参: mapping 字符到字形的映射
|
||||||
// 返回: bool 是否需要构建format 12子表
|
// 返回: bool 是否需要构建format 12子表
|
||||||
func shouldBuildCmapFormat12(mapping map[rune]uint16) bool {
|
func shouldBuildCmapFormat12(mapping map[rune]uint16) bool {
|
||||||
@@ -546,13 +559,11 @@ func shouldBuildCmapFormat12(mapping map[rune]uint16) bool {
|
|||||||
}
|
}
|
||||||
var codes []int
|
var codes []int
|
||||||
for r := range mapping {
|
for r := range mapping {
|
||||||
if r > 0xFFFF {
|
if r >= 0xFFFF {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if r != 0xFFFF {
|
|
||||||
codes = append(codes, int(r))
|
codes = append(codes, int(r))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if len(codes) == 0 {
|
if len(codes) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-4
@@ -27,6 +27,11 @@ type Box struct {
|
|||||||
X, Y, W, H float64
|
X, Y, W, H float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Point 平面坐标点
|
||||||
|
type Point struct {
|
||||||
|
X, Y float64
|
||||||
|
}
|
||||||
|
|
||||||
// ParseBox 解析Box字符串
|
// ParseBox 解析Box字符串
|
||||||
// 入参: s 字符串
|
// 入参: s 字符串
|
||||||
// 返回: Box 矩形对象, error 错误信息
|
// 返回: Box 矩形对象, error 错误信息
|
||||||
@@ -96,7 +101,7 @@ func (m Matrix) Transform(x, y float64) (float64, float64) {
|
|||||||
return nx, ny
|
return nx, ny
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransformBox 获取矩形经仿射变换后的轴对齐边界。
|
// TransformBox 获取矩形经仿射变换后的轴对齐边界
|
||||||
// 入参: box 原始矩形范围
|
// 入参: box 原始矩形范围
|
||||||
// 返回: Box 变换后的轴对齐边界
|
// 返回: Box 变换后的轴对齐边界
|
||||||
func (m Matrix) TransformBox(box Box) Box {
|
func (m Matrix) TransformBox(box Box) Box {
|
||||||
@@ -109,20 +114,20 @@ func (m Matrix) TransformBox(box Box) Box {
|
|||||||
return Box{X: left, Y: top, W: right - left, H: bottom - top}
|
return Box{X: left, Y: top, W: right - left, H: bottom - top}
|
||||||
}
|
}
|
||||||
|
|
||||||
// String 返回标准CTM属性值。
|
// String 返回标准CTM属性值
|
||||||
// 返回: string 六个空格分隔的矩阵分量
|
// 返回: string 六个空格分隔的矩阵分量
|
||||||
func (m Matrix) String() string {
|
func (m Matrix) String() string {
|
||||||
return fmt.Sprintf("%s %s %s %s %s %s", ofdNumber(m.a), ofdNumber(m.b), ofdNumber(m.c), ofdNumber(m.d), ofdNumber(m.e), ofdNumber(m.f))
|
return fmt.Sprintf("%s %s %s %s %s %s", ofdNumber(m.a), ofdNumber(m.b), ofdNumber(m.c), ofdNumber(m.d), ofdNumber(m.e), ofdNumber(m.f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// axisAlignedMatrix 判断变换是否保持坐标轴平行,含直角旋转与镜像。
|
// axisAlignedMatrix 判断变换是否保持坐标轴平行,含直角旋转与镜像
|
||||||
// 入参: m 仿射变换矩阵
|
// 入参: m 仿射变换矩阵
|
||||||
// 返回: bool 是否为非退化的轴对齐变换
|
// 返回: bool 是否为非退化的轴对齐变换
|
||||||
func axisAlignedMatrix(m Matrix) bool {
|
func axisAlignedMatrix(m Matrix) bool {
|
||||||
return m.b == 0 && m.c == 0 && m.a != 0 && m.d != 0 || m.a == 0 && m.d == 0 && m.b != 0 && m.c != 0
|
return m.b == 0 && m.c == 0 && m.a != 0 && m.d != 0 || m.a == 0 && m.d == 0 && m.b != 0 && m.c != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// matrixVector 变换位移,不包含平移分量。
|
// matrixVector 变换位移,不包含平移分量
|
||||||
// 入参: m 仿射变换矩阵, x、y 原始位移
|
// 入参: m 仿射变换矩阵, x、y 原始位移
|
||||||
// 返回: float64 变换后的X位移, float64 变换后的Y位移
|
// 返回: float64 变换后的X位移, float64 变换后的Y位移
|
||||||
func matrixVector(m Matrix, x, y float64) (float64, float64) {
|
func matrixVector(m Matrix, x, y float64) (float64, float64) {
|
||||||
|
|||||||
+11
-11
@@ -21,10 +21,10 @@ import (
|
|||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ObjectBounds 获取文字、路径或图片在页面坐标中的轴对齐范围,不修改对象。
|
// ObjectBounds 获取文字、路径或图片在页面坐标中的轴对齐范围,不修改对象
|
||||||
// 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素。
|
// 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素
|
||||||
// 底纹按填充或描边轮廓度量,不展开图案单元。
|
// 底纹按填充或描边轮廓度量,不展开图案单元
|
||||||
// 不应用页面边界或父级变换;无可见范围时返回零值,不支持的对象类型返回错误。
|
// 不应用页面边界或父级变换;无可见范围时返回零值,不支持的对象类型返回错误
|
||||||
// 入参: object 图形对象, drawParam 图层绘制参数标识,无继承时为空
|
// 入参: object 图形对象, drawParam 图层绘制参数标识,无继承时为空
|
||||||
// 返回: Box 毫米坐标范围, error 错误信息
|
// 返回: Box 毫米坐标范围, error 错误信息
|
||||||
func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, error) {
|
func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, error) {
|
||||||
@@ -94,7 +94,7 @@ func (r *Renderer) ObjectBounds(object GraphicObject, drawParam string) (Box, er
|
|||||||
return bounds.box, nil
|
return bounds.box, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// boundsColor 以底纹的基础颜色和透明度度量轮廓,不修改原画刷。
|
// boundsColor 以底纹的基础颜色和透明度度量轮廓,不修改原画刷
|
||||||
// 入参: color 填充或描边颜色
|
// 入参: color 填充或描边颜色
|
||||||
// 返回: *FillColor 用于度量的颜色
|
// 返回: *FillColor 用于度量的颜色
|
||||||
func boundsColor(color *FillColor) *FillColor {
|
func boundsColor(color *FillColor) *FillColor {
|
||||||
@@ -104,18 +104,18 @@ func boundsColor(color *FillColor) *FillColor {
|
|||||||
return &FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace, Alpha: color.Alpha}
|
return &FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace, Alpha: color.Alpha}
|
||||||
}
|
}
|
||||||
|
|
||||||
// boundsRenderer 收集绘制范围,不分配页面像素或合并独立图形轮廓。
|
// boundsRenderer 收集绘制范围,不分配页面像素或合并独立图形轮廓
|
||||||
type boundsRenderer struct {
|
type boundsRenderer struct {
|
||||||
box Box
|
box Box
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size 返回不限定边界的度量画布尺寸。
|
// Size 返回不限定边界的度量画布尺寸
|
||||||
// 返回: float64 宽度, float64 高度
|
// 返回: float64 宽度, float64 高度
|
||||||
func (r *boundsRenderer) Size() (float64, float64) {
|
func (r *boundsRenderer) Size() (float64, float64) {
|
||||||
return 0, 0
|
return 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// add 合并路径的精确曲线范围并转换为向下的纵轴。
|
// add 合并路径的精确曲线范围并转换为向下的纵轴
|
||||||
// 入参: path 绘制路径
|
// 入参: path 绘制路径
|
||||||
func (r *boundsRenderer) add(path *canvas.Path) {
|
func (r *boundsRenderer) add(path *canvas.Path) {
|
||||||
if path.Empty() {
|
if path.Empty() {
|
||||||
@@ -125,7 +125,7 @@ func (r *boundsRenderer) add(path *canvas.Path) {
|
|||||||
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()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderPath 收集填充与描边范围,保持线帽、连接和虚线语义。
|
// RenderPath 收集填充与描边范围,保持线帽、连接和虚线语义
|
||||||
// 入参: path 路径, style 绘制样式, m 变换矩阵
|
// 入参: path 路径, style 绘制样式, m 变换矩阵
|
||||||
func (r *boundsRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.Matrix) {
|
func (r *boundsRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.Matrix) {
|
||||||
if style.HasFill() {
|
if style.HasFill() {
|
||||||
@@ -137,12 +137,12 @@ func (r *boundsRenderer) RenderPath(path *canvas.Path, style canvas.Style, m can
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderText 收集绘制文字范围。
|
// RenderText 收集绘制文字范围
|
||||||
// 入参: text 文字, m 变换矩阵
|
// 入参: text 文字, m 变换矩阵
|
||||||
func (r *boundsRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
func (r *boundsRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
||||||
text.RenderTo(r, m, 0)
|
text.RenderTo(r, m, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderImage 度量不展开底纹图片。
|
// RenderImage 度量不展开底纹图片
|
||||||
// 入参: img 图片, m 变换矩阵
|
// 入参: img 图片, m 变换矩阵
|
||||||
func (r *boundsRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
|
func (r *boundsRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
|
||||||
@@ -171,7 +171,7 @@ func imageWithMask(img, mask image.Image) image.Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// imageWithClip 应用图片裁剪区域
|
// imageWithClip 应用图片裁剪区域
|
||||||
// 低分辨率图片按最低精度细化蒙版,细化后的像素上限16Mi,不降低原图分辨率。
|
// 低分辨率图片按最低精度细化蒙版,细化后的像素上限16Mi,不降低原图分辨率
|
||||||
// 入参: img 图片对象, clipPath 裁剪路径, m 图片变换矩阵, dpi 最低蒙版分辨率,0保留原图精度
|
// 入参: img 图片对象, clipPath 裁剪路径, m 图片变换矩阵, dpi 最低蒙版分辨率,0保留原图精度
|
||||||
// 返回: image.Image 裁剪后的图片对象
|
// 返回: image.Image 裁剪后的图片对象
|
||||||
func imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix, dpi float64) image.Image {
|
func imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix, dpi float64) image.Image {
|
||||||
|
|||||||
+8
-8
@@ -30,9 +30,9 @@ const (
|
|||||||
ShapeEllipse ShapeKind = "ellipse"
|
ShapeEllipse ShapeKind = "ellipse"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewShape 创建使用标准紧缩路径的图形,默认黑色描边、不填充。
|
// NewShape 创建使用标准紧缩路径的图形,默认黑色描边、不填充
|
||||||
// 可调整返回对象的颜色、线宽等属性后通过 Editor.AddObject 写入。
|
// 可调整返回对象的颜色、线宽等属性后通过 Editor.AddObject 写入
|
||||||
// 水平、垂直直线使用正尺寸边界,路径端点不变。
|
// 水平、垂直直线使用正尺寸边界,路径端点不变
|
||||||
// 入参: kind 图形类型, box 图形范围;直线从 (X,Y) 到 (X+W,Y+H),W、H 可为负或零
|
// 入参: kind 图形类型, box 图形范围;直线从 (X,Y) 到 (X+W,Y+H),W、H 可为负或零
|
||||||
// 返回: PathObject 路径对象, error 错误信息
|
// 返回: PathObject 路径对象, error 错误信息
|
||||||
func NewShape(kind ShapeKind, box Box) (PathObject, error) {
|
func NewShape(kind ShapeKind, box Box) (PathObject, error) {
|
||||||
@@ -65,8 +65,8 @@ func NewShape(kind ShapeKind, box Box) (PathObject, error) {
|
|||||||
return object, nil
|
return object, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shape 识别 NewShape 生成的基本路径及其轴对齐缩放、直角旋转、镜像和平移。
|
// Shape 识别 NewShape 生成的基本路径及其轴对齐缩放、直角旋转、镜像和平移
|
||||||
// 直线范围以起点和有符号的端点位移表示;非基本路径或不支持的变换返回空类型。
|
// 直线范围以起点和有符号的端点位移表示;非基本路径或不支持的变换返回空类型
|
||||||
// 返回: ShapeKind 图形类型, Box 几何范围(不含描边)
|
// 返回: ShapeKind 图形类型, Box 几何范围(不含描边)
|
||||||
func (p PathObject) Shape() (ShapeKind, Box) {
|
func (p PathObject) Shape() (ShapeKind, Box) {
|
||||||
tokens := strings.Fields(p.AbbreviatedData)
|
tokens := strings.Fields(p.AbbreviatedData)
|
||||||
@@ -126,7 +126,7 @@ func (p PathObject) Shape() (ShapeKind, Box) {
|
|||||||
return kind, box
|
return kind, box
|
||||||
}
|
}
|
||||||
|
|
||||||
// sameShapePath 比较基本路径的命令与数值,忽略数值格式差异。
|
// sameShapePath 比较基本路径的命令与数值,忽略数值格式差异
|
||||||
// 入参: a、b 路径词元
|
// 入参: a、b 路径词元
|
||||||
// 返回: bool 是否相同
|
// 返回: bool 是否相同
|
||||||
func sameShapePath(a, b []string) bool {
|
func sameShapePath(a, b []string) bool {
|
||||||
@@ -146,8 +146,8 @@ func sameShapePath(a, b []string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reshape 调整基本路径在所在坐标系中的几何范围,保留对象标识、方向、缩放和绘制属性,不缩放线宽。
|
// Reshape 调整基本路径在所在坐标系中的几何范围,保留对象标识、方向、缩放和绘制属性,不缩放线宽
|
||||||
// 返回对象可通过 Editor.UpdateObject 写入;仅支持 Shape 可识别的路径。
|
// 返回对象可通过 Editor.UpdateObject 写入;仅支持 Shape 可识别的路径
|
||||||
// 入参: box 新几何范围,直线使用起点和有符号的端点位移
|
// 入参: box 新几何范围,直线使用起点和有符号的端点位移
|
||||||
// 返回: PathObject 调整后的对象, error 错误信息
|
// 返回: PathObject 调整后的对象, error 错误信息
|
||||||
func (p PathObject) Reshape(box Box) (PathObject, error) {
|
func (p PathObject) Reshape(box Box) (PathObject, error) {
|
||||||
|
|||||||
+5
-4
@@ -30,8 +30,9 @@ import (
|
|||||||
const ofdNamespace = "http://www.ofdspec.org/2016"
|
const ofdNamespace = "http://www.ofdspec.org/2016"
|
||||||
|
|
||||||
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
|
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
|
||||||
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集;编辑资源、原文档字体、可变及彩色字体等保持原样。
|
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集
|
||||||
// 重开子集文档后输入未包含的文字,需要通过AddFont注册完整字体并替换原字体引用。
|
// 原有字体在全包引用可确定时保留字形编号裁剪;编辑资源、复杂字体及无法确定的引用保持原样
|
||||||
|
// 重开子集文档后输入未包含的文字,需要通过AddFont注册完整字体并替换原字体引用
|
||||||
// 入参: writer 输出流
|
// 入参: writer 输出流
|
||||||
// 返回: int64 已写入字节数, error 错误信息
|
// 返回: int64 已写入字节数, error 错误信息
|
||||||
func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
|
func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
|
||||||
@@ -207,7 +208,7 @@ func (e *Editor) writeParts(write func(string, []byte, bool) error) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// usedResources 筛选当前引用的新增资源及需要跨页复用的原资源文件,不修改资源池。
|
// usedResources 筛选当前引用的新增资源及需要跨页复用的原资源文件,不修改资源池
|
||||||
// 返回: []editorResource 新增字体, []editorResource 新增图片, []string 原资源文件
|
// 返回: []editorResource 新增字体, []editorResource 新增图片, []string 原资源文件
|
||||||
func (e *Editor) usedResources() (fonts, images []editorResource, sourceFiles []string) {
|
func (e *Editor) usedResources() (fonts, images []editorResource, sourceFiles []string) {
|
||||||
used := make(map[string]bool)
|
used := make(map[string]bool)
|
||||||
@@ -282,7 +283,7 @@ func (x *ofdXML) page(page PageContent) {
|
|||||||
x.end("Page")
|
x.end("Page")
|
||||||
}
|
}
|
||||||
|
|
||||||
// layer 写出新建图层及其对象。
|
// layer 写出新建图层及其对象
|
||||||
// 入参: layer 图层
|
// 入参: layer 图层
|
||||||
func (x *ofdXML) layer(layer Layer) {
|
func (x *ofdXML) layer(layer Layer) {
|
||||||
var attrs ofdAttrs
|
var attrs ofdAttrs
|
||||||
|
|||||||
Reference in new issue
Block a user