mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
526a207aa6
commit
d2c99b1610
5 files changed
+198
-27
No files matched your search
@@ -290,6 +290,8 @@
|
|||||||
<label class="form-row"><span>透明</span><span class="form-unit"><input id="annotationOpacity" type="number" min="0" max="100" step="any" value="80" aria-label="透明度" required><span>%</span></span></label>
|
<label class="form-row"><span>透明</span><span class="form-unit"><input id="annotationOpacity" type="number" min="0" max="100" step="any" value="80" aria-label="透明度" required><span>%</span></span></label>
|
||||||
</div>
|
</div>
|
||||||
<label class="form-row" data-annotation="watermark"><span>排列</span><select id="annotationTile"><option value="single">单个</option><option value="tile">平铺</option></select></label>
|
<label class="form-row" data-annotation="watermark"><span>排列</span><select id="annotationTile"><option value="single">单个</option><option value="tile">平铺</option></select></label>
|
||||||
|
<div class="form-row" data-annotation="watermark"><label id="annotationPlacementLabel" for="annotationPlacement">位置</label><div class="annotation-placement"><select id="annotationPlacement"><option value="page">居中</option><option value="custom">指定</option></select><button id="annotationPick" class="button" type="button">定位</button></div></div>
|
||||||
|
<label class="form-row" data-annotation="watermark"><span>应用</span><select id="annotationScope"><option value="current">本页</option><option value="all">全文</option><option value="custom">指定</option></select></label>
|
||||||
<label class="form-row" data-annotation="note watermark stamp link"><span>页面</span><input id="annotationPages" type="text" required></label>
|
<label class="form-row" data-annotation="note watermark stamp link"><span>页面</span><input id="annotationPages" type="text" required></label>
|
||||||
<div class="form-actions"><div id="annotationCreateStatus" class="form-status" role="status"></div><button id="annotationCreateCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
<div class="form-actions"><div id="annotationCreateStatus" class="form-status" role="status"></div><button id="annotationCreateCancel" class="button" type="button">取消</button><button class="button" type="submit">确定</button></div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ a:focus-visible {
|
|||||||
.object-picker-list {
|
.object-picker-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
max-height: 55vh;
|
max-height: 55vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -657,6 +658,12 @@ button:not(:disabled):hover {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.annotation-placement {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-row input[type="checkbox"] {
|
.form-row input[type="checkbox"] {
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
+79
-10
@@ -363,7 +363,7 @@ const fontManager = new FontManager({
|
|||||||
onPermissionChange: updateFontPermissionHint,
|
onPermissionChange: updateFontPermissionHint,
|
||||||
});
|
});
|
||||||
const fontPicker = new FontPicker(el.textFont, el.textFontToggle, el.textFontList, changeTextFont, loadEditorFonts);
|
const fontPicker = new FontPicker(el.textFont, el.textFontToggle, el.textFontList, changeTextFont, loadEditorFonts);
|
||||||
const annotationFields = Object.fromEntries(["CreateForm", "CreateCancel", "Content", "Old", "Font", "Text", "TextRow", "Image", "ImageRow", "Author", "LinkKind", "Address", "AddressRow", "Target", "TargetRow", "X", "Y", "Width", "Height", "Angle", "Opacity", "Tile", "Pages"].map(key => [key, document.querySelector(`#annotation${key}`)]));
|
const annotationFields = Object.fromEntries(["CreateForm", "CreateCancel", "Content", "Old", "Font", "Text", "TextRow", "Image", "ImageRow", "Author", "LinkKind", "Address", "AddressRow", "Target", "TargetRow", "X", "Y", "Width", "Height", "Angle", "Opacity", "Tile", "Placement", "PlacementLabel", "Pick", "Scope", "Pages"].map(key => [key, document.querySelector(`#annotation${key}`)]));
|
||||||
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
el.editorTools.addEventListener("scroll", () => fontPicker.position());
|
||||||
|
|
||||||
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
||||||
@@ -412,11 +412,26 @@ const canvasEditor = new CanvasEditor(el.viewerPanel, {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
drawStyle: shapeStyle,
|
drawStyle: shapeStyle,
|
||||||
onTool: updateDrawingControls,
|
onTool: () => {
|
||||||
|
updateDrawingControls();
|
||||||
|
const entry = state.annotationCreate;
|
||||||
|
if (entry?.picking && canvasEditor.tool !== "annotation:watermark") {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (state.annotationCreate === entry && entry.picking) {
|
||||||
|
entry.picking = false;
|
||||||
|
if (!state.editing || entry.openSeq !== state.openSeq || canvasEditor.tool) {
|
||||||
|
state.annotationCreate = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.annotationCreate.showModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
onDraw: (index, shape, box, style) => changeDocument("ofdgoInsertShape", null, index, shape,
|
onDraw: (index, shape, box, style) => changeDocument("ofdgoInsertShape", null, index, shape,
|
||||||
box.x, box.y, box.width, box.height, style.fill, style.fillColor, style.stroke, style.strokeColor, style.lineWidth),
|
box.x, box.y, box.width, box.height, style.fill, style.fillColor, style.stroke, style.strokeColor, style.lineWidth),
|
||||||
onDrawText: beginCanvasText,
|
onDrawText: beginCanvasText,
|
||||||
onDrawAnnotation: (index, kind, box) => openAnnotationCreate(kind, index, box),
|
onDrawAnnotation: placeAnnotation,
|
||||||
onDrawInk: (index, points, style, pressure) => changeDocument("ofdgoInsertInk", null, index, JSON.stringify(points), style.lineWidth, pressure, style.strokeColor),
|
onDrawInk: (index, points, style, pressure) => changeDocument("ofdgoInsertInk", null, index, JSON.stringify(points), style.lineWidth, pressure, style.strokeColor),
|
||||||
onCommitText: (item, value, fontData, color = null) => item.draft
|
onCommitText: (item, value, fontData, color = null) => item.draft
|
||||||
? changeDocument("ofdgoInsertText", null, item.index, value, fontData || item.fontData, item.x, item.y, item.size, item.color, item.width, item.wrap, item.align, item.paragraphHeight, item.letterSpacing, ...textIndents(item))
|
? changeDocument("ofdgoInsertText", null, item.index, value, fontData || item.fontData, item.x, item.y, item.size, item.color, item.width, item.wrap, item.align, item.paragraphHeight, item.letterSpacing, ...textIndents(item))
|
||||||
@@ -541,17 +556,24 @@ el.annotationTool.addEventListener("change", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
annotationFields.CreateCancel.addEventListener("click", () => el.annotationCreate.close());
|
annotationFields.CreateCancel.addEventListener("click", () => el.annotationCreate.close());
|
||||||
el.annotationCreate.addEventListener("close", () => { state.annotationCreate = null; });
|
el.annotationCreate.addEventListener("close", () => {
|
||||||
|
if (!state.annotationCreate?.picking) state.annotationCreate = null;
|
||||||
|
});
|
||||||
annotationFields.Content.addEventListener("change", updateAnnotationFields);
|
annotationFields.Content.addEventListener("change", updateAnnotationFields);
|
||||||
annotationFields.Height.addEventListener("input", () => { state.annotationCreate.height = annotationFields.Height.value; });
|
annotationFields.Height.addEventListener("input", () => { state.annotationCreate.height = annotationFields.Height.value; });
|
||||||
annotationFields.LinkKind.addEventListener("change", updateAnnotationFields);
|
annotationFields.LinkKind.addEventListener("change", updateAnnotationFields);
|
||||||
annotationFields.Old.addEventListener("change", () => { annotationFields.Text.value = annotationFields.Old.value; });
|
annotationFields.Old.addEventListener("change", () => { annotationFields.Text.value = annotationFields.Old.value; });
|
||||||
annotationFields.Tile.addEventListener("change", () => {
|
annotationFields.Tile.addEventListener("change", () => {
|
||||||
if (annotationFields.Tile.value === "tile") {
|
annotationFields.Placement.value = "page";
|
||||||
annotationFields.X.value = 10;
|
state.annotationCreate.area = null;
|
||||||
annotationFields.Y.value = 10;
|
updateAnnotationFields();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
annotationFields.Placement.addEventListener("change", () => {
|
||||||
|
if (annotationFields.Placement.value === "custom") pickWatermarkArea();
|
||||||
|
else state.annotationCreate.area = null;
|
||||||
|
});
|
||||||
|
annotationFields.Pick.addEventListener("click", pickWatermarkArea);
|
||||||
|
annotationFields.Scope.addEventListener("change", updateAnnotationFields);
|
||||||
annotationFields.CreateForm.addEventListener("submit", async event => {
|
annotationFields.CreateForm.addEventListener("submit", async event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const entry = state.annotationCreate;
|
const entry = state.annotationCreate;
|
||||||
@@ -569,9 +591,12 @@ annotationFields.CreateForm.addEventListener("submit", async event => {
|
|||||||
}
|
}
|
||||||
if (openSeq !== state.openSeq || state.annotationCreate !== entry) return;
|
if (openSeq !== state.openSeq || state.annotationCreate !== entry) return;
|
||||||
const options = { kind: entry.kind, id: entry.item?.id.replace("annotation:", "") || "", text: value("Text"), old: value("Old"),
|
const options = { kind: entry.kind, id: entry.item?.id.replace("annotation:", "") || "", text: value("Text"), old: value("Old"),
|
||||||
creator: value("Author"), pages: value("Pages"), uri: value("Address"), target: value("LinkKind") === "page" ? Number(value("Target")) : 0,
|
creator: value("Author"), pages: entry.kind === "watermark" && value("Scope") !== "custom"
|
||||||
|
? value("Scope") === "all" ? `1-${state.doc.pageCount}` : String(entry.index + 1) : value("Pages"),
|
||||||
|
uri: value("Address"), target: value("LinkKind") === "page" ? Number(value("Target")) : 0,
|
||||||
x: Number(value("X")), y: Number(value("Y")), width: entry.kind === "note" ? 5 : Number(value("Width")), height: entry.kind === "note" ? 5 : Number(value("Height")),
|
x: Number(value("X")), y: Number(value("Y")), width: entry.kind === "note" ? 5 : Number(value("Width")), height: entry.kind === "note" ? 5 : Number(value("Height")),
|
||||||
angle: Number(value("Angle")), alpha: Math.round((100 - Number(value("Opacity"))) * 255 / 100), tile: value("Tile") === "tile",
|
angle: Number(value("Angle")), alpha: Math.round((100 - Number(value("Opacity"))) * 255 / 100), tile: value("Tile") === "tile",
|
||||||
|
area: entry.kind === "watermark" && value("Placement") === "custom" ? entry.area : null,
|
||||||
size: state.textDefaults.size, color: state.textDefaults.color };
|
size: state.textDefaults.size, color: state.textDefaults.color };
|
||||||
if (entry.kind === "link" && entry.item) {
|
if (entry.kind === "link" && entry.item) {
|
||||||
options.base = entry.info?.linkBase || "";
|
options.base = entry.info?.linkBase || "";
|
||||||
@@ -1455,6 +1480,8 @@ async function createDocument(event) {
|
|||||||
state.editorViews.clear();
|
state.editorViews.clear();
|
||||||
state.styleClipboard = null;
|
state.styleClipboard = null;
|
||||||
state.fontRenderPending = false;
|
state.fontRenderPending = false;
|
||||||
|
state.annotationCreate = null;
|
||||||
|
canvasEditor.setTool("");
|
||||||
canvasEditor.clear();
|
canvasEditor.clear();
|
||||||
delete state.textDefaults.fontChoice;
|
delete state.textDefaults.fontChoice;
|
||||||
updateTextFonts(null, true);
|
updateTextFonts(null, true);
|
||||||
@@ -1514,7 +1541,7 @@ function openObjectPicker(items = canvasEditor.pageItems(state.pageIndex)) {
|
|||||||
function openAnnotationCreate(kind, index, box = null, item = null, info = null) {
|
function openAnnotationCreate(kind, index, box = null, item = null, info = null) {
|
||||||
const page = state.doc.pages[index];
|
const page = state.doc.pages[index];
|
||||||
const width = Math.min(60, page.width), height = 12;
|
const width = Math.min(60, page.width), height = 12;
|
||||||
state.annotationCreate = { kind, index, item, info, height: box?.height || height };
|
state.annotationCreate = { kind, index, item, info, openSeq: state.openSeq, height: box?.height || height };
|
||||||
annotationFields.CreateForm.reset();
|
annotationFields.CreateForm.reset();
|
||||||
annotationFields.Text.value = kind === "watermark" ? "水印" : "";
|
annotationFields.Text.value = kind === "watermark" ? "水印" : "";
|
||||||
annotationFields.Pages.value = String(index + 1);
|
annotationFields.Pages.value = String(index + 1);
|
||||||
@@ -1541,6 +1568,38 @@ function openAnnotationCreate(kind, index, box = null, item = null, info = null)
|
|||||||
focus?.focus();
|
focus?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickWatermarkArea() {
|
||||||
|
state.annotationCreate.picking = true;
|
||||||
|
annotationFields.Placement.value = state.annotationCreate.area ? "custom" : "page";
|
||||||
|
el.annotationCreate.close();
|
||||||
|
canvasEditor.select(null);
|
||||||
|
toggleDrawingTool("annotation:watermark");
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeAnnotation(index, kind, box) {
|
||||||
|
const entry = state.annotationCreate;
|
||||||
|
if (kind !== "watermark" || !entry?.picking) {
|
||||||
|
openAnnotationCreate(kind, index, box);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entry.picking = false;
|
||||||
|
const page = state.doc.pages[index], tile = annotationFields.Tile.value === "tile";
|
||||||
|
const x = Math.max(0, Math.min(1, box.x / page.width));
|
||||||
|
const y = Math.max(0, Math.min(1, box.y / page.height));
|
||||||
|
const right = Math.max(x, Math.min(1, (box.x + box.width) / page.width));
|
||||||
|
const bottom = Math.max(y, Math.min(1, (box.y + box.height) / page.height));
|
||||||
|
if (!tile || right > x && bottom > y) {
|
||||||
|
entry.index = index;
|
||||||
|
entry.area = tile ? { x, y, w: right - x, h: bottom - y } : { x: (x + right) / 2, y: (y + bottom) / 2, w: 0, h: 0 };
|
||||||
|
annotationFields.Placement.value = "custom";
|
||||||
|
if (annotationFields.Scope.value !== "custom") annotationFields.Pages.value = String(index + 1);
|
||||||
|
el.annotationCreateStatus.textContent = "";
|
||||||
|
} else {
|
||||||
|
el.annotationCreateStatus.textContent = "范围过小";
|
||||||
|
}
|
||||||
|
el.annotationCreate.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
function updateAnnotationFields() {
|
function updateAnnotationFields() {
|
||||||
const { kind, item } = state.annotationCreate;
|
const { kind, item } = state.annotationCreate;
|
||||||
for (const key of ["X", "Y", "Width", "Height", "Pages", "Author"]) annotationFields[key].closest(".form-row").hidden = false;
|
for (const key of ["X", "Y", "Width", "Height", "Pages", "Author"]) annotationFields[key].closest(".form-row").hidden = false;
|
||||||
@@ -1553,6 +1612,15 @@ function updateAnnotationFields() {
|
|||||||
annotationFields.TargetRow.hidden = kind !== "link" || annotationFields.LinkKind.value !== "page";
|
annotationFields.TargetRow.hidden = kind !== "link" || annotationFields.LinkKind.value !== "page";
|
||||||
annotationFields.Height.value = image ? "" : state.annotationCreate.height;
|
annotationFields.Height.value = image ? "" : state.annotationCreate.height;
|
||||||
annotationFields.Height.placeholder = image ? "等比" : "";
|
annotationFields.Height.placeholder = image ? "等比" : "";
|
||||||
|
if (kind === "watermark") {
|
||||||
|
for (const key of ["X", "Y"]) annotationFields[key].closest(".form-row").hidden = true;
|
||||||
|
annotationFields.Pages.closest(".form-row").hidden = annotationFields.Scope.value !== "custom";
|
||||||
|
const tile = annotationFields.Tile.value === "tile";
|
||||||
|
annotationFields.PlacementLabel.textContent = tile ? "范围" : "位置";
|
||||||
|
annotationFields.Placement.options[0].textContent = tile ? "整页" : "居中";
|
||||||
|
annotationFields.Placement.options[1].textContent = tile ? "选区" : "指定";
|
||||||
|
annotationFields.Pick.textContent = tile ? "框选" : "定位";
|
||||||
|
}
|
||||||
if (kind === "link" && item) {
|
if (kind === "link" && item) {
|
||||||
for (const key of ["X", "Y", "Width", "Height", "Pages", "Author"]) annotationFields[key].closest(".form-row").hidden = true;
|
for (const key of ["X", "Y", "Width", "Height", "Pages", "Author"]) annotationFields[key].closest(".form-row").hidden = true;
|
||||||
}
|
}
|
||||||
@@ -2734,6 +2802,7 @@ async function openOFD(file) {
|
|||||||
state.fileName = file.name || "ofdgo.ofd";
|
state.fileName = file.name || "ofdgo.ofd";
|
||||||
state.editing = false;
|
state.editing = false;
|
||||||
state.composite = null;
|
state.composite = null;
|
||||||
|
state.annotationCreate = null;
|
||||||
state.pageSelection.clear();
|
state.pageSelection.clear();
|
||||||
state.pageSelectionAnchor = null;
|
state.pageSelectionAnchor = null;
|
||||||
state.pageMultiSelect = false;
|
state.pageMultiSelect = false;
|
||||||
|
|||||||
@@ -783,7 +783,7 @@ export class CanvasEditor {
|
|||||||
this.viewer.releasePointerCapture(drag.pointerID);
|
this.viewer.releasePointerCapture(drag.pointerID);
|
||||||
if (drag.annotation) {
|
if (drag.annotation) {
|
||||||
const point = pagePoint(drag.clientX, drag.clientY, drag.rect, drag.page, drag.rotation);
|
const point = pagePoint(drag.clientX, drag.clientY, drag.rect, drag.page, drag.rotation);
|
||||||
const box = drag.box || { ...point, width: drag.annotation === "note" ? 5 : 40, height: drag.annotation === "note" ? 5 : 12 };
|
const box = drag.box || { ...point, width: drag.annotation === "watermark" ? 0 : drag.annotation === "note" ? 5 : 40, height: drag.annotation === "watermark" ? 0 : drag.annotation === "note" ? 5 : 12 };
|
||||||
drag.preview.remove();
|
drag.preview.remove();
|
||||||
this.setTool("");
|
this.setTool("");
|
||||||
this.options.onDrawAnnotation(drag.page.index, drag.annotation, box);
|
this.options.onDrawAnnotation(drag.page.index, drag.annotation, box);
|
||||||
@@ -975,6 +975,9 @@ export class CanvasEditor {
|
|||||||
const annotation = this.tool.startsWith("annotation:") ? this.tool.slice(11) : "";
|
const annotation = this.tool.startsWith("annotation:") ? this.tool.slice(11) : "";
|
||||||
const shape = annotation ? "rectangle" : this.tool;
|
const shape = annotation ? "rectangle" : this.tool;
|
||||||
const style = this.options.drawStyle();
|
const style = this.options.drawStyle();
|
||||||
|
if (annotation === "watermark") {
|
||||||
|
Object.assign(style, { fill: false, stroke: true, strokeColor: "var(--accent)", lineWidth: 1 });
|
||||||
|
}
|
||||||
if (lineShape(shape)) {
|
if (lineShape(shape)) {
|
||||||
style.fill = false;
|
style.fill = false;
|
||||||
style.stroke = true;
|
style.stroke = true;
|
||||||
@@ -982,6 +985,7 @@ export class CanvasEditor {
|
|||||||
this.drag = { shape, annotation, page, surface, ...this.createPreview(surface, page, shape === "text" ? "rectangle" : shape, style), style, pointerID: event.pointerId,
|
this.drag = { shape, annotation, page, surface, ...this.createPreview(surface, page, shape === "text" ? "rectangle" : shape, style), style, pointerID: event.pointerId,
|
||||||
rect: surface.getBoundingClientRect(), rotation: this.options.rotation(),
|
rect: surface.getBoundingClientRect(), rotation: this.options.rotation(),
|
||||||
clientX: event.clientX, clientY: event.clientY };
|
clientX: event.clientX, clientY: event.clientY };
|
||||||
|
if (annotation === "watermark") this.drag.node.setAttribute("vector-effect", "non-scaling-stroke");
|
||||||
this.viewer.setPointerCapture(event.pointerId);
|
this.viewer.setPointerCapture(event.pointerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+105
-16
@@ -945,6 +945,7 @@ type annotationOptions struct {
|
|||||||
Tile bool
|
Tile bool
|
||||||
Keep bool
|
Keep bool
|
||||||
Target int
|
Target int
|
||||||
|
Area *ofdgo.Box
|
||||||
}
|
}
|
||||||
|
|
||||||
// readAnnotation 获取编辑面板所需的注解内容,不修改文件
|
// readAnnotation 获取编辑面板所需的注解内容,不修改文件
|
||||||
@@ -1011,6 +1012,7 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
var ids []string
|
var ids []string
|
||||||
result, err := changeObjects(func() error {
|
result, err := changeObjects(func() error {
|
||||||
font, resource := "", ""
|
font, resource := "", ""
|
||||||
|
var fontData []byte
|
||||||
if !args[2].IsNull() {
|
if !args[2].IsNull() {
|
||||||
data, err := bytesFromJS(args[2])
|
data, err := bytesFromJS(args[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1020,6 +1022,7 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
fontData = data
|
||||||
}
|
}
|
||||||
if !args[3].IsNull() {
|
if !args[3].IsNull() {
|
||||||
data, err := bytesFromJS(args[3])
|
data, err := bytesFromJS(args[3])
|
||||||
@@ -1062,6 +1065,12 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var watermarkBounds ofdgo.Box
|
||||||
|
if options.Kind == "watermark" {
|
||||||
|
if area := options.Area; area != nil && (area.X < 0 || area.Y < 0 || area.W < 0 || area.H < 0 || area.X+area.W > 1 || area.Y+area.H > 1 || options.Tile && (area.W == 0 || area.H == 0)) {
|
||||||
|
return fmt.Errorf("invalid watermark area")
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, index := range pages {
|
for _, index := range pages {
|
||||||
box := ofdgo.Box{X: options.X, Y: options.Y, W: options.Width, H: options.Height}
|
box := ofdgo.Box{X: options.X, Y: options.Y, W: options.Width, H: options.Height}
|
||||||
var id string
|
var id string
|
||||||
@@ -1105,7 +1114,13 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported annotation kind %q", options.Kind)
|
return fmt.Errorf("unsupported annotation kind %q", options.Kind)
|
||||||
}
|
}
|
||||||
if options.Tile && options.Kind == "watermark" {
|
if options.Kind == "watermark" {
|
||||||
|
if watermarkBounds.W == 0 {
|
||||||
|
watermarkBounds, err = measureWatermark(annotation.Appearance.Objects[0], fontData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
content, err := currentSession.pageContent(index)
|
content, err := currentSession.pageContent(index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1114,21 +1129,7 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
annotation.Appearance.Boundary = fmt.Sprintf("0 0 %g %g", page.W, page.H)
|
layoutWatermark(&annotation, watermarkBounds, options, page)
|
||||||
base := annotation.Appearance.Objects[0]
|
|
||||||
annotation.Appearance.Objects = nil
|
|
||||||
for y := box.Y; y < page.H; y += box.H + 10 {
|
|
||||||
for x := box.X; x < page.W; x += box.W + 10 {
|
|
||||||
object := base
|
|
||||||
boundary := fmt.Sprintf("%g %g %g %g", x, y, box.W, box.H)
|
|
||||||
if object.Type == "TextObject" {
|
|
||||||
object.TextObject.Boundary = boundary
|
|
||||||
} else {
|
|
||||||
object.ImageObject.Boundary = boundary
|
|
||||||
}
|
|
||||||
annotation.Appearance.Objects = append(annotation.Appearance.Objects, object)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
id, err = currentEditor.AddAnnotation(index, annotation)
|
id, err = currentEditor.AddAnnotation(index, annotation)
|
||||||
}
|
}
|
||||||
@@ -1147,6 +1148,94 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
return editorSelectionInfo{editorInfo: result.(editorInfo), SelectedIDs: ids}, nil
|
return editorSelectionInfo{editorInfo: result.(editorInfo), SelectedIDs: ids}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// measureWatermark 复用标准渲染度量水印,临时文档保留尚未提交的字体引用
|
||||||
|
// 入参: object 水印图元, fontData 字体字节
|
||||||
|
// 返回: ofdgo.Box 实际绘制范围, error 错误信息
|
||||||
|
func measureWatermark(object ofdgo.GraphicObject, fontData []byte) (ofdgo.Box, error) {
|
||||||
|
if object.Type == "ImageObject" {
|
||||||
|
return ofdgo.NewMatrix(object.ImageObject.CTM).TransformBox(ofdgo.Box{W: 1, H: 1}), nil
|
||||||
|
}
|
||||||
|
editor := ofdgo.NewEditor()
|
||||||
|
if _, err := editor.AddPage(210, 297); err != nil {
|
||||||
|
return ofdgo.Box{}, err
|
||||||
|
}
|
||||||
|
font, err := editor.AddFont(FontFile{Data: fontData}, 0)
|
||||||
|
if err != nil {
|
||||||
|
return ofdgo.Box{}, err
|
||||||
|
}
|
||||||
|
object.TextObject.Font, object.TextObject.Alpha = font, nil
|
||||||
|
if _, err := editor.AddObject(0, object); err != nil {
|
||||||
|
return ofdgo.Box{}, err
|
||||||
|
}
|
||||||
|
reader, err := editor.Reader()
|
||||||
|
if err != nil {
|
||||||
|
return ofdgo.Box{}, err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
if _, err := reader.PageContentByIndex(0); err != nil {
|
||||||
|
return ofdgo.Box{}, err
|
||||||
|
}
|
||||||
|
bounds, err := ofdgo.NewRenderer(reader).ObjectBounds(ofdgo.GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: ofdgo.CompositeGraphicUnit{
|
||||||
|
Boundary: object.TextObject.Boundary, Objects: []ofdgo.GraphicObject{object},
|
||||||
|
}}, "")
|
||||||
|
if err != nil {
|
||||||
|
return ofdgo.Box{}, err
|
||||||
|
}
|
||||||
|
if bounds.W <= 0 || bounds.H <= 0 {
|
||||||
|
return ofdgo.Box{}, fmt.Errorf("watermark has no visible content")
|
||||||
|
}
|
||||||
|
return bounds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// layoutWatermark 按实际绘制边界居中水印或均匀平铺,选区按页面比例映射
|
||||||
|
// 入参: annotation 水印注解, bounds 绘制范围, options 输入选项, page 页面范围
|
||||||
|
func layoutWatermark(annotation *ofdgo.Annotation, bounds ofdgo.Box, options annotationOptions, page ofdgo.Box) {
|
||||||
|
base := annotation.Appearance.Objects[0]
|
||||||
|
area := ofdgo.Box{W: page.W, H: page.H}
|
||||||
|
if options.Area != nil {
|
||||||
|
area = ofdgo.Box{X: options.Area.X * page.W, Y: options.Area.Y * page.H, W: options.Area.W * page.W, H: options.Area.H * page.H}
|
||||||
|
}
|
||||||
|
scale := 1.0
|
||||||
|
if area.W > 0 && area.H > 0 {
|
||||||
|
scale = math.Min(1, math.Min(area.W/bounds.W, area.H/bounds.H))
|
||||||
|
}
|
||||||
|
if scale != 1 {
|
||||||
|
matrix := ofdgo.NewMatrix(fmt.Sprintf("%g 0 0 %g 0 0", scale, scale))
|
||||||
|
if base.Type == "TextObject" {
|
||||||
|
base.TextObject.CTM = matrix.Multiply(ofdgo.NewMatrix(base.TextObject.CTM)).String()
|
||||||
|
} else {
|
||||||
|
base.ImageObject.CTM = matrix.Multiply(ofdgo.NewMatrix(base.ImageObject.CTM)).String()
|
||||||
|
}
|
||||||
|
bounds = matrix.TransformBox(bounds)
|
||||||
|
}
|
||||||
|
columns, rows := 1, 1
|
||||||
|
if options.Tile {
|
||||||
|
angle := options.Angle * math.Pi / 180
|
||||||
|
width := math.Max(bounds.W, (math.Abs(math.Cos(angle))*options.Width+math.Abs(math.Sin(angle))*options.Height)*scale)
|
||||||
|
height := math.Max(bounds.H, (math.Abs(math.Sin(angle))*options.Width+math.Abs(math.Cos(angle))*options.Height)*scale)
|
||||||
|
columns = max(1, int((area.W+10)/(width+10)))
|
||||||
|
rows = max(1, int((area.H+10)/(height+10)))
|
||||||
|
} else {
|
||||||
|
area = ofdgo.Box{X: area.X + (area.W-bounds.W)/2, Y: area.Y + (area.H-bounds.H)/2, W: bounds.W, H: bounds.H}
|
||||||
|
}
|
||||||
|
annotation.Appearance.Boundary = fmt.Sprintf("%g %g %g %g", area.X, area.Y, area.W, area.H)
|
||||||
|
annotation.Appearance.Objects = make([]ofdgo.GraphicObject, 0, columns*rows)
|
||||||
|
for row := range rows {
|
||||||
|
for column := range columns {
|
||||||
|
x := (float64(column)+0.5)*area.W/float64(columns) - bounds.X - bounds.W/2
|
||||||
|
y := (float64(row)+0.5)*area.H/float64(rows) - bounds.Y - bounds.H/2
|
||||||
|
object := base
|
||||||
|
boundary := fmt.Sprintf("%g %g %g %g", x, y, options.Width, options.Height)
|
||||||
|
if object.Type == "TextObject" {
|
||||||
|
object.TextObject.Boundary = boundary
|
||||||
|
} else {
|
||||||
|
object.ImageObject.Boundary = boundary
|
||||||
|
}
|
||||||
|
annotation.Appearance.Objects = append(annotation.Appearance.Objects, object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// insertInk 将一笔采样写为一个标准矢量对象,复用内部插入与撤销逻辑
|
// insertInk 将一笔采样写为一个标准矢量对象,复用内部插入与撤销逻辑
|
||||||
// 入参: args 页面、JSON采样、线宽、压感开关、颜色及可选父路径
|
// 入参: args 页面、JSON采样、线宽、压感开关、颜色及可选父路径
|
||||||
// 返回: any 文档和选区, error 错误信息
|
// 返回: any 文档和选区, error 错误信息
|
||||||
|
|||||||
Reference in new issue
Block a user