From cdc58b7a527d8ab6b2b68186e519ad700389def3 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sun, 19 Jun 2022 16:39:54 +0200 Subject: [PATCH] Rotate annotations based on the MK::R value (bug 1675139) - it aims to fix: https://bugzilla.mozilla.org/show_bug.cgi?id=1675139; - An annotation can be rotated (counterclockwise); - the rotation can be set in using JS. --- src/core/annotation.js | 340 ++++++++++++++++++++++------- src/display/annotation_layer.js | 38 +++- src/scripting_api/field.js | 18 +- test/integration/scripting_spec.js | 43 ++++ test/pdfs/.gitignore | 1 + test/pdfs/bug1675139.pdf | Bin 0 -> 14835 bytes test/test_manifest.json | 39 ++++ test/unit/annotation_spec.js | 158 ++++++++++++++ test/unit/api_spec.js | 2 + web/annotation_layer_builder.css | 1 + 10 files changed, 562 insertions(+), 78 deletions(-) create mode 100755 test/pdfs/bug1675139.pdf diff --git a/src/core/annotation.js b/src/core/annotation.js index bd9dc692a..2665ac66a 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -24,6 +24,7 @@ import { assert, escapeString, getModificationDate, + IDENTITY_MATRIX, isAscii, LINE_DESCENT_FACTOR, LINE_FACTOR, @@ -430,9 +431,11 @@ class Annotation { this.setColor(dict.getArray("C")); this.setBorderStyle(dict); this.setAppearance(dict); - this.setBorderAndBackgroundColors(dict.get("MK")); - this._hasOwnCanvas = false; + const MK = dict.get("MK"); + this.setBorderAndBackgroundColors(MK); + this.setRotation(MK); + this._streams = []; if (this.appearance) { this._streams.push(this.appearance); @@ -445,12 +448,14 @@ class Annotation { color: this.color, backgroundColor: this.backgroundColor, borderColor: this.borderColor, + rotation: this.rotation, contentsObj: this._contents, hasAppearance: !!this.appearance, id: params.id, modificationDate: this.modificationDate, rect: this.rectangle, subtype: params.subtype, + hasOwnCanvas: false, }; if (params.collectFields) { @@ -704,6 +709,22 @@ class Annotation { } } + setRotation(mk) { + this.rotation = 0; + if (mk instanceof Dict) { + let angle = mk.get("R") || 0; + if (Number.isInteger(angle) && angle !== 0) { + angle %= 360; + if (angle < 0) { + angle += 360; + } + if (angle % 90 === 0) { + this.rotation = angle; + } + } + } + } + /** * Set the color for background and border if any. * The default values are transparent. @@ -721,33 +742,6 @@ class Annotation { } } - getBorderAndBackgroundAppearances() { - if (!this.backgroundColor && !this.borderColor) { - return ""; - } - const width = this.data.rect[2] - this.data.rect[0]; - const height = this.data.rect[3] - this.data.rect[1]; - const rect = `0 0 ${width} ${height} re`; - - let str = ""; - if (this.backgroundColor) { - str = `${getPdfColor( - this.backgroundColor, - /* isFill */ true - )} ${rect} f `; - } - - if (this.borderColor) { - const borderWidth = this.borderStyle.width || 1; - str += `${borderWidth} w ${getPdfColor( - this.borderColor, - /* isFill */ false - )} ${rect} S `; - } - - return str; - } - /** * Set the border style (as AnnotationBorderStyle object). * @@ -849,7 +843,7 @@ class Annotation { const data = this.data; let appearance = this.appearance; const isUsingOwnCanvas = - this._hasOwnCanvas && intent & RenderingIntentFlag.DISPLAY; + this.data.hasOwnCanvas && intent & RenderingIntentFlag.DISPLAY; if (!appearance) { if (!isUsingOwnCanvas) { return Promise.resolve(new OperatorList()); @@ -918,6 +912,7 @@ class Annotation { type: "", kidIds: this.data.kidIds, page: this.data.pageIndex, + rotation: this.rotation, }; } return null; @@ -1466,6 +1461,72 @@ class WidgetAnnotation extends Annotation { return !!(this.data.fieldFlags & flag); } + getRotationMatrix(annotationStorage) { + const storageEntry = annotationStorage + ? annotationStorage.get(this.data.id) + : undefined; + let rotation = storageEntry && storageEntry.rotation; + if (rotation === undefined) { + rotation = this.rotation; + } + + if (rotation === 0) { + return IDENTITY_MATRIX; + } + + const width = this.data.rect[2] - this.data.rect[0]; + const height = this.data.rect[3] - this.data.rect[1]; + + switch (rotation) { + case 90: + return [0, 1, -1, 0, width, 0]; + case 180: + return [-1, 0, 0, -1, width, height]; + case 270: + return [0, -1, 1, 0, 0, height]; + default: + throw new Error("Invalid rotation"); + } + } + + getBorderAndBackgroundAppearances(annotationStorage) { + const storageEntry = annotationStorage + ? annotationStorage.get(this.data.id) + : undefined; + let rotation = storageEntry && storageEntry.rotation; + if (rotation === undefined) { + rotation = this.rotation; + } + + if (!this.backgroundColor && !this.borderColor) { + return ""; + } + const width = this.data.rect[2] - this.data.rect[0]; + const height = this.data.rect[3] - this.data.rect[1]; + const rect = + rotation === 0 || rotation === 180 + ? `0 0 ${width} ${height} re` + : `0 0 ${height} ${width} re`; + + let str = ""; + if (this.backgroundColor) { + str = `${getPdfColor( + this.backgroundColor, + /* isFill */ true + )} ${rect} f `; + } + + if (this.borderColor) { + const borderWidth = this.borderStyle.width || 1; + str += `${borderWidth} w ${getPdfColor( + this.borderColor, + /* isFill */ false + )} ${rect} S `; + } + + return str; + } + getOperatorList(evaluator, task, intent, renderForms, annotationStorage) { // Do not render form elements on the canvas when interactive forms are // enabled. The display layer is responsible for rendering them instead. @@ -1516,7 +1577,7 @@ class WidgetAnnotation extends Annotation { this.data.id, this.data.rect, transform, - matrix, + this.getRotationMatrix(annotationStorage), ]); const stream = new StringStream(content); @@ -1535,13 +1596,34 @@ class WidgetAnnotation extends Annotation { ); } + _getMKDict(rotation) { + const mk = new Dict(null); + if (rotation) { + mk.set("R", rotation); + } + if (this.borderColor) { + mk.set( + "BC", + Array.from(this.borderColor).map(c => c / 255) + ); + } + if (this.backgroundColor) { + mk.set( + "BG", + Array.from(this.backgroundColor).map(c => c / 255) + ); + } + return mk.size > 0 ? mk : null; + } + async save(evaluator, task, annotationStorage) { const storageEntry = annotationStorage ? annotationStorage.get(this.data.id) : undefined; let value = storageEntry && storageEntry.value; + let rotation = storageEntry && storageEntry.rotation; if (value === this.data.fieldValue || value === undefined) { - if (!this._hasValueFromXFA) { + if (!this._hasValueFromXFA && rotation === undefined) { return null; } value = value || this.data.fieldValue; @@ -1549,6 +1631,7 @@ class WidgetAnnotation extends Annotation { // Value can be an array (with choice list and multiple selections) if ( + rotation === undefined && !this._hasValueFromXFA && Array.isArray(value) && Array.isArray(this.data.fieldValue) && @@ -1558,6 +1641,10 @@ class WidgetAnnotation extends Annotation { return null; } + if (rotation === undefined) { + rotation = this.rotation; + } + let appearance = await this._getAppearance( evaluator, task, @@ -1606,12 +1693,23 @@ class WidgetAnnotation extends Annotation { dict.set("AP", AP); dict.set("M", `D:${getModificationDate()}`); + const maybeMK = this._getMKDict(rotation); + if (maybeMK) { + dict.set("MK", maybeMK); + } + const appearanceDict = new Dict(xref); appearanceDict.set("Length", appearance.length); appearanceDict.set("Subtype", Name.get("Form")); appearanceDict.set("Resources", this._getSaveFieldResources(xref)); appearanceDict.set("BBox", bbox); + const rotationMatrix = this.getRotationMatrix(annotationStorage); + if (rotationMatrix !== IDENTITY_MATRIX) { + // The matrix isn't the identity one. + appearanceDict.set("Matrix", rotationMatrix); + } + const bufferOriginal = [`${this.ref.num} ${this.ref.gen} obj\n`]; writeDict(dict, bufferOriginal, originalTransform); bufferOriginal.push("\nendobj\n"); @@ -1637,13 +1735,21 @@ class WidgetAnnotation extends Annotation { const storageEntry = annotationStorage ? annotationStorage.get(this.data.id) : undefined; - let value = - storageEntry && (storageEntry.formattedValue || storageEntry.value); - if (value === undefined) { + + let value, rotation; + if (storageEntry) { + value = storageEntry.formattedValue || storageEntry.value; + rotation = storageEntry.rotation; + } + + if (rotation === undefined && value === undefined) { if (!this._hasValueFromXFA || this.appearance) { // The annotation hasn't been rendered so use the appearance. return null; } + } + + if (value === undefined) { // The annotation has its value in XFA datasets but not in the V field. value = this.data.fieldValue; if (!value) { @@ -1651,6 +1757,10 @@ class WidgetAnnotation extends Annotation { } } + if (Array.isArray(value) && value.length === 1) { + value = value[0]; + } + assert(typeof value === "string", "Expected `value` to be a string."); value = value.trim(); @@ -1660,6 +1770,10 @@ class WidgetAnnotation extends Annotation { return ""; } + if (rotation === undefined) { + rotation = this.rotation; + } + let lineCount = -1; if (this.data.multiLine) { lineCount = value.split(/\r\n|\r|\n/).length; @@ -1667,8 +1781,12 @@ class WidgetAnnotation extends Annotation { const defaultPadding = 2; const hPadding = defaultPadding; - const totalHeight = this.data.rect[3] - this.data.rect[1]; - const totalWidth = this.data.rect[2] - this.data.rect[0]; + let totalHeight = this.data.rect[3] - this.data.rect[1]; + let totalWidth = this.data.rect[2] - this.data.rect[0]; + + if (rotation === 90 || rotation === 270) { + [totalWidth, totalHeight] = [totalHeight, totalWidth]; + } if (!this._defaultAppearance) { // The DA is required and must be a string. @@ -1719,7 +1837,8 @@ class WidgetAnnotation extends Annotation { totalHeight, alignment, hPadding, - vPadding + vPadding, + annotationStorage ); } @@ -1733,12 +1852,13 @@ class WidgetAnnotation extends Annotation { encodedString, totalWidth, hPadding, - vPadding + vPadding, + annotationStorage ); } // Empty or it has a trailing whitespace. - const colors = this.getBorderAndBackgroundAppearances(); + const colors = this.getBorderAndBackgroundAppearances(annotationStorage); if (alignment === 0 || alignment > 2) { // Left alignment: nothing to do @@ -1989,7 +2109,15 @@ class TextWidgetAnnotation extends WidgetAnnotation { this.data.doNotScroll = this.hasFieldFlag(AnnotationFieldFlag.DONOTSCROLL); } - _getCombAppearance(defaultAppearance, font, text, width, hPadding, vPadding) { + _getCombAppearance( + defaultAppearance, + font, + text, + width, + hPadding, + vPadding, + annotationStorage + ) { const combWidth = numberToString(width / this.data.maxLen); const buf = []; const positions = font.getCharPositions(text); @@ -1998,7 +2126,7 @@ class TextWidgetAnnotation extends WidgetAnnotation { } // Empty or it has a trailing whitespace. - const colors = this.getBorderAndBackgroundAppearances(); + const colors = this.getBorderAndBackgroundAppearances(annotationStorage); const renderedComb = buf.join(` ${combWidth} 0 Td `); return ( `/Tx BMC q ${colors}BT ` + @@ -2017,7 +2145,8 @@ class TextWidgetAnnotation extends WidgetAnnotation { height, alignment, hPadding, - vPadding + vPadding, + annotationStorage ) { const lines = text.split(/\r\n?|\n/); const buf = []; @@ -2043,7 +2172,7 @@ class TextWidgetAnnotation extends WidgetAnnotation { const renderedText = buf.join("\n"); // Empty or it has a trailing whitespace. - const colors = this.getBorderAndBackgroundAppearances(); + const colors = this.getBorderAndBackgroundAppearances(annotationStorage); return ( `/Tx BMC q ${colors}BT ` + @@ -2137,6 +2266,7 @@ class TextWidgetAnnotation extends WidgetAnnotation { page: this.data.pageIndex, strokeColor: this.data.borderColor, fillColor: this.data.backgroundColor, + rotation: this.rotation, type: "text", }; } @@ -2163,7 +2293,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { } else if (this.data.radioButton) { this._processRadioButton(params); } else if (this.data.pushButton) { - this._hasOwnCanvas = true; + this.data.hasOwnCanvas = true; this._processPushButton(params); } else { warn("Invalid field flags for button widget annotation"); @@ -2188,24 +2318,26 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { } let value = null; + let rotation = null; if (annotationStorage) { const storageEntry = annotationStorage.get(this.data.id); value = storageEntry ? storageEntry.value : null; + rotation = storageEntry ? storageEntry.rotation : null; } - if (value === null) { + if (value === null && this.appearance) { // Nothing in the annotationStorage. - if (this.appearance) { - // But we've a default appearance so use it. - return super.getOperatorList( - evaluator, - task, - intent, - renderForms, - annotationStorage - ); - } + // But we've a default appearance so use it. + return super.getOperatorList( + evaluator, + task, + intent, + renderForms, + annotationStorage + ); + } + if (value === null || value === undefined) { // There is no default appearance so use the one derived // from the field value. if (this.data.checkBox) { @@ -2220,6 +2352,15 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { : this.uncheckedAppearance; if (appearance) { const savedAppearance = this.appearance; + const savedMatrix = appearance.dict.getArray("Matrix") || IDENTITY_MATRIX; + + if (rotation) { + appearance.dict.set( + "Matrix", + this.getRotationMatrix(annotationStorage) + ); + } + this.appearance = appearance; const operatorList = super.getOperatorList( evaluator, @@ -2229,6 +2370,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { annotationStorage ); this.appearance = savedAppearance; + appearance.dict.set("Matrix", savedMatrix); return operatorList; } @@ -2254,14 +2396,18 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { return null; } const storageEntry = annotationStorage.get(this.data.id); - const value = storageEntry && storageEntry.value; - if (value === undefined) { - return null; - } + let rotation = storageEntry && storageEntry.rotation; + let value = storageEntry && storageEntry.value; - const defaultValue = this.data.fieldValue === this.data.exportValue; - if (defaultValue === value) { - return null; + if (rotation === undefined) { + if (value === undefined) { + return null; + } + + const defaultValue = this.data.fieldValue === this.data.exportValue; + if (defaultValue === value) { + return null; + } } const dict = evaluator.xref.fetchIfRef(this.ref); @@ -2269,6 +2415,13 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { return null; } + if (rotation === undefined) { + rotation = this.rotation; + } + if (value === undefined) { + value = this.data.fieldValue === this.data.exportValue; + } + const xfa = { path: stringToPDFString(dict.get("T") || ""), value: value ? this.data.exportValue : "", @@ -2279,6 +2432,11 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { dict.set("AS", name); dict.set("M", `D:${getModificationDate()}`); + const maybeMK = this._getMKDict(rotation); + if (maybeMK) { + dict.set("MK", maybeMK); + } + const encrypt = evaluator.xref.encrypt; let originalTransform = null; if (encrypt) { @@ -2300,14 +2458,18 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { return null; } const storageEntry = annotationStorage.get(this.data.id); - const value = storageEntry && storageEntry.value; - if (value === undefined) { - return null; - } + let rotation = storageEntry && storageEntry.rotation; + let value = storageEntry && storageEntry.value; - const defaultValue = this.data.fieldValue === this.data.buttonValue; - if (defaultValue === value) { - return null; + if (rotation === undefined) { + if (value === undefined) { + return null; + } + + const defaultValue = this.data.fieldValue === this.data.buttonValue; + if (defaultValue === value) { + return null; + } } const dict = evaluator.xref.fetchIfRef(this.ref); @@ -2315,6 +2477,14 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { return null; } + if (value === undefined) { + value = this.data.fieldValue === this.data.buttonValue; + } + + if (rotation === undefined) { + rotation = this.rotation; + } + const xfa = { path: stringToPDFString(dict.get("T") || ""), value: value ? this.data.buttonValue : "", @@ -2346,6 +2516,11 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { dict.set("AS", name); dict.set("M", `D:${getModificationDate()}`); + const maybeMK = this._getMKDict(rotation); + if (maybeMK) { + dict.set("MK", maybeMK); + } + let originalTransform = null; if (encrypt) { originalTransform = encrypt.createCipherTransform( @@ -2579,6 +2754,7 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { page: this.data.pageIndex, strokeColor: this.data.borderColor, fillColor: this.data.backgroundColor, + rotation: this.rotation, type, }; } @@ -2662,6 +2838,7 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation { page: this.data.pageIndex, strokeColor: this.data.borderColor, fillColor: this.data.backgroundColor, + rotation: this.rotation, type, }; } @@ -2674,21 +2851,34 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation { if (!annotationStorage) { return null; } + const storageEntry = annotationStorage.get(this.data.id); - let exportedValue = storageEntry && storageEntry.value; - if (exportedValue === undefined) { + if (!storageEntry) { + return null; + } + + const rotation = storageEntry.rotation; + let exportedValue = storageEntry.value; + if (rotation === undefined && exportedValue === undefined) { // The annotation hasn't been rendered so use the appearance return null; } - if (!Array.isArray(exportedValue)) { + if (exportedValue === undefined) { + exportedValue = this.data.fieldValue; + } else if (!Array.isArray(exportedValue)) { exportedValue = [exportedValue]; } const defaultPadding = 2; const hPadding = defaultPadding; - const totalHeight = this.data.rect[3] - this.data.rect[1]; - const totalWidth = this.data.rect[2] - this.data.rect[0]; + let totalHeight = this.data.rect[3] - this.data.rect[1]; + let totalWidth = this.data.rect[2] - this.data.rect[0]; + + if (rotation === 90 || rotation === 270) { + [totalWidth, totalHeight] = [totalHeight, totalWidth]; + } + const lineCount = this.data.options.length; const valueIndices = []; for (let i = 0; i < lineCount; i++) { diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 49d062206..a4dc26e71 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -264,12 +264,39 @@ class AnnotationElement { container.style.left = `${(100 * (rect[0] - pageLLx)) / pageWidth}%`; container.style.top = `${(100 * (rect[1] - pageLLy)) / pageHeight}%`; - container.style.width = `${(100 * width) / pageWidth}%`; - container.style.height = `${(100 * height) / pageHeight}%`; + + const { rotation } = data; + if (data.hasOwnCanvas || rotation === 0) { + container.style.width = `${(100 * width) / pageWidth}%`; + container.style.height = `${(100 * height) / pageHeight}%`; + } else { + this.setRotation(rotation, container); + } return container; } + setRotation(angle, container = this.container) { + const [pageLLx, pageLLy, pageURx, pageURy] = this.viewport.viewBox; + const pageWidth = pageURx - pageLLx; + const pageHeight = pageURy - pageLLy; + const { width, height } = getRectDims(this.data.rect); + + let elementWidth, elementHeight; + if (angle % 180 === 0) { + elementWidth = (100 * width) / pageWidth; + elementHeight = (100 * height) / pageHeight; + } else { + elementWidth = (100 * height) / pageWidth; + elementHeight = (100 * width) / pageHeight; + } + + container.style.width = `${elementWidth}%`; + container.style.height = `${elementHeight}%`; + + container.setAttribute("data-annotation-rotation", (360 - angle) % 360); + } + get _commonActions() { const setColor = (jsName, styleName, event) => { const color = event.detail[jsName]; @@ -335,6 +362,13 @@ class AnnotationElement { strokeColor: event => { setColor("strokeColor", "borderColor", event); }, + rotation: event => { + const angle = event.detail.rotation; + this.setRotation(angle); + this.annotationStorage.setValue(this.data.id, { + rotation: angle, + }); + }, }); } diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index 3c9e957cd..cf7b99044 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -57,7 +57,6 @@ class Field extends PDFObject { this.required = data.required; this.richText = data.richText; this.richValue = data.richValue; - this.rotation = data.rotation; this.style = data.style; this.submitName = data.submitName; this.textFont = data.textFont; @@ -84,6 +83,7 @@ class Field extends PDFObject { this._kidIds = data.kidIds || null; this._fieldType = getFieldType(this._actions); this._siblings = data.siblings || null; + this._rotation = data.rotation || 0; this._globalEval = data.globalEval; this._appObjects = data.appObjects; @@ -188,6 +188,22 @@ class Field extends PDFObject { throw new Error("field.page is read-only"); } + get rotation() { + return this._rotation; + } + + set rotation(angle) { + angle = Math.floor(angle); + if (angle % 90 !== 0) { + throw new Error("Invalid rotation: must be a multiple of 90"); + } + angle %= 360; + if (angle < 0) { + angle += 360; + } + this._rotation = angle; + } + get textColor() { return this._textColor; } diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index 0c1107e8d..5b36303aa 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -1401,4 +1401,47 @@ describe("Interaction", () => { ); }); }); + + describe("in bug1675139.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("bug1675139.pdf", getSelector("48R")); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that data-annotation-rotation is correc", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + let base = 0; + + while (base !== 360) { + for (const [ref, angle] of [ + [47, 0], + [42, 90], + [45, 180], + [46, 270], + ]) { + const rotation = await page.$eval( + `[data-annotation-id='${ref}R']`, + el => parseInt(el.getAttribute("data-annotation-rotation") || 0) + ); + expect(rotation) + .withContext(`In ${browserName}`) + .toEqual((360 + ((360 - (base + angle)) % 360)) % 360); + } + base += 90; + await page.click(getSelector("48R")); + } + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 854e1b359..70817f3a2 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -528,3 +528,4 @@ !bug1771477.pdf !bug1724918.pdf !issue15053.pdf +!bug1675139.pdf diff --git a/test/pdfs/bug1675139.pdf b/test/pdfs/bug1675139.pdf new file mode 100755 index 0000000000000000000000000000000000000000..77cb6f2f333b2008202aaf18ecfbdcf08bdccc69 GIT binary patch literal 14835 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 O#jgE9mmWth{{0J!Ehs7g literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index 34fc5b584..e5c120813 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -6583,5 +6583,44 @@ "rounds": 1, "type": "eq", "annotations": true + }, + { "id": "bug1675139", + "file": "pdfs/bug1675139.pdf", + "md5": "052c2c3dcc7ef4d4ac622282cb0fb17a", + "rounds": 1, + "type": "eq", + "annotations": true + }, + { "id": "bug1675139-print", + "file": "pdfs/bug1675139.pdf", + "md5": "052c2c3dcc7ef4d4ac622282cb0fb17a", + "rounds": 1, + "type": "eq", + "print": true, + "annotationStorage": { + "42R": { + "value": "pi/2" + }, + "46R": { + "value": "3*pi/2", + "rotation": 180 + }, + "47R": { + "value": "0*pi/2" + }, + "45R": { + "value": "pi" + }, + "55R": { + "value": "C", + "rotation": 90 + }, + "52R": { + "value": "Yes" + }, + "56R": { + "rotation": 270 + } + } } ] diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index e4d96e15f..50f2c2eaa 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -2058,6 +2058,52 @@ describe("annotation", function () { ); }); + it("should save rotated text", async function () { + const textWidgetRef = Ref.get(123, 0); + const xref = new XRefMock([ + { ref: textWidgetRef, data: textWidgetDict }, + helvRefObj, + ]); + partialEvaluator.xref = xref; + const task = new WorkerTask("test save"); + + const annotation = await AnnotationFactory.create( + xref, + textWidgetRef, + pdfManagerMock, + idFactoryMock + ); + const annotationStorage = new Map(); + annotationStorage.set(annotation.data.id, { + value: "hello world", + rotation: 90, + }); + + const data = await annotation.save( + partialEvaluator, + task, + annotationStorage + ); + expect(data.length).toEqual(2); + const [oldData, newData] = data; + expect(oldData.ref).toEqual(Ref.get(123, 0)); + expect(newData.ref).toEqual(Ref.get(2, 0)); + + oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)"); + expect(oldData.data).toEqual( + "123 0 obj\n" + + "<< /Type /Annot /Subtype /Widget /FT /Tx /DA (/Helv 5 Tf) /DR " + + "<< /Font << /Helv 314 0 R>>>> /Rect [0 0 32 10] " + + "/V (hello world) /AP << /N 2 0 R>> /M (date) /MK << /R 90>>>>\nendobj\n" + ); + expect(newData.data).toEqual( + "2 0 obj\n<< /Length 74 /Subtype /Form /Resources " + + "<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10] /Matrix [0 1 -1 0 32 0]>> stream\n" + + "/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.04 Td (hello world) Tj " + + "ET Q EMC\nendstream\nendobj\n" + ); + }); + it("should get field object for usage in JS sandbox", async function () { const textWidgetRef = Ref.get(123, 0); const xDictRef = Ref.get(141, 0); @@ -2612,6 +2658,57 @@ describe("annotation", function () { expect(data).toEqual(null); }); + it("should save rotated checkboxes", async function () { + const appearanceStatesDict = new Dict(); + const normalAppearanceDict = new Dict(); + + normalAppearanceDict.set("Checked", Ref.get(314, 0)); + normalAppearanceDict.set("Off", Ref.get(271, 0)); + appearanceStatesDict.set("N", normalAppearanceDict); + + buttonWidgetDict.set("AP", appearanceStatesDict); + buttonWidgetDict.set("V", Name.get("Off")); + + const buttonWidgetRef = Ref.get(123, 0); + const xref = new XRefMock([ + { ref: buttonWidgetRef, data: buttonWidgetDict }, + ]); + partialEvaluator.xref = xref; + const task = new WorkerTask("test save"); + + const annotation = await AnnotationFactory.create( + xref, + buttonWidgetRef, + pdfManagerMock, + idFactoryMock + ); + const annotationStorage = new Map(); + annotationStorage.set(annotation.data.id, { value: true, rotation: 180 }); + + const [oldData] = await annotation.save( + partialEvaluator, + task, + annotationStorage + ); + oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)"); + expect(oldData.ref).toEqual(Ref.get(123, 0)); + expect(oldData.data).toEqual( + "123 0 obj\n" + + "<< /Type /Annot /Subtype /Widget /FT /Btn " + + "/AP << /N << /Checked 314 0 R /Off 271 0 R>>>> " + + "/V /Checked /AS /Checked /M (date) /MK << /R 180>>>>\nendobj\n" + ); + + annotationStorage.set(annotation.data.id, { value: false }); + + const data = await annotation.save( + partialEvaluator, + task, + annotationStorage + ); + expect(data).toEqual(null); + }); + it("should handle radio buttons with a field value", async function () { const parentDict = new Dict(); parentDict.set("V", Name.get("1")); @@ -3485,6 +3582,67 @@ describe("annotation", function () { ); }); + it("should save rotated choice", async function () { + choiceWidgetDict.set("Opt", ["A", "B", "C"]); + choiceWidgetDict.set("V", "A"); + + const choiceWidgetRef = Ref.get(123, 0); + const xref = new XRefMock([ + { ref: choiceWidgetRef, data: choiceWidgetDict }, + fontRefObj, + ]); + partialEvaluator.xref = xref; + const task = new WorkerTask("test save"); + + const annotation = await AnnotationFactory.create( + xref, + choiceWidgetRef, + pdfManagerMock, + idFactoryMock + ); + const annotationStorage = new Map(); + annotationStorage.set(annotation.data.id, { value: "C", rotation: 270 }); + + const data = await annotation.save( + partialEvaluator, + task, + annotationStorage + ); + expect(data.length).toEqual(2); + const [oldData, newData] = data; + expect(oldData.ref).toEqual(Ref.get(123, 0)); + expect(newData.ref).toEqual(Ref.get(2, 0)); + + oldData.data = oldData.data.replace(/\(D:\d+\)/, "(date)"); + expect(oldData.data).toEqual( + "123 0 obj\n" + + "<< /Type /Annot /Subtype /Widget /FT /Ch /DA (/Helv 5 Tf) /DR " + + "<< /Font << /Helv 314 0 R>>>> " + + "/Rect [0 0 32 10] /Opt [(A) (B) (C)] /V (C) " + + "/AP << /N 2 0 R>> /M (date) /MK << /R 270>>>>\nendobj\n" + ); + expect(newData.data).toEqual( + [ + "2 0 obj", + "<< /Length 170 /Subtype /Form /Resources << /Font << /Helv 314 0 R>>>> " + + "/BBox [0 0 32 10] /Matrix [0 -1 1 0 0 10]>> stream", + "/Tx BMC q", + "1 1 10 32 re W n", + "0.600006 0.756866 0.854904 rg", + "1 11.75 10 6.75 re f", + "BT", + "/Helv 5 Tf", + "1 0 0 1 0 32 Tm", + "2 -5.88 Td (A) Tj", + "0 -6.75 Td (B) Tj", + "0 -6.75 Td (C) Tj", + "ET Q EMC", + "endstream", + "endobj\n", + ].join("\n") + ); + }); + it("should save choice", async function () { choiceWidgetDict.set("Opt", ["A", "B", "C"]); choiceWidgetDict.set("V", "A"); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 4ecbad29b..1523e83ef 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -1333,6 +1333,7 @@ describe("api", function () { page: 0, strokeColor: null, fillColor: null, + rotation: 0, type: "text", }, ], @@ -1354,6 +1355,7 @@ describe("api", function () { page: 0, strokeColor: null, fillColor: new Uint8ClampedArray([192, 192, 192]), + rotation: 0, type: "button", }, ], diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index bb8cb6bde..96e11c4cd 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -51,6 +51,7 @@ text-align: initial; pointer-events: auto; box-sizing: border-box; + transform-origin: 0 0; } .annotationLayer .linkAnnotation > a,