diff --git a/src/core/annotation.js b/src/core/annotation.js index 0e3d81687..1c6877972 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -4382,7 +4382,7 @@ class InkAnnotation extends MarkupAnnotation { const { dict, xref } = params; this.data.annotationType = AnnotationType.INK; this.data.inkLists = []; - this.data.isEditable = !this.data.noHTML && this.data.it === "InkHighlight"; + this.data.isEditable = !this.data.noHTML; // We want to be able to add mouse listeners to the annotation. this.data.noHTML = false; this.data.opacity = dict.get("CA") || 1; @@ -4459,17 +4459,30 @@ class InkAnnotation extends MarkupAnnotation { } static createNewDict(annotation, xref, { apRef, ap }) { - const { color, opacity, paths, outlines, rect, rotation, thickness } = - annotation; - const ink = new Dict(xref); + const { + oldAnnotation, + color, + opacity, + paths, + outlines, + rect, + rotation, + thickness, + user, + } = annotation; + const ink = oldAnnotation || new Dict(xref); ink.set("Type", Name.get("Annot")); ink.set("Subtype", Name.get("Ink")); - ink.set("CreationDate", `D:${getModificationDate()}`); + ink.set(oldAnnotation ? "M" : "CreationDate", `D:${getModificationDate()}`); ink.set("Rect", rect); ink.set("InkList", outlines?.points || paths.points); ink.set("F", 4); ink.set("Rotate", rotation); + if (user) { + ink.set("T", stringToAsciiOrUTF16BE(user)); + } + if (outlines) { // Free highlight. // There's nothing about this in the spec, but it's used when highlighting @@ -4524,12 +4537,15 @@ class InkAnnotation extends MarkupAnnotation { } for (const outline of paths.lines) { - for (let i = 0, ii = outline.length; i < ii; i += 6) { + appearanceBuffer.push( + `${numberToString(outline[4])} ${numberToString(outline[5])} m` + ); + for (let i = 6, ii = outline.length; i < ii; i += 6) { if (isNaN(outline[i])) { appearanceBuffer.push( `${numberToString(outline[i + 4])} ${numberToString( outline[i + 5] - )} m` + )} l` ); } else { const [c1x, c1y, c2x, c2y, x, y] = outline.slice(i, i + 6); @@ -5006,7 +5022,6 @@ class StampAnnotation extends MarkupAnnotation { oldAnnotation ? "M" : "CreationDate", `D:${getModificationDate()}` ); - stamp.set("CreationDate", `D:${getModificationDate()}`); stamp.set("Rect", rect); stamp.set("F", 4); stamp.set("Border", [0, 0, 0]); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index fdfdf7e26..3198522d7 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -2792,6 +2792,8 @@ class CaretAnnotationElement extends AnnotationElement { } class InkAnnotationElement extends AnnotationElement { + #polylinesGroupElement = null; + #polylines = []; constructor(parameters) { @@ -2809,6 +2811,38 @@ class InkAnnotationElement extends AnnotationElement { : AnnotationEditorType.INK; } + #getTransform(rotation, rect) { + // PDF coordinates are calculated from a bottom left origin, so + // transform the polyline coordinates to a top left origin for the + // SVG element. + switch (rotation) { + case 90: + return { + transform: `rotate(90) translate(${-rect[0]},${rect[1]}) scale(1,-1)`, + width: rect[3] - rect[1], + height: rect[2] - rect[0], + }; + case 180: + return { + transform: `rotate(180) translate(${-rect[2]},${rect[1]}) scale(1,-1)`, + width: rect[2] - rect[0], + height: rect[3] - rect[1], + }; + case 270: + return { + transform: `rotate(270) translate(${-rect[2]},${rect[3]}) scale(1,-1)`, + width: rect[3] - rect[1], + height: rect[2] - rect[0], + }; + default: + return { + transform: `translate(${-rect[0]},${rect[3]}) scale(1,-1)`, + width: rect[2] - rect[0], + height: rect[3] - rect[1], + }; + } + } + render() { this.container.classList.add(this.containerClassName); @@ -2817,47 +2851,31 @@ class InkAnnotationElement extends AnnotationElement { const { data: { rect, rotation, inkLists, borderStyle, popupRef }, } = this; - let { width, height } = getRectDims(rect); - let transform; - - // PDF coordinates are calculated from a bottom left origin, so - // transform the polyline coordinates to a top left origin for the - // SVG element. - switch (rotation) { - case 90: - transform = `rotate(90) translate(${-rect[0]},${rect[3] - height}) scale(1,-1)`; - [width, height] = [height, width]; - break; - case 180: - transform = `rotate(180) translate(${-rect[0] - width},${rect[3] - height}) scale(1,-1)`; - break; - case 270: - transform = `rotate(270) translate(${-rect[0] - width},${rect[3]}) scale(1,-1)`; - [width, height] = [height, width]; - break; - default: - transform = `translate(${-rect[0]},${rect[3]}) scale(1,-1)`; - break; - } + const { transform, width, height } = this.#getTransform(rotation, rect); const svg = this.svgFactory.create( width, height, /* skipDimensions = */ true ); - const basePolyline = this.svgFactory.createElement(this.svgElementName); + const g = (this.#polylinesGroupElement = + this.svgFactory.createElement("svg:g")); + svg.append(g); // Ensure that the 'stroke-width' is always non-zero, since otherwise it // won't be possible to open/close the popup (note e.g. issue 11122). - basePolyline.setAttribute("stroke-width", borderStyle.width || 1); - basePolyline.setAttribute("stroke", "transparent"); - basePolyline.setAttribute("fill", "transparent"); - basePolyline.setAttribute("transform", transform); + g.setAttribute("stroke-width", borderStyle.width || 1); + g.setAttribute("stroke-linecap", "round"); + g.setAttribute("stroke-linejoin", "round"); + g.setAttribute("stroke-miterlimit", 10); + g.setAttribute("stroke", "transparent"); + g.setAttribute("fill", "transparent"); + g.setAttribute("transform", transform); for (let i = 0, ii = inkLists.length; i < ii; i++) { - const polyline = i < ii - 1 ? basePolyline.cloneNode() : basePolyline; + const polyline = this.svgFactory.createElement(this.svgElementName); this.#polylines.push(polyline); polyline.setAttribute("points", inkLists[i].join(",")); - svg.append(polyline); + g.append(polyline); } if (!popupRef && this.hasPopupData) { @@ -2870,6 +2888,29 @@ class InkAnnotationElement extends AnnotationElement { return this.container; } + updateEdited(params) { + super.updateEdited(params); + const { thickness, points, rect } = params; + const g = this.#polylinesGroupElement; + if (thickness >= 0) { + g.setAttribute("stroke-width", thickness || 1); + } + if (points) { + for (let i = 0, ii = this.#polylines.length; i < ii; i++) { + this.#polylines[i].setAttribute("points", points[i].join(",")); + } + } + if (rect) { + const { transform, width, height } = this.#getTransform( + this.data.rotation, + rect + ); + const root = g.parentElement; + root.setAttribute("viewBox", `0 0 ${width} ${height}`); + g.setAttribute("transform", transform); + } + } + getElementsToTriggerPopup() { return this.#polylines; } diff --git a/src/display/editor/drawers/inkdraw.js b/src/display/editor/drawers/inkdraw.js index bcc7f2b57..8e0314c75 100644 --- a/src/display/editor/drawers/inkdraw.js +++ b/src/display/editor/drawers/inkdraw.js @@ -108,14 +108,7 @@ class InkDrawOutliner { } this.#last.set([x1, y1, x2, y2, x, y], 0); - this.#line.push( - (x1 + 5 * x2) / 6, - (y1 + 5 * y2) / 6, - (5 * x2 + x) / 6, - (5 * y2 + y) / 6, - (x2 + x) / 2, - (y2 + y) / 2 - ); + this.#line.push(...Outline.createBezierPoints(x1, y1, x2, y2, x, y)); return { path: { @@ -485,6 +478,51 @@ class InkDrawOutline extends Outline { break; } + if (!lines) { + lines = []; + for (const point of points) { + const len = point.length; + if (len === 2) { + lines.push( + new Float32Array([NaN, NaN, NaN, NaN, point[0], point[1]]) + ); + continue; + } + if (len === 4) { + lines.push( + new Float32Array([ + NaN, + NaN, + NaN, + NaN, + point[0], + point[1], + NaN, + NaN, + NaN, + NaN, + point[2], + point[3], + ]) + ); + continue; + } + const line = new Float32Array(3 * (len - 2)); + lines.push(line); + let [x1, y1, x2, y2] = point.subarray(0, 4); + line.set([NaN, NaN, NaN, NaN, x1, y1], 0); + for (let i = 4; i < len; i += 2) { + const x = point[i]; + const y = point[i + 1]; + line.set( + Outline.createBezierPoints(x1, y1, x2, y2, x, y), + (i - 2) * 3 + ); + [x1, y1, x2, y2] = [x2, y2, x, y]; + } + } + } + for (let i = 0, ii = lines.length; i < ii; i++) { newLines.push({ line: rescaleFn( diff --git a/src/display/editor/drawers/outline.js b/src/display/editor/drawers/outline.js index 27fceed76..e06aafcb3 100644 --- a/src/display/editor/drawers/outline.js +++ b/src/display/editor/drawers/outline.js @@ -97,6 +97,17 @@ class Outline { return [x, y]; } } + + static createBezierPoints(x1, y1, x2, y2, x3, y3) { + return [ + (x1 + 5 * x2) / 6, + (y1 + 5 * y2) / 6, + (5 * x2 + x3) / 6, + (5 * y2 + y3) / 6, + (x2 + x3) / 2, + (y2 + y3) / 2, + ]; + } } export { Outline }; diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index e47388e0a..026d22574 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -66,7 +66,7 @@ class AnnotationEditor { #hasBeenClicked = false; - #initialPosition = null; + #initialRect = null; #isEditing = false; @@ -468,13 +468,13 @@ class AnnotationEditor { * @param {number} y - y-translation in page coordinates. */ translateInPage(x, y) { - this.#initialPosition ||= [this.x, this.y]; + this.#initialRect ||= [this.x, this.y, this.width, this.height]; this.#translate(this.pageDimensions, x, y); this.div.scrollIntoView({ block: "nearest" }); } drag(tx, ty) { - this.#initialPosition ||= [this.x, this.y]; + this.#initialRect ||= [this.x, this.y, this.width, this.height]; const { div, parentDimensions: [parentWidth, parentHeight], @@ -530,9 +530,16 @@ class AnnotationEditor { get _hasBeenMoved() { return ( - !!this.#initialPosition && - (this.#initialPosition[0] !== this.x || - this.#initialPosition[1] !== this.y) + !!this.#initialRect && + (this.#initialRect[0] !== this.x || this.#initialRect[1] !== this.y) + ); + } + + get _hasBeenResized() { + return ( + !!this.#initialRect && + (this.#initialRect[2] !== this.width || + this.#initialRect[3] !== this.height) ); } @@ -989,6 +996,7 @@ class AnnotationEditor { const newX = oppositeX - transfOppositePoint[0]; const newY = oppositeY - transfOppositePoint[1]; + this.#initialRect ||= [this.x, this.y, this.width, this.height]; this.width = newWidth; this.height = newHeight; this.x = newX; diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index e1a10f0c1..2f30dce59 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -138,11 +138,44 @@ class InkEditor extends DrawingEditor { /** @inheritdoc */ static async deserialize(data, parent, uiManager) { + let initialData = null; if (data instanceof InkAnnotationElement) { - return null; + const { + data: { + inkLists, + rect, + rotation, + id, + color, + opacity, + borderStyle: { rawWidth: thickness }, + popupRef, + }, + parent: { + page: { pageNumber }, + }, + } = data; + initialData = data = { + annotationType: AnnotationEditorType.INK, + color: Array.from(color), + thickness, + opacity, + paths: { points: inkLists }, + boxes: null, + pageIndex: pageNumber - 1, + rect: rect.slice(0), + rotation, + id, + deleted: false, + popupRef, + }; } - return super.deserialize(data, parent, uiManager); + const editor = await super.deserialize(data, parent, uiManager); + editor.annotationElementId = data.id || null; + editor._initialData = initialData; + + return editor; } /** @inheritdoc */ @@ -214,9 +247,41 @@ class InkEditor extends DrawingEditor { structTreeParentId: this._structTreeParentId, }; + if (isForCopying) { + return serialized; + } + + if (this.annotationElementId && !this.#hasElementChanged(serialized)) { + return null; + } + serialized.id = this.annotationElementId; return serialized; } + + #hasElementChanged(serialized) { + const { color, thickness, opacity, pageIndex } = this._initialData; + return ( + this._hasBeenMoved || + this._hasBeenResized || + serialized.color.some((c, i) => c !== color[i]) || + serialized.thickness !== thickness || + serialized.opacity !== opacity || + serialized.pageIndex !== pageIndex + ); + } + + /** @inheritdoc */ + renderAnnotationElement(annotation) { + const { points, rect } = this.serializeDraw(/* isForCopying = */ false); + annotation.updateEdited({ + rect, + thickness: this._drawingOptions["stroke-width"], + points, + }); + + return null; + } } export { InkEditor }; diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index a2d0ff3dc..972e009ca 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -921,19 +921,19 @@ class StampEditor extends AnnotationEditor { #hasElementChanged(serialized) { const { - rect, pageIndex, accessibilityData: { altText }, } = this._initialData; - const isSameRect = serialized.rect.every( - (x, i) => Math.abs(x - rect[i]) < 1 - ); const isSamePageIndex = serialized.pageIndex === pageIndex; const isSameAltText = (serialized.accessibilityData?.alt || "") === altText; return { - isSame: isSameRect && isSamePageIndex && isSameAltText, + isSame: + !this._hasBeenMoved && + !this._hasBeenResized && + isSamePageIndex && + isSameAltText, isSameAltText, }; } diff --git a/test/integration/annotation_spec.mjs b/test/integration/annotation_spec.mjs index 7431a17d1..4e444f5a4 100644 --- a/test/integration/annotation_spec.mjs +++ b/test/integration/annotation_spec.mjs @@ -504,14 +504,6 @@ describe("ResetForm action", () => { it("must check that the Ink annotation has a popup", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - if (browserName) { - // TODO - pending( - "Re-enable this test when the Ink annotation has been made editable." - ); - return; - } - await page.waitForFunction( `document.querySelector("[data-annotation-id='25R']").hidden === false` ); diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index 29e89f28a..0c3f9d724 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -28,6 +28,7 @@ import { getSelectedEditors, getSerialized, hover, + isCanvasWhite, kbBigMoveDown, kbBigMoveLeft, kbBigMoveRight, @@ -988,27 +989,8 @@ describe("FreeText Editor", () => { pages.map(async ([browserName, page]) => { await switchToFreeText(page); - const isEditorWhite = editorRect => - page.evaluate(rect => { - const canvas = document.querySelector(".canvasWrapper canvas"); - const ctx = canvas.getContext("2d"); - rect ||= { - x: 0, - y: 0, - width: canvas.width, - height: canvas.height, - }; - const { data } = ctx.getImageData( - rect.x, - rect.y, - rect.width, - rect.height - ); - return data.every(x => x === 0xff); - }, editorRect); - // The page has been re-rendered but with no freetext annotations. - let isWhite = await isEditorWhite(); + let isWhite = await isCanvasWhite(page, 1); expect(isWhite).withContext(`In ${browserName}`).toBeTrue(); let editorIds = await getEditors(page, "freeText"); @@ -1066,7 +1048,7 @@ describe("FreeText Editor", () => { editorIds = await getEditors(page, "freeText"); expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1); - isWhite = await isEditorWhite(editorRect); + isWhite = await isCanvasWhite(page, 1, editorRect); expect(isWhite).withContext(`In ${browserName}`).toBeTrue(); // Check we've now a div containing the text. diff --git a/test/integration/ink_editor_spec.mjs b/test/integration/ink_editor_spec.mjs index 01ba30fcd..1f908fde7 100644 --- a/test/integration/ink_editor_spec.mjs +++ b/test/integration/ink_editor_spec.mjs @@ -17,9 +17,14 @@ import { awaitPromise, closePages, createPromise, + dragAndDropAnnotation, + getAnnotationSelector, + getEditors, getEditorSelector, getRect, getSelectedEditors, + getSerialized, + isCanvasWhite, kbRedo, kbSelectAll, kbUndo, @@ -27,8 +32,10 @@ import { scrollIntoView, switchToEditor, waitForNoElement, + waitForSelectedEditor, waitForSerialized, waitForStorageEntries, + waitForTimeout, } from "./test_utils.mjs"; const waitForPointerUp = page => @@ -580,7 +587,7 @@ describe("Ink Editor", () => { await closePages(pages); }); - it("must check that the color has been changed", async () => { + it("must check that the deletion has been undid", async () => { await Promise.all( pages.map(async ([browserName, page]) => { await switchToInk(page); @@ -669,4 +676,157 @@ describe("Ink Editor", () => { ); }); }); + + describe("Ink (update existing)", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("inks.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must update an existing annotation", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const annotationsRect = await page.evaluate(() => { + let xm = Infinity, + xM = -Infinity, + ym = Infinity, + yM = -Infinity; + for (const el of document.querySelectorAll( + "section.inkAnnotation" + )) { + const { x, y, width, height } = el.getBoundingClientRect(); + xm = Math.min(xm, x); + xM = Math.max(xM, x + width); + ym = Math.min(ym, y); + yM = Math.max(yM, y + height); + } + return { x: xm, y: ym, width: xM - xm, height: yM - ym }; + }); + + await switchToInk(page); + + // The page has been re-rendered but with no ink annotations. + let isWhite = await isCanvasWhite(page, 1, annotationsRect); + expect(isWhite).withContext(`In ${browserName}`).toBeTrue(); + + let editorIds = await getEditors(page, "ink"); + expect(editorIds.length).withContext(`In ${browserName}`).toEqual(15); + + const pdfjsA = getEditorSelector(0); + const editorRect = await getRect(page, pdfjsA); + await page.mouse.click( + editorRect.x + editorRect.width / 2, + editorRect.y + editorRect.height / 2 + ); + await waitForSelectedEditor(page, pdfjsA); + + const red = "#ff0000"; + page.evaluate(value => { + window.PDFViewerApplication.eventBus.dispatch( + "switchannotationeditorparams", + { + source: null, + type: window.pdfjsLib.AnnotationEditorParamsType.INK_COLOR, + value, + } + ); + }, red); + + const serialized = await getSerialized(page); + expect(serialized.length).withContext(`In ${browserName}`).toEqual(1); + expect(serialized[0].color).toEqual([255, 0, 0]); + + // Disable editing mode. + await switchToInk(page, /* disable = */ true); + + // We want to check that the editor is displayed but not the original + // canvas. + editorIds = await getEditors(page, "ink"); + expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1); + + isWhite = await isCanvasWhite(page, 1, editorRect); + expect(isWhite).withContext(`In ${browserName}`).toBeTrue(); + + // Check we've now a svg with a red stroke. + await page.waitForSelector("svg[stroke = '#ff0000']", { + visible: true, + }); + + // Re-enable editing mode. + await switchToInk(page); + await page.focus(".annotationEditorLayer"); + + await kbUndo(page); + await waitForSerialized(page, 0); + + editorIds = await getEditors(page, "ink"); + expect(editorIds.length).withContext(`In ${browserName}`).toEqual(15); + + // Undo again. + await kbUndo(page); + // Nothing should happen, it's why we can't wait for something + // specific! + // eslint-disable-next-line no-restricted-syntax + await waitForTimeout(200); + + // We check that the editor hasn't been removed. + editorIds = await getEditors(page, "ink"); + expect(editorIds.length).withContext(`In ${browserName}`).toEqual(15); + }) + ); + }); + }); + + describe("Ink (move existing)", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("inks.pdf", getAnnotationSelector("277R")); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must move an annotation", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click(getAnnotationSelector("277R"), { count: 2 }); + const edgeB = getEditorSelector(10); + await waitForSelectedEditor(page, edgeB); + + const editorIds = await getEditors(page, "ink"); + expect(editorIds.length).withContext(`In ${browserName}`).toEqual(15); + + // All the current annotations should be serialized as null objects + // because they haven't been edited yet. + const serialized = await getSerialized(page); + expect(serialized).withContext(`In ${browserName}`).toEqual([]); + + const editorRect = await page.$eval(edgeB, el => { + const { x, y, width, height } = el.getBoundingClientRect(); + return { x, y, width, height }; + }); + + // Select the annotation we want to move. + await page.mouse.click(editorRect.x + 2, editorRect.y + 2); + await waitForSelectedEditor(page, edgeB); + + await dragAndDropAnnotation( + page, + editorRect.x + editorRect.width / 2, + editorRect.y + editorRect.height / 2, + 100, + 100 + ); + await waitForSerialized(page, 1); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 65213567a..6138035b0 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -781,6 +781,28 @@ function waitForNoElement(page, selector) { ); } +function isCanvasWhite(page, pageNumber, rectangle) { + return page.evaluate( + (rect, pageN) => { + const canvas = document.querySelector( + `.page[data-page-number = "${pageN}"] .canvasWrapper canvas` + ); + const canvasRect = canvas.getBoundingClientRect(); + const ctx = canvas.getContext("2d"); + rect ||= canvasRect; + const { data } = ctx.getImageData( + rect.x - canvasRect.x, + rect.y - canvasRect.y, + rect.width, + rect.height + ); + return new Uint32Array(data.buffer).every(x => x === 0xffffffff); + }, + rectangle, + pageNumber + ); +} + export { applyFunctionToEditor, awaitPromise, @@ -806,6 +828,7 @@ export { getSerialized, getSpanRectFromText, hover, + isCanvasWhite, isVisible, kbBigMoveDown, kbBigMoveLeft, diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 45949cc35..7083ac7d9 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -685,3 +685,5 @@ !issue19120.pdf !bug1934157.pdf !rotated_ink.pdf +!inks.pdf +!inks_basic.pdf diff --git a/test/pdfs/inks.pdf b/test/pdfs/inks.pdf new file mode 100644 index 000000000..371144239 Binary files /dev/null and b/test/pdfs/inks.pdf differ diff --git a/test/pdfs/inks_basic.pdf b/test/pdfs/inks_basic.pdf new file mode 100644 index 000000000..de82e5a18 Binary files /dev/null and b/test/pdfs/inks_basic.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 98a5c23da..d3167615d 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -10872,5 +10872,334 @@ "talos": false, "type": "eq", "link": true + }, + { + "id": "inks_basic-editor-save-print", + "file": "pdfs/inks_basic.pdf", + "md5": "2615610de59b4849993dcc2614ebb266", + "rounds": 1, + "lastPage": 1, + "type": "eq", + "save": true, + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 15, + "color": [241, 17, 41], + "opacity": 1, + "thickness": 20, + "paths": { + "lines": [ + [ + null, + null, + null, + null, + 114, + 691.5, + null, + null, + null, + null, + 159.75, + 631.5 + ] + ], + "points": [[114, 691.5, 159.75, 631.5]] + }, + "pageIndex": 0, + "rect": [ + 104.0000076523194, 621.5000049701105, 169.7500193485847, + 701.5000020036331 + ], + "rotation": 0, + "structTreeParentId": null, + "id": "16R" + }, + "pdfjs_internal_editor_1": { + "id": "17R", + "deleted": true, + "pageIndex": 0, + "popupRef": "" + }, + "pdfjs_internal_editor_2": { + "annotationType": 15, + "color": [0, 0, 0], + "opacity": 1, + "thickness": 3, + "paths": { + "lines": [ + [ + null, + null, + null, + null, + 475.0961608886719, + 647.4615478515625, + null, + null, + null, + null, + 520.8461303710938, + 587.4615478515625 + ] + ], + "points": [ + [ + 475.0961608886719, 647.4615478515625, 520.8461303710938, + 587.4615478515625 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 473.59618278650134, 585.9615278473268, 522.3461772570244, + 648.9615135559669 + ], + "rotation": 0, + "structTreeParentId": null, + "id": "18R" + }, + "pdfjs_internal_editor_3": { + "annotationType": 15, + "color": [250, 23, 28], + "opacity": 0.55, + "thickness": 3, + "paths": { + "lines": [ + [ + null, + null, + null, + null, + 79.04861450195312, + 532.353759765625, + null, + null, + null, + null, + 198.18736267089844, + 378.6045837402344 + ] + ], + "points": [ + [ + 79.04861450195312, 532.353759765625, 198.18736267089844, + 378.6045837402344 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 77.54861622361037, 377.10462435392236, 199.6873893210521, + 533.8537948498359 + ], + "rotation": 0, + "structTreeParentId": null, + "id": "19R" + }, + "pdfjs_internal_editor_4": { + "annotationType": 15, + "color": [70, 108, 241], + "opacity": 0.7, + "thickness": 20, + "paths": { + "lines": [ + [ + null, + null, + null, + null, + 273.6300048828125, + 535.47998046875, + null, + null, + null, + null, + 319.3800048828125, + 475.4800109863281 + ] + ], + "points": [ + [ + 273.6300048828125, 535.47998046875, 319.3800048828125, + 475.4800109863281 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 263.629992209948, 465.48002000955444, 329.38000390621335, + 545.4799757370582 + ], + "rotation": 0, + "structTreeParentId": null, + "id": "20R" + } + } + }, + { + "id": "inks_basic-editor-print", + "file": "pdfs/inks_basic.pdf", + "md5": "2615610de59b4849993dcc2614ebb266", + "rounds": 1, + "lastPage": 1, + "type": "eq", + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 15, + "color": [241, 17, 41], + "opacity": 1, + "thickness": 20, + "paths": { + "lines": [ + [ + null, + null, + null, + null, + 114, + 691.5, + null, + null, + null, + null, + 159.75, + 631.5 + ] + ], + "points": [[114, 691.5, 159.75, 631.5]] + }, + "pageIndex": 0, + "rect": [ + 104.0000076523194, 621.5000049701105, 169.7500193485847, + 701.5000020036331 + ], + "rotation": 0, + "structTreeParentId": null, + "id": "16R" + }, + "pdfjs_internal_editor_1": { + "id": "17R", + "deleted": true, + "pageIndex": 0, + "popupRef": "" + }, + "pdfjs_internal_editor_2": { + "annotationType": 15, + "color": [0, 0, 0], + "opacity": 1, + "thickness": 3, + "paths": { + "lines": [ + [ + null, + null, + null, + null, + 475.0961608886719, + 647.4615478515625, + null, + null, + null, + null, + 520.8461303710938, + 587.4615478515625 + ] + ], + "points": [ + [ + 475.0961608886719, 647.4615478515625, 520.8461303710938, + 587.4615478515625 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 473.59618278650134, 585.9615278473268, 522.3461772570244, + 648.9615135559669 + ], + "rotation": 0, + "structTreeParentId": null, + "id": "18R" + }, + "pdfjs_internal_editor_3": { + "annotationType": 15, + "color": [250, 23, 28], + "opacity": 0.55, + "thickness": 3, + "paths": { + "lines": [ + [ + null, + null, + null, + null, + 79.04861450195312, + 532.353759765625, + null, + null, + null, + null, + 198.18736267089844, + 378.6045837402344 + ] + ], + "points": [ + [ + 79.04861450195312, 532.353759765625, 198.18736267089844, + 378.6045837402344 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 77.54861622361037, 377.10462435392236, 199.6873893210521, + 533.8537948498359 + ], + "rotation": 0, + "structTreeParentId": null, + "id": "19R" + }, + "pdfjs_internal_editor_4": { + "annotationType": 15, + "color": [70, 108, 241], + "opacity": 0.7, + "thickness": 20, + "paths": { + "lines": [ + [ + null, + null, + null, + null, + 273.6300048828125, + 535.47998046875, + null, + null, + null, + null, + 319.3800048828125, + 475.4800109863281 + ] + ], + "points": [ + [ + 273.6300048828125, 535.47998046875, 319.3800048828125, + 475.4800109863281 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 263.629992209948, 465.48002000955444, 329.38000390621335, + 545.4799757370582 + ], + "rotation": 0, + "structTreeParentId": null, + "id": "20R" + } + } } ]