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 000000000..77cb6f2f3 Binary files /dev/null and b/test/pdfs/bug1675139.pdf differ 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,