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.
This commit is contained in:
parent
54777b42c2
commit
cdc58b7a52
@ -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++) {
|
||||
|
@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -528,3 +528,4 @@
|
||||
!bug1771477.pdf
|
||||
!bug1724918.pdf
|
||||
!issue15053.pdf
|
||||
!bug1675139.pdf
|
||||
|
BIN
test/pdfs/bug1675139.pdf
Executable file
BIN
test/pdfs/bug1675139.pdf
Executable file
Binary file not shown.
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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");
|
||||
|
@ -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",
|
||||
},
|
||||
],
|
||||
|
@ -51,6 +51,7 @@
|
||||
text-align: initial;
|
||||
pointer-events: auto;
|
||||
box-sizing: border-box;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.annotationLayer .linkAnnotation > a,
|
||||
|
Loading…
Reference in New Issue
Block a user