mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:53:44 +08:00
1 parent
71e67be9c1
commit
1914b5329a
25 files changed
+744
-250
No files matched your search
@@ -93,7 +93,8 @@
|
||||
</div>
|
||||
<div class="editor-group editor-erase">
|
||||
<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>
|
||||
</div>
|
||||
<div class="editor-group">
|
||||
|
||||
@@ -112,6 +112,7 @@ a:focus-visible {
|
||||
}
|
||||
|
||||
.editor-file,
|
||||
.editor-erase,
|
||||
.editor-paragraph,
|
||||
.editor-image,
|
||||
.editor-group:has(#textFont),
|
||||
@@ -119,11 +120,15 @@ a:focus-visible {
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
}
|
||||
|
||||
.editor-transform,
|
||||
.editor-erase {
|
||||
.editor-transform {
|
||||
grid-template-columns: max-content;
|
||||
}
|
||||
|
||||
#eraseObjectButton {
|
||||
grid-row: span 2;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.editor-image > select {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
+24
-13
@@ -120,6 +120,7 @@ const el = {
|
||||
drawEllipseButton: document.querySelector("#drawEllipseButton"),
|
||||
eraseObjectButton: document.querySelector("#eraseObjectButton"),
|
||||
eraseRegionButton: document.querySelector("#eraseRegionButton"),
|
||||
erasePathButton: document.querySelector("#erasePathButton"),
|
||||
shapeFill: document.querySelector("#shapeFill"),
|
||||
shapeFillColor: document.querySelector("#shapeFillColor"),
|
||||
shapeStroke: document.querySelector("#shapeStroke"),
|
||||
@@ -277,10 +278,11 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||
if (state.fontRenderPending) window.setTimeout(refreshPendingFonts, 0);
|
||||
},
|
||||
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"));
|
||||
if (blocked) { setStatus(objectEditReason(blocked) || "对象暂不可擦除"); return false; }
|
||||
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)
|
||||
: changeDocument("ofdgoDeleteObjects", item);
|
||||
},
|
||||
@@ -437,7 +439,7 @@ el.resetCropButton.addEventListener("click", () => {
|
||||
editorClick(el.undoButton, () => changeDocument("ofdgoUndo"));
|
||||
editorClick(el.redoButton, () => changeDocument("ofdgoRedo"));
|
||||
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, () => {
|
||||
if (!state.editing || document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
@@ -976,7 +978,7 @@ async function beginCanvasText(index, box, surface, page) {
|
||||
const openSeq = state.openSeq;
|
||||
setBusy(true);
|
||||
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);
|
||||
await face.load();
|
||||
if (openSeq !== state.openSeq || !surface.isConnected) return;
|
||||
@@ -991,7 +993,7 @@ async function beginCanvasText(index, box, surface, page) {
|
||||
}
|
||||
|
||||
function editorFonts(item) {
|
||||
const fonts = [...fontManager.userFonts.filter((font) => font.enabled), ...fontManager.catalog];
|
||||
const fonts = fontManager.editorFonts();
|
||||
if (item) {
|
||||
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() {
|
||||
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;
|
||||
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 selected = state.textFonts[Number(fontPicker.value)];
|
||||
const query = el.textFont.value === (selected?.fullName || selected?.name || "") ? "" : el.textFont.value;
|
||||
updateTextFonts(canvasEditor.selected, true);
|
||||
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 {
|
||||
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) {
|
||||
const id = item?.type === "TextObject" && !item.draft ? item.font : null;
|
||||
if (refresh || state.textFontID !== id) {
|
||||
@@ -1039,7 +1050,7 @@ async function changeTextFont() {
|
||||
const openSeq = state.openSeq;
|
||||
setBusy(true);
|
||||
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);
|
||||
await face.load();
|
||||
if (openSeq !== state.openSeq || canvasEditor.input !== editing) return;
|
||||
@@ -1056,7 +1067,7 @@ async function changeTextFont() {
|
||||
}
|
||||
} else if (item?.type === "TextObject") {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
@@ -1221,7 +1232,7 @@ function updateDrawingControls() {
|
||||
button.disabled = disabled || !pageCan("insert");
|
||||
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.setAttribute("aria-pressed", String(tool === name));
|
||||
}
|
||||
@@ -1256,7 +1267,7 @@ async function changeDocument(name, item, ...args) {
|
||||
return true;
|
||||
}
|
||||
setEditorInfo(doc);
|
||||
if (!item || name === "ofdgoDeleteObject" || name === "ofdgoDeleteObjects" || name === "ofdgoEraseObjects") {
|
||||
if (!item || name === "ofdgoDeleteObject" || name === "ofdgoDeleteObjects" || name === "ofdgoEraseObjects" || name === "ofdgoEraseObjectsPath") {
|
||||
canvasEditor.clear();
|
||||
}
|
||||
if (name === "ofdgoCopyObjects" || name === "ofdgoPasteObjects" || name === "ofdgoInsertShape" || name === "ofdgoInsertText" || name === "ofdgoInsertImage") {
|
||||
@@ -3021,7 +3032,7 @@ async function pasteEditorContent(event) {
|
||||
return;
|
||||
}
|
||||
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,
|
||||
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
|
||||
&& item.y < drag.box.y + drag.box.height && item.y + item.height > drag.box.y) : [];
|
||||
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;
|
||||
}
|
||||
if (drag.crop) {
|
||||
@@ -591,6 +591,11 @@ export class CanvasEditor {
|
||||
rect: surface.getBoundingClientRect(), rotation: this.options.rotation() };
|
||||
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 }));
|
||||
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.moveErase(event, this.drag);
|
||||
}
|
||||
@@ -598,6 +603,18 @@ export class CanvasEditor {
|
||||
moveErase(event, drag) {
|
||||
if (drag.erase === "erase-region") { this.moveShape(event, drag); return; }
|
||||
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
|
||||
&& point.y >= item.y && point.y <= item.y + item.height);
|
||||
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 terms = normalize(query).split(" ").filter(Boolean);
|
||||
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));
|
||||
});
|
||||
this.list.replaceChildren();
|
||||
@@ -105,8 +105,10 @@ export class FontPicker {
|
||||
|
||||
commitExact() {
|
||||
const value = this.input.value.trim().toLocaleLowerCase();
|
||||
const index = this.fonts.findIndex(font => !font.disabled
|
||||
&& [font.fullName, font.name, font.postscriptName].some(name => name?.toLocaleLowerCase() === value));
|
||||
const matches = font => font && !font.disabled
|
||||
&& [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);
|
||||
}
|
||||
|
||||
@@ -283,6 +285,22 @@ export class FontManager {
|
||||
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) {
|
||||
let saved = true;
|
||||
try {
|
||||
@@ -320,6 +338,7 @@ export class FontManager {
|
||||
const fonts = stored.map((font) => ({
|
||||
...font,
|
||||
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",
|
||||
}));
|
||||
fonts.push(...this.userFonts.filter((font) => font.source === "upload"));
|
||||
|
||||
+65
-22
@@ -97,6 +97,8 @@ func RunWASM() {
|
||||
registerExportCallback("ofdgoExportDocument", exportDocument)
|
||||
registerExportCallback("ofdgoExportAttachment", exportAttachment)
|
||||
registerCallback("ofdgoMatchFontFiles", matchFontFiles)
|
||||
registerCallback("ofdgoFontFaces", fontFaces)
|
||||
registerCallback("ofdgoFontFace", fontFace)
|
||||
registerCallback("ofdgoCreateDocument", createDocument)
|
||||
registerCallback("ofdgoEditDocument", editDocument)
|
||||
registerCallback("ofdgoChangePage", changePage)
|
||||
@@ -115,6 +117,7 @@ func RunWASM() {
|
||||
registerCallback("ofdgoAlignObjects", alignObjects)
|
||||
registerCallback("ofdgoDeleteObjects", deleteObjects)
|
||||
registerCallback("ofdgoEraseObjects", eraseObjects)
|
||||
registerCallback("ofdgoEraseObjectsPath", eraseObjectsPath)
|
||||
registerCallback("ofdgoCopyObjects", copyObjects)
|
||||
registerCallback("ofdgoCaptureObjects", captureObjects)
|
||||
registerCallback("ofdgoPasteObjects", pasteObjects)
|
||||
@@ -467,6 +470,32 @@ func matchFontFiles(args []js.Value) (any, error) {
|
||||
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 从浏览器值读取二进制数据
|
||||
// 入参: value 浏览器值
|
||||
// 返回: []byte 二进制数据, error 错误信息
|
||||
@@ -535,13 +564,13 @@ func encodeResult(result apiResult) string {
|
||||
// currentEditor 当前编辑文档
|
||||
var currentEditor *ofdgo.Editor
|
||||
|
||||
// editorClipboard 当前编辑文档中的对象快照与剪贴板标识。
|
||||
// editorClipboard 当前编辑文档中的对象快照与剪贴板标识
|
||||
type editorClipboard struct {
|
||||
token string
|
||||
objects []ofdgo.GraphicObject
|
||||
}
|
||||
|
||||
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据。
|
||||
// copiedObjects 当前对象剪贴板,不保存字体或图片的重复数据
|
||||
var copiedObjects *editorClipboard
|
||||
|
||||
// editorInfo 编辑文档信息与操作状态
|
||||
@@ -560,7 +589,7 @@ type editorPageInfo struct {
|
||||
PageIndex int `json:"pageIndex"`
|
||||
}
|
||||
|
||||
// editorSelectionInfo 对象复制结果及新选区。
|
||||
// editorSelectionInfo 对象复制结果及新选区
|
||||
type editorSelectionInfo struct {
|
||||
editorInfo
|
||||
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())
|
||||
}
|
||||
|
||||
// captureObjects 保存独立选区快照,不修改文档或历史。
|
||||
// captureObjects 保存独立选区快照,不修改文档或历史
|
||||
// 入参: args 页面索引、对象标识数组和剪贴板标识
|
||||
// 返回: any 空结果, error 错误信息
|
||||
func captureObjects(args []js.Value) (any, error) {
|
||||
@@ -866,7 +895,7 @@ func captureObjects(args []js.Value) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// pasteObjects 将当前文档的对象快照粘贴到目标页。
|
||||
// pasteObjects 将当前文档的对象快照粘贴到目标页
|
||||
// 入参: args 目标页、剪贴板标识和横纵位移
|
||||
// 返回: 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())
|
||||
}
|
||||
|
||||
// copyEditorObjects 复制快照并更新预览,返回新对象标识。
|
||||
// copyEditorObjects 复制快照并更新预览,返回新对象标识
|
||||
// 入参: page 目标页, objects 对象快照, dx、dy 毫米位移
|
||||
// 返回: 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 页面索引、对象标识数组和分布轴
|
||||
// 返回: 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())
|
||||
}
|
||||
|
||||
// editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置。
|
||||
// editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置
|
||||
// 入参: args 浏览器参数
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// layoutText 调整文字框和段落排版,保留软换行前的原文。
|
||||
// layoutText 调整文字框和段落排版,保留软换行前的原文
|
||||
// 入参: args 页码、对象标识、本地左侧偏移、宽度、折行、对齐、行距和字距
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// editorBox 将库层毫米边界传递给画布交互层。
|
||||
// editorBox 将库层毫米边界传递给画布交互层
|
||||
// 入参: box 毫米坐标中的矩形范围
|
||||
// 返回: map[string]any 可直接传递给JavaScript的横纵坐标和宽高
|
||||
func editorBox(box ofdgo.Box) map[string]any {
|
||||
return map[string]any{"x": box.X, "y": box.Y, "width": box.W, "height": box.H}
|
||||
}
|
||||
|
||||
// editorMatrix 将标准CTM转换为浏览器仿射矩阵分量。
|
||||
// editorMatrix 将标准CTM转换为浏览器仿射矩阵分量
|
||||
// 入参: matrix 库层仿射矩阵
|
||||
// 返回: []any 可直接传递给JavaScript的a、b、c、d、e、f分量,位移单位为毫米
|
||||
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}
|
||||
}
|
||||
|
||||
// rotateObjects 旋转同页选区,通用几何与历史记录由库负责。
|
||||
// rotateObjects 旋转同页选区,通用几何与历史记录由库负责
|
||||
// 入参: args 页面索引、对象标识数组和顺时针角度
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func rotateObjects(args []js.Value) (any, error) {
|
||||
@@ -1302,7 +1331,7 @@ func rotateObjects(args []js.Value) (any, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// flipObjects 镜像同页选区。
|
||||
// flipObjects 镜像同页选区
|
||||
// 入参: args 页面索引、对象标识数组和镜像轴
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func flipObjects(args []js.Value) (any, error) {
|
||||
@@ -1311,7 +1340,7 @@ func flipObjects(args []js.Value) (any, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// cropImage 设置图片裁剪范围,保留原始图片数据。
|
||||
// cropImage 设置图片裁剪范围,保留原始图片数据
|
||||
// 入参: args 页面索引、对象标识及页面毫米坐标中的横纵坐标和宽高
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func cropImage(args []js.Value) (any, error) {
|
||||
@@ -1320,7 +1349,7 @@ func cropImage(args []js.Value) (any, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// fitImage 按原始比例适应或填充图片框。
|
||||
// fitImage 按原始比例适应或填充图片框
|
||||
// 入参: args 页面索引、对象标识和适应方式
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func fitImage(args []js.Value) (any, error) {
|
||||
@@ -1329,7 +1358,7 @@ func fitImage(args []js.Value) (any, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// previewImage 在页面副本中显示指定图片的完整内容,不改变正文、历史或缓存。
|
||||
// previewImage 在页面副本中显示指定图片的完整内容,不改变正文、历史或缓存
|
||||
// 入参: args 页面索引和图片对象标识
|
||||
// 返回: any 页面SVG和分离的图片资源, 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
|
||||
}
|
||||
|
||||
// svgImagesToJS 将图片资源以二进制数组传给前端,不使用base64。
|
||||
// svgImagesToJS 将图片资源以二进制数组传给前端,不使用base64
|
||||
// 入参: images SVG引用的图片资源
|
||||
// 返回: []any 图片标识、类型和数据
|
||||
func svgImagesToJS(images []ofdgo.SVGImage) []any {
|
||||
@@ -1373,7 +1402,7 @@ func svgImagesToJS(images []ofdgo.SVGImage) []any {
|
||||
return items
|
||||
}
|
||||
|
||||
// transformObjects 统一移动或缩放选区,生成一次撤销记录。
|
||||
// transformObjects 统一移动或缩放选区,生成一次撤销记录
|
||||
// 入参: args 页面索引、对象标识数组、横纵位移和正缩放比例
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func transformObjects(args []js.Value) (any, error) {
|
||||
@@ -1382,7 +1411,7 @@ func transformObjects(args []js.Value) (any, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// alignObjects 对齐选区中的对象。
|
||||
// alignObjects 对齐选区中的对象
|
||||
// 入参: args 页面索引、对象标识数组和对齐方式
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func alignObjects(args []js.Value) (any, error) {
|
||||
@@ -1391,7 +1420,7 @@ func alignObjects(args []js.Value) (any, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// deleteObjects 一次删除选区中的对象。
|
||||
// deleteObjects 一次删除选区中的对象
|
||||
// 入参: args 页面索引和对象标识数组
|
||||
// 返回: 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 页面索引、对象标识数组和毫米坐标范围
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func eraseObjects(args []js.Value) (any, error) {
|
||||
@@ -1411,7 +1454,7 @@ func eraseObjects(args []js.Value) (any, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// changeObjects 提交库层批量操作,无修改时复用当前预览。
|
||||
// changeObjects 提交库层批量操作,无修改时复用当前预览
|
||||
// 入参: apply 待执行的编辑操作
|
||||
// 返回: any 文档信息, error 错误信息
|
||||
func changeObjects(apply func() error) (any, error) {
|
||||
|
||||
+8
-16
@@ -278,9 +278,6 @@ func (e *Editor) nextID() string {
|
||||
// 入参: file 字体文件, index 集合内字体索引,单字体为0
|
||||
// 返回: 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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -289,16 +286,11 @@ func (e *Editor) AddFont(file FontFile, index int) (string, error) {
|
||||
if id, ok := e.resourceID[key]; ok {
|
||||
return id, nil
|
||||
}
|
||||
if bytes.HasPrefix(data, []byte("ttcf")) {
|
||||
data, err = extractCollectionFont(data, index)
|
||||
data, err = (FontFile{Data: data}).Face(index)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
index = 0
|
||||
} else {
|
||||
data = bytes.Clone(data)
|
||||
}
|
||||
sfnt, err := font.ParseSFNT(data, index)
|
||||
sfnt, err := font.ParseSFNT(data, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -348,7 +340,7 @@ func editorFontName(sfnt *font.SFNT, names ...font.NameID) string {
|
||||
}
|
||||
|
||||
// AddImage 注册PNG或JPEG图片,重复资源复用标识,引用后写入文档
|
||||
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码。
|
||||
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
|
||||
// 入参: data 图片数据
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// editorBinaryImage 尝试纯黑白无损编码,不二值化,不改变透明度,无体积收益时保留原图。
|
||||
// editorBinaryImage 尝试纯黑白无损编码,不二值化,不改变透明度,无体积收益时保留原图
|
||||
// 入参: data PNG图片数据
|
||||
// 返回: []byte 更小的JBIG2数据,不适用时为nil
|
||||
func editorBinaryImage(data []byte) []byte {
|
||||
@@ -416,8 +408,8 @@ func (e *Editor) AddObject(page int, object GraphicObject) (string, error) {
|
||||
return ids[0], nil
|
||||
}
|
||||
|
||||
// Object 获取对象的独立副本,修改副本不影响文档。
|
||||
// 可编辑的继承样式解析为直接属性;仅在对象修改或复制时写入独立样式。
|
||||
// Object 获取对象的独立副本,修改副本不影响文档
|
||||
// 可编辑的继承样式解析为直接属性;仅在对象修改或复制时写入独立样式
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// transformEditorObject 变换独立对象副本,保留绘制属性和编辑中的段落信息。
|
||||
// transformEditorObject 变换独立对象副本,保留绘制属性和编辑中的段落信息
|
||||
// 入参: object 对象副本, dx 横向位移, dy 纵向位移, scale 正缩放比例
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// ReplaceImage 替换图片资源,保留对象标识和绘制顺序,一次撤销恢复资源及布局。
|
||||
// ReplaceImage 替换图片资源,保留对象标识和绘制顺序,一次撤销恢复资源及布局
|
||||
// 入参: page 页面索引, id 图片对象标识, data PNG或JPEG数据, fit 为contain、cover或空,空值保留原变换和裁剪
|
||||
// 返回: 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"
|
||||
)
|
||||
|
||||
// editorObjectPosition 对象所属图层及层内索引。
|
||||
// editorObjectPosition 对象所属图层及层内索引
|
||||
type editorObjectPosition struct {
|
||||
layer int
|
||||
index int
|
||||
}
|
||||
|
||||
// compareEditorPosition 比较对象在页面结构中的先后顺序。
|
||||
// compareEditorPosition 比较对象在页面结构中的先后顺序
|
||||
// 入参: a、b 对象位置
|
||||
// 返回: int 比较结果
|
||||
func compareEditorPosition(a, b editorObjectPosition) int {
|
||||
@@ -40,7 +40,7 @@ func compareEditorPosition(a, b editorObjectPosition) int {
|
||||
return cmp.Compare(a.index, b.index)
|
||||
}
|
||||
|
||||
// Objects 按绘制顺序获取选区的独立快照,保留编辑中的段落信息。
|
||||
// Objects 按绘制顺序获取选区的独立快照,保留编辑中的段落信息
|
||||
// 入参: page 页面索引, ids 对象标识,不得重复
|
||||
// 返回: []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
|
||||
}
|
||||
|
||||
// CopyObjects 将对象快照按输入顺序复制到目标页并平移,分配新ID,提交一次撤销记录。
|
||||
// 对象引用当前Editor已注册的资源,保留段落信息,不修改输入快照。
|
||||
// CopyObjects 将对象快照按输入顺序复制到目标页并平移,分配新ID,提交一次撤销记录
|
||||
// 对象引用当前Editor已注册的资源,保留段落信息,不修改输入快照
|
||||
// 入参: page 目标页面索引, objects 对象快照, dx、dy 毫米位移
|
||||
// 返回: []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
|
||||
}
|
||||
|
||||
// ObjectPosition 对象在图层或页块直接成员中的位置,Index从0开始,Count不含嵌套页块的对象。
|
||||
// ObjectPosition 对象在图层或页块直接成员中的位置,Index从0开始,Count不含嵌套页块的对象
|
||||
type ObjectPosition struct {
|
||||
Layer string
|
||||
Container string
|
||||
@@ -140,7 +140,7 @@ type ObjectPosition struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// ObjectPosition 获取对象的直接容器及排序位置。
|
||||
// ObjectPosition 获取对象的直接容器及排序位置
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// objectOrderIndexes 获取同一直接容器的对象索引,不跨越图层或页块。
|
||||
// objectOrderIndexes 获取同一直接容器的对象索引,不跨越图层或页块
|
||||
// 入参: page 页面索引, layer 图层, id 对象标识
|
||||
// 返回: *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
|
||||
}
|
||||
|
||||
// OrderObjects 调整同一图层或页块内选区的绘制顺序,保留各组选中及未选中对象的相对顺序。
|
||||
// OrderObjects 调整同一图层或页块内选区的绘制顺序,保留各组选中及未选中对象的相对顺序
|
||||
// 入参: page 页面索引, ids 对象标识, order 为up、down、top或bottom
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// DistributeObjects 按可见范围等距分布同页对象,固定两端对象,保留绘制顺序。
|
||||
// 不足三个对象时不修改;允许负间距,无法保持位置顺序的重叠布局返回错误。
|
||||
// DistributeObjects 按可见范围等距分布同页对象,固定两端对象,保留绘制顺序
|
||||
// 不足三个对象时不修改;允许负间距,无法保持位置顺序的重叠布局返回错误
|
||||
// 入参: page 页面索引, ids 对象标识, axis 为horizontal或vertical
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作。
|
||||
// replaceLayers 替换图层容器,隔离历史快照与后续的增删、排序操作
|
||||
// 入参: page 页面索引, layers 新图层列表
|
||||
func (e *Editor) replaceLayers(page int, layers []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不得重复
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) UpdateObjects(page int, objects []GraphicObject) error {
|
||||
return e.updateObjects(page, objects, false)
|
||||
}
|
||||
|
||||
// updateObjects 校验内容更新或纯几何变换,原子提交跨图层选区。
|
||||
// updateObjects 校验内容更新或纯几何变换,原子提交跨图层选区
|
||||
// 入参: page 页面索引, objects 新对象, geometry 是否保留原内容的几何操作
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// TransformObjects 同页对象以页面原点等比缩放后统一平移,保持相对位置,一次撤销恢复全部。
|
||||
// TransformObjects 同页对象以页面原点等比缩放后统一平移,保持相对位置,一次撤销恢复全部
|
||||
// 入参: page 页面索引, ids 对象标识, dx、dy 位移, scale 正缩放比例
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// AlignObjects 单对象对齐页面,多对象相互对齐至选区边界,文字采用实际字形范围。
|
||||
// AlignObjects 单对象对齐页面,多对象相互对齐至选区边界,文字采用实际字形范围
|
||||
// 入参: page 页面索引, ids 对象标识, alignment 为left、center、right、top、middle或bottom
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部。
|
||||
// DeleteObjects 原子删除同页对象,保留其余对象顺序,一次撤销恢复全部
|
||||
// 入参: page 页面索引, ids 对象标识
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// selectedObjects 校验同页选择,返回内部只读对象与对应索引。
|
||||
// selectedObjects 校验同页选择,返回内部只读对象与对应索引
|
||||
// 入参: page 页面索引, ids 对象标识,不得重复
|
||||
// 返回: []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
|
||||
}
|
||||
|
||||
// editorObjectID 获取可创作对象的标准标识。
|
||||
// editorObjectID 获取可创作对象的标准标识
|
||||
// 入参: object 图形对象
|
||||
// 返回: string 对象标识,不支持的类型返回空字符串
|
||||
func editorObjectID(object GraphicObject) string {
|
||||
@@ -558,7 +558,7 @@ func editorObjectID(object GraphicObject) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// objectBounds 批量获取对齐范围,复用同一渲染器的资源和字形缓存。
|
||||
// objectBounds 批量获取对齐范围,复用同一渲染器的资源和字形缓存
|
||||
// 入参: page 页面索引, objects 待度量的对象
|
||||
// 返回: []Box 对象范围, error 错误信息
|
||||
func (e *Editor) objectBounds(page int, objects []GraphicObject) ([]Box, error) {
|
||||
|
||||
+70
-7
@@ -19,17 +19,55 @@ import (
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// EraseObjects 擦除同页对象与矩形区域重叠的可见部分,一次撤销恢复全部。
|
||||
// 全部覆盖时删除对象;局部覆盖使用OFD路径裁剪,保留原文字和资源,不用于敏感信息脱敏。
|
||||
// 不改变未相交对象,无法保真编辑时返回错误且不提交修改。
|
||||
// EraseObjects 擦除同页对象与矩形区域重叠的可见部分,一次撤销恢复全部
|
||||
// 全部覆盖时删除对象;局部覆盖使用OFD路径裁剪,保留原文字和资源,不用于敏感信息脱敏
|
||||
// 不改变未相交对象,无法保真编辑时返回错误且不提交修改
|
||||
// 入参: page 页面索引, ids 对象标识, box 页面毫米坐标中的擦除范围
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
||||
if _, err := creationBox(editorBoxString(box)); err != nil {
|
||||
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)
|
||||
if err != nil || len(objects) == 0 {
|
||||
return err
|
||||
@@ -48,7 +86,15 @@ func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
||||
if right <= x || bottom <= y {
|
||||
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
|
||||
changed = true
|
||||
continue
|
||||
@@ -79,12 +125,16 @@ func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
||||
}
|
||||
}
|
||||
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: bottom, W: outer.W, H: outer.Y + outer.H - bottom},
|
||||
{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},
|
||||
} {
|
||||
}
|
||||
if region != nil {
|
||||
areas = []Box{unionTextBox(outer, box)}
|
||||
}
|
||||
for _, area := range areas {
|
||||
if area.W <= 0 || area.H <= 0 {
|
||||
continue
|
||||
}
|
||||
@@ -95,6 +145,19 @@ func (e *Editor) EraseObjects(page int, ids []string, box Box) error {
|
||||
}
|
||||
fill, stroke := true, false
|
||||
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}})
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// editorObjectClips 获取基本对象的裁剪字段。
|
||||
// editorObjectClips 获取基本对象的裁剪字段
|
||||
// 入参: object 文字、路径或图片对象
|
||||
// 返回: **Clips 裁剪字段地址,不支持的对象返回nil
|
||||
func editorObjectClips(object *GraphicObject) **Clips {
|
||||
|
||||
+156
-2
@@ -16,13 +16,167 @@ package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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 错误信息
|
||||
func (e *Editor) subsetFonts() (map[string][]byte, error) {
|
||||
used := make(map[string]map[uint16]bool)
|
||||
@@ -77,7 +231,7 @@ func (e *Editor) subsetFonts() (map[string][]byte, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// subsetEditorFont 裁剪TrueType字形及复合依赖,保留字符映射、名称、度量和提示指令。
|
||||
// subsetEditorFont 裁剪TrueType字形及复合依赖,保留字符映射、名称、度量和提示指令
|
||||
// 入参: data 字体数据, glyphs 已排序的字形编号,包含0
|
||||
// 返回: []byte 字体子集, error 错误信息
|
||||
func subsetEditorFont(data []byte, glyphs []uint16) ([]byte, error) {
|
||||
|
||||
+30
-30
@@ -28,7 +28,7 @@ import (
|
||||
"github.com/tdewolff/font"
|
||||
)
|
||||
|
||||
// editorSource 保留输入包及按需加载的原始页面。
|
||||
// editorSource 保留输入包及按需加载的原始页面
|
||||
type editorSource struct {
|
||||
reader *Reader
|
||||
document *Document
|
||||
@@ -39,7 +39,7 @@ type editorSource struct {
|
||||
idsReady bool
|
||||
}
|
||||
|
||||
// editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用。
|
||||
// editorObjectOrigin 保留原对象的XML语义,供复制和局部更新复用
|
||||
type editorObjectOrigin struct {
|
||||
page *editorSourcePage
|
||||
node *editorXML
|
||||
@@ -47,7 +47,7 @@ type editorObjectOrigin struct {
|
||||
reason string
|
||||
}
|
||||
|
||||
// editorSourcePage 保存页面原文、解析快照及图层和页块内的对象位置。
|
||||
// editorSourcePage 保存页面原文、解析快照及图层和页块内的对象位置
|
||||
type editorSourcePage struct {
|
||||
ref Page
|
||||
data []byte
|
||||
@@ -56,9 +56,9 @@ type editorSourcePage struct {
|
||||
nodes map[string]*editorXML
|
||||
}
|
||||
|
||||
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因。
|
||||
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块。
|
||||
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复。
|
||||
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因
|
||||
// Update表示完整替换内容,ReplaceFont表示可显式替换文字字体,Transform表示保留文字定位的几何变换,Order仅限同一图层或末级页块
|
||||
// Arrange表示可准确度量对齐与旋转范围;Reflow表示可重排文字,LayoutKnown表示段落选项可恢复
|
||||
type ObjectCapabilities struct {
|
||||
Update bool
|
||||
ReplaceFont bool
|
||||
@@ -72,7 +72,7 @@ type ObjectCapabilities struct {
|
||||
Reason string
|
||||
}
|
||||
|
||||
// PageCapabilities 页面可执行的操作,Insert表示可以添加RGB对象;新增空白页面始终可用。
|
||||
// PageCapabilities 页面可执行的操作,Insert表示可以添加RGB对象;新增空白页面始终可用
|
||||
type PageCapabilities struct {
|
||||
Insert bool
|
||||
Copy bool
|
||||
@@ -81,9 +81,9 @@ type PageCapabilities struct {
|
||||
Resize bool
|
||||
}
|
||||
|
||||
// Editor 打开主文档进行保真编辑,不修改输入包,不以权限声明或签名限制编辑。
|
||||
// 原权限和签名数据保留,修改受保护内容会使原签名失效,不重新签名。
|
||||
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader。
|
||||
// Editor 打开主文档进行保真编辑,不修改输入包,不以权限声明或签名限制编辑
|
||||
// 原权限和签名数据保留,修改受保护内容会使原签名失效,不重新签名
|
||||
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader
|
||||
// 返回: *Editor 编辑器, error 错误信息
|
||||
func (r *Reader) Editor() (*Editor, error) {
|
||||
if r.Zip != nil {
|
||||
@@ -129,7 +129,7 @@ func (r *Reader) Editor() (*Editor, error) {
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// editorDirectoryExists 判断包内目录是否已经使用,避免覆盖原始资源。
|
||||
// editorDirectoryExists 判断包内目录是否已经使用,避免覆盖原始资源
|
||||
// 入参: reader 输入包, directory 目录路径
|
||||
// 返回: bool 是否存在
|
||||
func editorDirectoryExists(reader *Reader, directory string) bool {
|
||||
@@ -146,7 +146,7 @@ func editorDirectoryExists(reader *Reader, directory string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// prepareSourceIDs 首次新增内容前流式核对原包标识,不依赖可能缺失或过期的MaxUnitID。
|
||||
// prepareSourceIDs 首次新增内容前流式核对原包标识,不依赖可能缺失或过期的MaxUnitID
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) prepareSourceIDs() error {
|
||||
if e.source == nil || e.source.idsReady {
|
||||
@@ -180,7 +180,7 @@ func (e *Editor) prepareSourceIDs() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// editorXMLMaxID 流式读取OFD结构中的最大标识,跳过非OFD的XML附件。
|
||||
// editorXMLMaxID 流式读取OFD结构中的最大标识,跳过非OFD的XML附件
|
||||
// 入参: input XML输入流
|
||||
// 返回: int 最大标识, error 错误信息
|
||||
func editorXMLMaxID(input io.Reader) (int, error) {
|
||||
@@ -217,7 +217,7 @@ func editorXMLMaxID(input io.Reader) (int, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// resourceDirectory 获取新增二进制资源的目录。
|
||||
// resourceDirectory 获取新增二进制资源的目录
|
||||
// 返回: string 包内路径
|
||||
func (e *Editor) resourceDirectory() string {
|
||||
if e.source != nil {
|
||||
@@ -226,14 +226,14 @@ func (e *Editor) resourceDirectory() string {
|
||||
return "Doc_0/Res"
|
||||
}
|
||||
|
||||
// originalPage 判断索引是否指向原文档页面。
|
||||
// originalPage 判断索引是否指向原文档页面
|
||||
// 入参: index 页面索引
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// PageCapabilities 获取页面操作范围;原始页面暂不复制、删除,避免破坏模板与外部引用。
|
||||
// PageCapabilities 获取页面操作范围;原始页面暂不复制、删除,避免破坏模板与外部引用
|
||||
// 入参: index 页面索引
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// sourceRGB 判断新建RGB对象能否直接使用文档默认颜色空间。
|
||||
// sourceRGB 判断新建RGB对象能否直接使用文档默认颜色空间
|
||||
// 返回: bool 是否为8位RGB文档
|
||||
func (e *Editor) sourceRGB() bool {
|
||||
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
|
||||
}
|
||||
|
||||
// loadSourcePage 首次访问时解析原始页面,不加载图片或字体二进制数据。
|
||||
// loadSourcePage 首次访问时解析原始页面,不加载图片或字体二进制数据
|
||||
// 入参: index 页面索引
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) loadSourcePage(index int) error {
|
||||
@@ -310,9 +310,9 @@ func (e *Editor) loadSourcePage(index int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObjectCapabilities 获取对象可执行的操作,不支持的内容保持原文,不强制转换。
|
||||
// 字体缺失或子集不包含原文时仍可移动、删除,不允许重新排版或复制。
|
||||
// 显式提供可用字体后可通过UpdateObject替换;原文布局未知时不自动推断段落选项。
|
||||
// ObjectCapabilities 获取对象可执行的操作,不支持的内容保持原文,不强制转换
|
||||
// 字体缺失或子集不包含原文时仍可移动、删除,不允许重新排版或复制
|
||||
// 显式提供可用字体后可通过UpdateObject替换;原文布局未知时不自动推断段落选项
|
||||
// 入参: page 页面索引, id 对象标识
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// objectOrigin 获取原对象或其副本的保真来源。
|
||||
// objectOrigin 获取原对象或其副本的保真来源
|
||||
// 入参: id 对象标识
|
||||
// 返回: *editorObjectOrigin 原始对象,无来源时为nil
|
||||
func (e *Editor) objectOrigin(id string) *editorObjectOrigin {
|
||||
@@ -375,7 +375,7 @@ func (e *Editor) objectOrigin(id string) *editorObjectOrigin {
|
||||
return e.source.origins[id]
|
||||
}
|
||||
|
||||
// editorContainerOrderable 判断容器的直接对象能否在保留页块的前提下排序。
|
||||
// editorContainerOrderable 判断容器的直接对象能否在保留页块的前提下排序
|
||||
// 入参: container 图层或页块
|
||||
// 返回: bool 是否可排序
|
||||
func editorContainerOrderable(container *editorXML) bool {
|
||||
@@ -387,7 +387,7 @@ func editorContainerOrderable(container *editorXML) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// editorGeometry 获取基本对象的边界与变换。
|
||||
// editorGeometry 获取基本对象的边界与变换
|
||||
// 入参: object 对象
|
||||
// 返回: string 边界, string 变换矩阵
|
||||
func editorGeometry(object GraphicObject) (string, string) {
|
||||
@@ -402,7 +402,7 @@ func editorGeometry(object GraphicObject) (string, string) {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// editorFont 按需解析原文档内嵌字体,不用系统回退替换原字体。
|
||||
// editorFont 按需解析原文档内嵌字体,不用系统回退替换原字体
|
||||
// 入参: id 字体资源标识
|
||||
// 返回: *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)
|
||||
}
|
||||
|
||||
// editorImage 按需读取原图片尺寸,不解码完整像素。
|
||||
// editorImage 按需读取原图片尺寸,不解码完整像素
|
||||
// 入参: id 图片资源标识
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// cloneEditorData 深复制编辑快照,未导出的排版信息保持不可变共享。
|
||||
// cloneEditorData 深复制编辑快照,未导出的排版信息保持不可变共享
|
||||
// 入参: value 原值
|
||||
// 返回: T 独立副本
|
||||
func cloneEditorData[T any](value T) T {
|
||||
return cloneEditorValue(reflect.ValueOf(value)).Interface().(T)
|
||||
}
|
||||
|
||||
// cloneEditorValue 复制结构体中的可变指针和切片。
|
||||
// cloneEditorValue 复制结构体中的可变指针和切片
|
||||
// 入参: value 原值
|
||||
// 返回: reflect.Value 独立副本
|
||||
func cloneEditorValue(value reflect.Value) reflect.Value {
|
||||
@@ -495,7 +495,7 @@ func cloneEditorValue(value reflect.Value) reflect.Value {
|
||||
return value
|
||||
}
|
||||
|
||||
// editorObjectXML 编码可编辑对象,包含独立命名空间声明。
|
||||
// editorObjectXML 编码可编辑对象,包含独立命名空间声明
|
||||
// 入参: object 对象
|
||||
// 返回: []byte XML片段, 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
|
||||
}
|
||||
|
||||
// validateEditorGeometry 校验几何操作产生的数值,不重新排版或替换字体。
|
||||
// validateEditorGeometry 校验几何操作产生的数值,不重新排版或替换字体
|
||||
// 入参: object 变换后的对象
|
||||
// 返回: error 错误信息
|
||||
func validateEditorGeometry(object GraphicObject) error {
|
||||
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// sourceParts 生成编辑过的XML和新增资源,不读取未修改页面或原始二进制资源。
|
||||
// sourceParts 生成编辑过的XML和新增资源,不读取未修改页面或原始二进制资源
|
||||
// 返回: map[string][]byte 替换及新增条目, error 错误信息
|
||||
func (e *Editor) sourceParts() (map[string][]byte, error) {
|
||||
parts := make(map[string][]byte)
|
||||
@@ -171,7 +171,7 @@ func (e *Editor) sourceParts() (map[string][]byte, error) {
|
||||
return parts, nil
|
||||
}
|
||||
|
||||
// sourcePageXML 仅改写已修改的对象、页面尺寸和新增图层,保留原页块层级。
|
||||
// sourcePageXML 仅改写已修改的对象、页面尺寸和新增图层,保留原页块层级
|
||||
// 入参: index 页面索引, source 原页面
|
||||
// 返回: []byte 页面XML, 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
|
||||
}
|
||||
|
||||
// sourceLayerXML 写出新增图层,对复制对象保留原始定位和显式默认值。
|
||||
// sourceLayerXML 写出新增图层,对复制对象保留原始定位和显式默认值
|
||||
// 入参: layer 图层
|
||||
// 返回: []byte 图层XML, 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)
|
||||
}
|
||||
|
||||
// sourceNewPageXML 编码新增页面,同时保留复制对象的原文语义。
|
||||
// sourceNewPageXML 编码新增页面,同时保留复制对象的原文语义
|
||||
// 入参: page 页面
|
||||
// 返回: []byte 页面XML, 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...))
|
||||
}
|
||||
|
||||
// sourceInfoXML 更新公开元数据,保留原创建程序和未识别字段。
|
||||
// sourceInfoXML 更新公开元数据,保留原创建程序和未识别字段
|
||||
// 返回: []byte 根索引XML, error 错误信息
|
||||
func (e *Editor) sourceInfoXML() ([]byte, error) {
|
||||
data, err := e.source.reader.readFile("OFD.xml")
|
||||
@@ -373,7 +373,7 @@ func (e *Editor) sourceInfoXML() ([]byte, error) {
|
||||
return editorXMLSetText(data, info, values), nil
|
||||
}
|
||||
|
||||
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本。
|
||||
// writeSource 将未修改ZIP条目直接复制到新包,逐项写入改动,不持有原资源解压副本
|
||||
// 入参: writer 输出流, fonts 新增字体子集
|
||||
// 返回: 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
|
||||
}
|
||||
maps.Copy(parts, fonts)
|
||||
removed, err := e.pruneSourceResources(parts)
|
||||
removed, err := e.compactSourceResources(parts)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -445,7 +445,7 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte) (int64,
|
||||
return output.count, err
|
||||
}
|
||||
|
||||
// sourceReader 生成原包与修改条目组成的独立预览快照,不进行ZIP压缩。
|
||||
// sourceReader 生成原包与修改条目组成的独立预览快照,不进行ZIP压缩
|
||||
// 返回: *Reader 阅读器, error 错误信息
|
||||
func (e *Editor) sourceReader() (*Reader, error) {
|
||||
parts, err := e.sourceParts()
|
||||
|
||||
+63
-11
@@ -26,18 +26,19 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活。
|
||||
// editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活
|
||||
type editorResourceRefs struct {
|
||||
ids map[string]bool
|
||||
files map[string]bool
|
||||
fonts map[string]*editorFontUsage
|
||||
}
|
||||
|
||||
// pruneSourceResources 在保存副本中清理无引用字体和图片,保留其他资源及原始编辑状态。
|
||||
// 按GB/T 33190-2016附录A检查标识和路径引用,包括模板、注释、底纹、裁剪、版本和其他文档。
|
||||
// 全包检查保留孤立XML中的引用;未知命名空间、扩展数据或无法解析的XML使本次清理跳过。
|
||||
// compactSourceResources 在保存副本中清理无引用字体和图片,并裁剪可完整确认用字的原有字体
|
||||
// 按GB/T 33190-2016附录A检查标识和路径引用,包括模板、注释、底纹、裁剪、版本和其他文档
|
||||
// 全包检查保留孤立XML中的引用;未知命名空间、扩展数据或无法解析的XML使本次清理跳过
|
||||
// 入参: parts 已修改和新增的包内条目
|
||||
// 返回: 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 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -52,7 +53,7 @@ func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool,
|
||||
for name := range parts {
|
||||
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
|
||||
for _, name := range slices.Sorted(maps.Keys(names)) {
|
||||
if strings.HasSuffix(name, "/") {
|
||||
@@ -80,6 +81,7 @@ func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool,
|
||||
removed := make(map[string]bool)
|
||||
usedFiles := maps.Clone(refs.files)
|
||||
updates := make(map[string][]byte)
|
||||
fontFiles := make(map[string]*editorFontUsage)
|
||||
for _, name := range resources {
|
||||
data, ok := parts[name]
|
||||
if !ok {
|
||||
@@ -114,9 +116,21 @@ func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool,
|
||||
return nil, nil
|
||||
}
|
||||
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 {
|
||||
usedFiles[location] = true
|
||||
usedFiles[strings.ToLower(cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value)))] = true
|
||||
usedFiles[alternate] = true
|
||||
} else {
|
||||
removed[location] = true
|
||||
}
|
||||
@@ -139,6 +153,20 @@ func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool,
|
||||
for name := range usedFiles {
|
||||
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)
|
||||
for name := range parts {
|
||||
if removed[strings.ToLower(cleanPackagePath(name))] {
|
||||
@@ -148,7 +176,7 @@ func (e *Editor) pruneSourceResources(parts map[string][]byte) (map[string]bool,
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
// scan 流式读取包内XML引用,二进制条目只探测文件头,不展开页面对象或图片像素。
|
||||
// scan 流式读取包内XML引用,二进制条目只探测文件头,不展开页面对象或图片像素
|
||||
// 入参: input 文件流, name 包内路径
|
||||
// 返回: 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)
|
||||
var stack []string
|
||||
var fonts []*editorFontUsage
|
||||
var text strings.Builder
|
||||
resource, base := false, ""
|
||||
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 {
|
||||
if attr.Name.Space == "xmlns" || attr.Name.Local == "xmlns" || attr.Name.Space == "http://www.w3.org/XML/1998/namespace" {
|
||||
continue
|
||||
@@ -204,14 +240,30 @@ func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
||||
if attr.Name.Local != "ID" {
|
||||
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)
|
||||
fonts = append(fonts, usage)
|
||||
text.Reset()
|
||||
case xml.CharData:
|
||||
text.Write(token)
|
||||
case xml.EndElement:
|
||||
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]
|
||||
fonts = fonts[:len(fonts)-1]
|
||||
text.Reset()
|
||||
case xml.Directive:
|
||||
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 是否资源索引
|
||||
func (r *editorResourceRefs) reference(name, base, key, value string, resource bool) {
|
||||
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
|
||||
}
|
||||
|
||||
// editorResourceID 规范化ST_ID与ST_RefID的十进制表示。
|
||||
// editorResourceID 规范化ST_ID与ST_RefID的十进制表示
|
||||
// 入参: value 标识文本
|
||||
// 返回: string 标准十进制标识,无效时为空
|
||||
func editorResourceID(value string) string {
|
||||
@@ -259,7 +311,7 @@ func editorResourceID(value string) string {
|
||||
return strconv.FormatUint(id, 10)
|
||||
}
|
||||
|
||||
// editorResourceLocation 按ST_Loc与Res.BaseLoc解析路径,大小写折叠用于保守保留共享文件。
|
||||
// editorResourceLocation 按ST_Loc与Res.BaseLoc解析路径,大小写折叠用于保守保留共享文件
|
||||
// 入参: name 所在XML, base 资源目录, value 路径值
|
||||
// 返回: string 包内路径
|
||||
func editorResourceLocation(name, base, value string) string {
|
||||
|
||||
@@ -16,7 +16,7 @@ package ofdgo
|
||||
|
||||
import "fmt"
|
||||
|
||||
// editorDrawParam 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照。
|
||||
// editorDrawParam 解析原文档的绘制参数继承链,缺失或循环引用不进入编辑快照
|
||||
// 入参: id 绘制参数标识, visited 已访问标识
|
||||
// 返回: *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
|
||||
}
|
||||
|
||||
// resolveEditorStyle 为对象生成独立的有效样式,不改写共享资源或原XML。
|
||||
// resolveEditorStyle 为对象生成独立的有效样式,不改写共享资源或原XML
|
||||
// 入参: object 原对象, layer 图层绘制参数标识
|
||||
// 返回: GraphicObject 编辑快照, error 错误信息
|
||||
func (e *Editor) resolveEditorStyle(object GraphicObject, layer string) (GraphicObject, error) {
|
||||
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
"github.com/go-text/typesetting/segmenter"
|
||||
)
|
||||
|
||||
// TextLayout 本地横向段落选项,Wrap按CTM变换前的边界宽度折行,Align为left、center、right或justify。
|
||||
// LineHeight为毫米单位的基线间距,0使用字体度量;LetterSpacing为字素间的附加毫米间距,可为负。
|
||||
// 零值保持显式换行和左对齐。
|
||||
// TextLayout 本地横向段落选项,Wrap按CTM变换前的边界宽度折行,Align为left、center、right或justify
|
||||
// LineHeight为毫米单位的基线间距,0使用字体度量;LetterSpacing为字素间的附加毫米间距,可为负
|
||||
// 零值保持显式换行和左对齐
|
||||
type TextLayout struct {
|
||||
Wrap bool
|
||||
Align string
|
||||
@@ -30,14 +30,14 @@ type TextLayout struct {
|
||||
LetterSpacing float64
|
||||
}
|
||||
|
||||
// textLayout 保存编辑中的原文与选项,不写入OFD,也不参与渲染。
|
||||
// textLayout 保存编辑中的原文与选项,不写入OFD,也不参与渲染
|
||||
type textLayout struct {
|
||||
value string
|
||||
options TextLayout
|
||||
}
|
||||
|
||||
// TextLayout 获取最近一次排版的原文与选项,区分软换行和显式换行。
|
||||
// 信息仅在当前编辑过程保留,读取OFD时返回定位后的文字和默认选项。
|
||||
// TextLayout 获取最近一次排版的原文与选项,区分软换行和显式换行
|
||||
// 信息仅在当前编辑过程保留,读取OFD时返回定位后的文字和默认选项
|
||||
// 返回: string 原文, TextLayout 排版选项
|
||||
func (obj TextObject) TextLayout() (string, TextLayout) {
|
||||
if obj.layout != nil {
|
||||
@@ -46,7 +46,7 @@ func (obj TextObject) TextLayout() (string, TextLayout) {
|
||||
return obj.Text(), TextLayout{}
|
||||
}
|
||||
|
||||
// breakTextLines 优先使用Unicode断行机会,过长词仅在字素边界折行,不丢弃空白。
|
||||
// breakTextLines 优先使用Unicode断行机会,过长词仅在字素边界折行,不丢弃空白
|
||||
// 入参: runes 段落字符, advances 含字距的字符步进, width 本地排版宽度, wrap 是否自动折行, spacing 附加字距
|
||||
// 返回: [][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
|
||||
}
|
||||
|
||||
// alignTextLine 将段落对齐转换为标准X和DeltaX,两端对齐保留段落末行左对齐。
|
||||
// alignTextLine 将段落对齐转换为标准X和DeltaX,两端对齐保留段落末行左对齐
|
||||
// 入参: runes 行内字符, advances 含字距的字符步进, width 本地排版宽度, alignment 对齐方式, justify 是否允许本行两端对齐, spacing 附加字距
|
||||
// 返回: float64 行首X坐标, string 字符间的DeltaX序列
|
||||
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, " ")
|
||||
}
|
||||
|
||||
// spaceTextAdvances 将字距加入各字素末尾,不拆散组合字符。
|
||||
// spaceTextAdvances 将字距加入各字素末尾,不拆散组合字符
|
||||
// 入参: runes 段落字符, advances 待更新的字符步进, spacing 附加毫米字距
|
||||
func spaceTextAdvances(runes []rune, advances []float64, spacing float64) {
|
||||
if spacing == 0 {
|
||||
|
||||
+20
-20
@@ -20,15 +20,15 @@ import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
// RotateObject 绕对象可见范围中心旋转。
|
||||
// RotateObject 绕对象可见范围中心旋转
|
||||
// 入参: page 页面索引, id 对象标识, degrees 顺时针角度
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) RotateObject(page int, id string, degrees int) error {
|
||||
return e.RotateObjects(page, []string{id}, degrees)
|
||||
}
|
||||
|
||||
// RotateObjects 绕同页选区的可见范围中心旋转。
|
||||
// 保留相对位置及资源,一次撤销恢复全部。
|
||||
// RotateObjects 绕同页选区的可见范围中心旋转
|
||||
// 保留相对位置及资源,一次撤销恢复全部
|
||||
// 入参: page 页面索引, ids 对象标识, degrees 顺时针角度,仅支持90度的整数倍
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// FlipObject 绕对象可见范围中心镜像。
|
||||
// FlipObject 绕对象可见范围中心镜像
|
||||
// 入参: page 页面索引, id 对象标识, axis 为horizontal或vertical
|
||||
// 返回: error 错误信息
|
||||
func (e *Editor) FlipObject(page int, id, axis string) error {
|
||||
return e.FlipObjects(page, []string{id}, axis)
|
||||
}
|
||||
|
||||
// FlipObjects 以同页选区中心镜像,保留资源及绘制顺序,一次撤销恢复全部。
|
||||
// 以对象可见范围计算选区。
|
||||
// FlipObjects 以同页选区中心镜像,保留资源及绘制顺序,一次撤销恢复全部
|
||||
// 以对象可见范围计算选区
|
||||
// 入参: page 页面索引, ids 对象标识, axis 为horizontal或vertical
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// orientObjects 更新Boundary与CTM,不重排文字或重采样图片。
|
||||
// orientObjects 更新Boundary与CTM,不重排文字或重采样图片
|
||||
// 入参: page 页面索引, ids 对象标识, 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)
|
||||
}
|
||||
|
||||
// transformObjectClips 变换裁剪区域,不修改原始裁剪数据。
|
||||
// transformObjectClips 变换裁剪区域,不修改原始裁剪数据
|
||||
// 入参: clips 裁剪集合, matrix 对象边界坐标中的变换
|
||||
// 返回: *Clips 变换后的裁剪集合
|
||||
func transformObjectClips(clips *Clips, matrix Matrix) *Clips {
|
||||
@@ -126,7 +126,7 @@ func transformObjectClips(clips *Clips, matrix Matrix) *Clips {
|
||||
return &result
|
||||
}
|
||||
|
||||
// ImageBounds 返回完整图片经Boundary位移及CTM变换后的轴对齐边界,不应用裁剪或父级变换。
|
||||
// ImageBounds 返回完整图片经Boundary位移及CTM变换后的轴对齐边界,不应用裁剪或父级变换
|
||||
// 返回: Box 所在坐标系中的完整图片边界, error 错误信息
|
||||
func (obj ImageObject) ImageBounds() (Box, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// CropImage 按页面毫米坐标重设图片裁剪,保留原始资源及像素,可再次扩大裁剪区域。
|
||||
// 使用ImageObject.ImageBounds返回的范围可还原完整图片;提交一次撤销记录。
|
||||
// CropImage 按页面毫米坐标重设图片裁剪,保留原始资源及像素,可再次扩大裁剪区域
|
||||
// 使用ImageObject.ImageBounds返回的范围可还原完整图片;提交一次撤销记录
|
||||
// 入参: page 页面索引, id 图片对象标识, 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)
|
||||
}
|
||||
|
||||
// cropImageObject 更新图片副本的裁剪和局部坐标,不修改资源。
|
||||
// cropImageObject 更新图片副本的裁剪和局部坐标,不修改资源
|
||||
// 入参: image 图片对象副本, box 页面毫米坐标中的保留范围
|
||||
// 返回: error 错误信息
|
||||
func cropImageObject(image *ImageObject, box Box) error {
|
||||
@@ -200,8 +200,8 @@ func cropImageObject(image *ImageObject, box Box) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FitImage 按原始像素比例居中适应或填充当前Boundary,保留直角旋转及镜像方向。
|
||||
// 保留原始图片,不重采样;重设裁剪,一次撤销恢复原布局。
|
||||
// FitImage 按原始像素比例居中适应或填充当前Boundary,保留直角旋转及镜像方向
|
||||
// 保留原始图片,不重采样;重设裁剪,一次撤销恢复原布局
|
||||
// 入参: page 页面索引, id 图片对象标识, mode 为contain(完整显示)或cover(填满裁剪)
|
||||
// 返回: 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)
|
||||
}
|
||||
|
||||
// fitImage 将原始像素比例应用到当前图片框,适应时保留空白,填充时裁掉超出部分。
|
||||
// fitImage 将原始像素比例应用到当前图片框,适应时保留空白,填充时裁掉超出部分
|
||||
// 入参: obj 图片对象副本, mode 为contain或cover
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// TextFrame 将对象Boundary逆变换到本地坐标并返回轴对齐边界。
|
||||
// 直角旋转与翻转不改变本地段落宽度。
|
||||
// TextFrame 将对象Boundary逆变换到本地坐标并返回轴对齐边界
|
||||
// 直角旋转与翻转不改变本地段落宽度
|
||||
// 返回: Box 对象本地坐标中的边界, error 错误信息
|
||||
func (obj TextObject) TextFrame() (Box, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// ResizeTextFrame 调整文字本地坐标的左侧偏移及宽度,保留页面方向;重新排版后通过UpdateObject提交。
|
||||
// 支持轴向缩放、直角旋转与镜像,不支持斜切或其他角度旋转。
|
||||
// ResizeTextFrame 调整文字本地坐标的左侧偏移及宽度,保留页面方向;重新排版后通过UpdateObject提交
|
||||
// 支持轴向缩放、直角旋转与镜像,不支持斜切或其他角度旋转
|
||||
// 入参: offset 本地左侧位移, width 新的本地排版宽度,单位为毫米
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// editorBoxString 使用统一精度序列化创作边界。
|
||||
// editorBoxString 使用统一精度序列化创作边界
|
||||
// 入参: box 矩形范围
|
||||
// 返回: string 标准Boundary属性值
|
||||
func editorBoxString(box Box) string {
|
||||
|
||||
+17
-17
@@ -23,7 +23,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// editorXML 保存XML节点的原文位置,不重写未修改的内容。
|
||||
// editorXML 保存XML节点的原文位置,不重写未修改的内容
|
||||
type editorXML struct {
|
||||
name xml.Name
|
||||
attrs []xml.Attr
|
||||
@@ -32,13 +32,13 @@ type editorXML struct {
|
||||
parent *editorXML
|
||||
}
|
||||
|
||||
// editorXMLPatch 替换原文中的连续区间。
|
||||
// editorXMLPatch 替换原文中的连续区间
|
||||
type editorXMLPatch struct {
|
||||
start, end int
|
||||
data []byte
|
||||
}
|
||||
|
||||
// parseEditorXML 解析节点位置,保留命名空间、注释和原始格式。
|
||||
// parseEditorXML 解析节点位置,保留命名空间、注释和原始格式
|
||||
// 入参: data XML原文
|
||||
// 返回: *editorXML 根节点, error 错误信息
|
||||
func parseEditorXML(data []byte) (*editorXML, error) {
|
||||
@@ -80,7 +80,7 @@ func parseEditorXML(data []byte) (*editorXML, error) {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
// child 查找OFD直接子节点。
|
||||
// child 查找OFD直接子节点
|
||||
// 入参: name 节点名称
|
||||
// 返回: *editorXML 节点,不存在时为nil
|
||||
func (n *editorXML) child(name string) *editorXML {
|
||||
@@ -92,7 +92,7 @@ func (n *editorXML) child(name string) *editorXML {
|
||||
return nil
|
||||
}
|
||||
|
||||
// attr 获取无命名空间的属性值。
|
||||
// attr 获取无命名空间的属性值
|
||||
// 入参: name 属性名称
|
||||
// 返回: string 属性值
|
||||
func (n *editorXML) attr(name string) string {
|
||||
@@ -104,7 +104,7 @@ func (n *editorXML) attr(name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// editorPatchXML 按位置应用互不重叠的替换。
|
||||
// editorPatchXML 按位置应用互不重叠的替换
|
||||
// 入参: data 原文, patches 修改区间
|
||||
// 返回: []byte 修改后的XML
|
||||
func editorPatchXML(data []byte, patches []editorXMLPatch) []byte {
|
||||
@@ -125,7 +125,7 @@ func editorPatchXML(data []byte, patches []editorXMLPatch) []byte {
|
||||
return result.Bytes()
|
||||
}
|
||||
|
||||
// editorXMLContent 替换节点内部内容,兼容自闭合节点。
|
||||
// editorXMLContent 替换节点内部内容,兼容自闭合节点
|
||||
// 入参: data 原文, node 节点, content 新内容
|
||||
// 返回: 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}
|
||||
}
|
||||
|
||||
// editorXMLText 编码带独立命名空间的文本节点。
|
||||
// editorXMLText 编码带独立命名空间的文本节点
|
||||
// 入参: name 节点名称, value 文本
|
||||
// 返回: []byte XML片段
|
||||
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 + ">")
|
||||
}
|
||||
|
||||
// editorXMLSetText 更新或追加直接子节点,保留其他节点。
|
||||
// editorXMLSetText 更新或追加直接子节点,保留其他节点
|
||||
// 入参: data 原文, node 父节点, values 需要修改的字段
|
||||
// 返回: []byte 修改后的XML
|
||||
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)
|
||||
}
|
||||
|
||||
// editorXMLObjects 递归登记图层和页块内的对象位置,保留原容器层级。
|
||||
// editorXMLObjects 递归登记图层和页块内的对象位置,保留原容器层级
|
||||
// 入参: container 图层或页块, nodes 标识与节点映射
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// editorXMLAttributes 判断节点的命名空间与属性是否在编辑器支持范围内。
|
||||
// editorXMLAttributes 判断节点的命名空间与属性是否在编辑器支持范围内
|
||||
// 入参: node 节点, allowed 允许的属性名称
|
||||
// 返回: bool 是否支持
|
||||
func editorXMLAttributes(node *editorXML, allowed string) bool {
|
||||
@@ -225,7 +225,7 @@ func editorXMLAttributes(node *editorXML, allowed string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// editorXMLSupported 判断原文是否仅包含创建器能够保留的对象字段。
|
||||
// editorXMLSupported 判断原文是否仅包含创建器能够保留的对象字段
|
||||
// 入参: node 对象或子节点
|
||||
// 返回: bool 是否支持完整编辑
|
||||
func editorXMLSupported(node *editorXML) bool {
|
||||
@@ -266,8 +266,8 @@ func editorXMLSupported(node *editorXML) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// editorXMLObject 修改对象发生变化的属性与内容,保留原文中的显式默认值。
|
||||
// 有效样式快照在修改后展开原绘制参数,避免已清除的属性重新继承。
|
||||
// editorXMLObject 修改对象发生变化的属性与内容,保留原文中的显式默认值
|
||||
// 有效样式快照在修改后展开原绘制参数,避免已清除的属性重新继承
|
||||
// 入参: data 页面原文, node 原对象节点, before 原对象, after 新对象
|
||||
// 返回: []byte 对象XML, 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)
|
||||
}
|
||||
|
||||
// editorXMLMerge 按标准编码的差异更新原节点,递归保留未变化的显式默认值与命名空间。
|
||||
// editorXMLMerge 按标准编码的差异更新原节点,递归保留未变化的显式默认值与命名空间
|
||||
// 入参: data 原文, node 原节点, oldXML 修改前编码, newXML 修改后编码
|
||||
// 返回: []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
|
||||
}
|
||||
|
||||
// editorXMLContainer 用标准编码器封装已编码的子节点。
|
||||
// editorXMLContainer 用标准编码器封装已编码的子节点
|
||||
// 入参: name 节点名称, attrs 属性, content 子节点XML
|
||||
// 返回: []byte XML片段, 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
|
||||
}
|
||||
|
||||
// editorXMLStandalone 补齐原对象继承的命名空间,使跨页复制不依赖原父节点。
|
||||
// editorXMLStandalone 补齐原对象继承的命名空间,使跨页复制不依赖原父节点
|
||||
// 入参: data 对象XML, source 原节点
|
||||
// 返回: []byte 独立XML片段, 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 名称表数据
|
||||
// 返回: []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 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
count := int(binary.BigEndian.Uint16(data[2:]))
|
||||
storage := int(binary.BigEndian.Uint16(data[4:]))
|
||||
if 6+count*12 > storage || storage > len(data) {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
records := make([]fontNameRecord, count)
|
||||
if err := binary.Read(bytes.NewReader(data[6:]), binary.BigEndian, records); err != nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
values := make(map[[4]uint16]string)
|
||||
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
|
||||
}
|
||||
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
|
||||
return records, values
|
||||
}
|
||||
+28
-17
@@ -25,15 +25,33 @@ import (
|
||||
// 入参: data 集合数据, index 字体索引
|
||||
// 返回: []byte 单字体数据, error 错误信息
|
||||
func extractCollectionFont(data []byte, index int) ([]byte, error) {
|
||||
if len(data) < 12 || string(data[:4]) != "ttcf" {
|
||||
return nil, fmt.Errorf("invalid font collection header")
|
||||
tables, err := fontFileTables(data, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
version := binary.BigEndian.Uint32(data[4:])
|
||||
count := uint64(binary.BigEndian.Uint32(data[8:]))
|
||||
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")
|
||||
if len(tables["head"]) < 12 {
|
||||
return nil, fmt.Errorf("invalid head font table")
|
||||
}
|
||||
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) {
|
||||
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]
|
||||
}
|
||||
if len(tables["head"]) < 12 {
|
||||
return nil, fmt.Errorf("invalid head font table")
|
||||
}
|
||||
tables["head"] = bytes.Clone(tables["head"])
|
||||
clear(tables["head"][8:12])
|
||||
return serializeOTF(tables)
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
// packedGlyphRune 获取包装字体字符
|
||||
@@ -537,7 +550,7 @@ func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
|
||||
return mainBuf.Bytes()
|
||||
}
|
||||
|
||||
// shouldBuildCmapFormat12 判断字符超出BMP或format 4长度溢出时是否需要format 12
|
||||
// shouldBuildCmapFormat12 判断终止码、补充平面字符或format 4长度溢出时是否需要format 12
|
||||
// 入参: mapping 字符到字形的映射
|
||||
// 返回: bool 是否需要构建format 12子表
|
||||
func shouldBuildCmapFormat12(mapping map[rune]uint16) bool {
|
||||
@@ -546,13 +559,11 @@ func shouldBuildCmapFormat12(mapping map[rune]uint16) bool {
|
||||
}
|
||||
var codes []int
|
||||
for r := range mapping {
|
||||
if r > 0xFFFF {
|
||||
if r >= 0xFFFF {
|
||||
return true
|
||||
}
|
||||
if r != 0xFFFF {
|
||||
codes = append(codes, int(r))
|
||||
}
|
||||
}
|
||||
if len(codes) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
+9
-4
@@ -27,6 +27,11 @@ type Box struct {
|
||||
X, Y, W, H float64
|
||||
}
|
||||
|
||||
// Point 平面坐标点
|
||||
type Point struct {
|
||||
X, Y float64
|
||||
}
|
||||
|
||||
// ParseBox 解析Box字符串
|
||||
// 入参: s 字符串
|
||||
// 返回: Box 矩形对象, error 错误信息
|
||||
@@ -96,7 +101,7 @@ func (m Matrix) Transform(x, y float64) (float64, float64) {
|
||||
return nx, ny
|
||||
}
|
||||
|
||||
// TransformBox 获取矩形经仿射变换后的轴对齐边界。
|
||||
// TransformBox 获取矩形经仿射变换后的轴对齐边界
|
||||
// 入参: 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}
|
||||
}
|
||||
|
||||
// String 返回标准CTM属性值。
|
||||
// String 返回标准CTM属性值
|
||||
// 返回: 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))
|
||||
}
|
||||
|
||||
// axisAlignedMatrix 判断变换是否保持坐标轴平行,含直角旋转与镜像。
|
||||
// axisAlignedMatrix 判断变换是否保持坐标轴平行,含直角旋转与镜像
|
||||
// 入参: m 仿射变换矩阵
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// matrixVector 变换位移,不包含平移分量。
|
||||
// matrixVector 变换位移,不包含平移分量
|
||||
// 入参: m 仿射变换矩阵, x、y 原始位移
|
||||
// 返回: float64 变换后的X位移, float64 变换后的Y位移
|
||||
func matrixVector(m Matrix, x, y float64) (float64, float64) {
|
||||
|
||||
+11
-11
@@ -21,10 +21,10 @@ import (
|
||||
"github.com/tdewolff/canvas"
|
||||
)
|
||||
|
||||
// ObjectBounds 获取文字、路径或图片在页面坐标中的轴对齐范围,不修改对象。
|
||||
// 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素。
|
||||
// 底纹按填充或描边轮廓度量,不展开图案单元。
|
||||
// 不应用页面边界或父级变换;无可见范围时返回零值,不支持的对象类型返回错误。
|
||||
// ObjectBounds 获取文字、路径或图片在页面坐标中的轴对齐范围,不修改对象
|
||||
// 文字采用字形范围,路径包含描边与裁剪,图片采用裁剪后的几何范围,不解码像素或排除透明像素
|
||||
// 底纹按填充或描边轮廓度量,不展开图案单元
|
||||
// 不应用页面边界或父级变换;无可见范围时返回零值,不支持的对象类型返回错误
|
||||
// 入参: object 图形对象, drawParam 图层绘制参数标识,无继承时为空
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// boundsColor 以底纹的基础颜色和透明度度量轮廓,不修改原画刷。
|
||||
// boundsColor 以底纹的基础颜色和透明度度量轮廓,不修改原画刷
|
||||
// 入参: color 填充或描边颜色
|
||||
// 返回: *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}
|
||||
}
|
||||
|
||||
// boundsRenderer 收集绘制范围,不分配页面像素或合并独立图形轮廓。
|
||||
// boundsRenderer 收集绘制范围,不分配页面像素或合并独立图形轮廓
|
||||
type boundsRenderer struct {
|
||||
box Box
|
||||
}
|
||||
|
||||
// Size 返回不限定边界的度量画布尺寸。
|
||||
// Size 返回不限定边界的度量画布尺寸
|
||||
// 返回: float64 宽度, float64 高度
|
||||
func (r *boundsRenderer) Size() (float64, float64) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// add 合并路径的精确曲线范围并转换为向下的纵轴。
|
||||
// add 合并路径的精确曲线范围并转换为向下的纵轴
|
||||
// 入参: path 绘制路径
|
||||
func (r *boundsRenderer) add(path *canvas.Path) {
|
||||
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()})
|
||||
}
|
||||
|
||||
// RenderPath 收集填充与描边范围,保持线帽、连接和虚线语义。
|
||||
// RenderPath 收集填充与描边范围,保持线帽、连接和虚线语义
|
||||
// 入参: path 路径, style 绘制样式, m 变换矩阵
|
||||
func (r *boundsRenderer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.Matrix) {
|
||||
if style.HasFill() {
|
||||
@@ -137,12 +137,12 @@ func (r *boundsRenderer) RenderPath(path *canvas.Path, style canvas.Style, m can
|
||||
}
|
||||
}
|
||||
|
||||
// RenderText 收集绘制文字范围。
|
||||
// RenderText 收集绘制文字范围
|
||||
// 入参: text 文字, m 变换矩阵
|
||||
func (r *boundsRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
|
||||
text.RenderTo(r, m, 0)
|
||||
}
|
||||
|
||||
// RenderImage 度量不展开底纹图片。
|
||||
// RenderImage 度量不展开底纹图片
|
||||
// 入参: img 图片, m 变换矩阵
|
||||
func (r *boundsRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
|
||||
@@ -171,7 +171,7 @@ func imageWithMask(img, mask image.Image) image.Image {
|
||||
}
|
||||
|
||||
// imageWithClip 应用图片裁剪区域
|
||||
// 低分辨率图片按最低精度细化蒙版,细化后的像素上限16Mi,不降低原图分辨率。
|
||||
// 低分辨率图片按最低精度细化蒙版,细化后的像素上限16Mi,不降低原图分辨率
|
||||
// 入参: img 图片对象, clipPath 裁剪路径, m 图片变换矩阵, dpi 最低蒙版分辨率,0保留原图精度
|
||||
// 返回: 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"
|
||||
)
|
||||
|
||||
// NewShape 创建使用标准紧缩路径的图形,默认黑色描边、不填充。
|
||||
// 可调整返回对象的颜色、线宽等属性后通过 Editor.AddObject 写入。
|
||||
// 水平、垂直直线使用正尺寸边界,路径端点不变。
|
||||
// NewShape 创建使用标准紧缩路径的图形,默认黑色描边、不填充
|
||||
// 可调整返回对象的颜色、线宽等属性后通过 Editor.AddObject 写入
|
||||
// 水平、垂直直线使用正尺寸边界,路径端点不变
|
||||
// 入参: kind 图形类型, box 图形范围;直线从 (X,Y) 到 (X+W,Y+H),W、H 可为负或零
|
||||
// 返回: 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
|
||||
}
|
||||
|
||||
// Shape 识别 NewShape 生成的基本路径及其轴对齐缩放、直角旋转、镜像和平移。
|
||||
// 直线范围以起点和有符号的端点位移表示;非基本路径或不支持的变换返回空类型。
|
||||
// Shape 识别 NewShape 生成的基本路径及其轴对齐缩放、直角旋转、镜像和平移
|
||||
// 直线范围以起点和有符号的端点位移表示;非基本路径或不支持的变换返回空类型
|
||||
// 返回: ShapeKind 图形类型, Box 几何范围(不含描边)
|
||||
func (p PathObject) Shape() (ShapeKind, Box) {
|
||||
tokens := strings.Fields(p.AbbreviatedData)
|
||||
@@ -126,7 +126,7 @@ func (p PathObject) Shape() (ShapeKind, Box) {
|
||||
return kind, box
|
||||
}
|
||||
|
||||
// sameShapePath 比较基本路径的命令与数值,忽略数值格式差异。
|
||||
// sameShapePath 比较基本路径的命令与数值,忽略数值格式差异
|
||||
// 入参: a、b 路径词元
|
||||
// 返回: bool 是否相同
|
||||
func sameShapePath(a, b []string) bool {
|
||||
@@ -146,8 +146,8 @@ func sameShapePath(a, b []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Reshape 调整基本路径在所在坐标系中的几何范围,保留对象标识、方向、缩放和绘制属性,不缩放线宽。
|
||||
// 返回对象可通过 Editor.UpdateObject 写入;仅支持 Shape 可识别的路径。
|
||||
// Reshape 调整基本路径在所在坐标系中的几何范围,保留对象标识、方向、缩放和绘制属性,不缩放线宽
|
||||
// 返回对象可通过 Editor.UpdateObject 写入;仅支持 Shape 可识别的路径
|
||||
// 入参: 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"
|
||||
|
||||
// WriteTo 逐个条目写出OFD,不关闭调用方输出流,出错时应丢弃本次输出
|
||||
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集;编辑资源、原文档字体、可变及彩色字体等保持原样。
|
||||
// 重开子集文档后输入未包含的文字,需要通过AddFont注册完整字体并替换原字体引用。
|
||||
// 新增资源仅写入实际引用的部分,静态TrueType轮廓字体按实际文字生成子集
|
||||
// 原有字体在全包引用可确定时保留字形编号裁剪;编辑资源、复杂字体及无法确定的引用保持原样
|
||||
// 重开子集文档后输入未包含的文字,需要通过AddFont注册完整字体并替换原字体引用
|
||||
// 入参: 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
|
||||
}
|
||||
|
||||
// usedResources 筛选当前引用的新增资源及需要跨页复用的原资源文件,不修改资源池。
|
||||
// usedResources 筛选当前引用的新增资源及需要跨页复用的原资源文件,不修改资源池
|
||||
// 返回: []editorResource 新增字体, []editorResource 新增图片, []string 原资源文件
|
||||
func (e *Editor) usedResources() (fonts, images []editorResource, sourceFiles []string) {
|
||||
used := make(map[string]bool)
|
||||
@@ -282,7 +283,7 @@ func (x *ofdXML) page(page PageContent) {
|
||||
x.end("Page")
|
||||
}
|
||||
|
||||
// layer 写出新建图层及其对象。
|
||||
// layer 写出新建图层及其对象
|
||||
// 入参: layer 图层
|
||||
func (x *ofdXML) layer(layer Layer) {
|
||||
var attrs ofdAttrs
|
||||
|
||||
Reference in new issue
Block a user