Merge pull request #17458 from calixteman/bug1871353
Preserve the whitespaces when getting text from FreeText annotations (bug 1871353)
This commit is contained in:
commit
130a0fef3d
@ -1207,6 +1207,7 @@ class Annotation {
|
|||||||
task,
|
task,
|
||||||
resources,
|
resources,
|
||||||
includeMarkedContent: true,
|
includeMarkedContent: true,
|
||||||
|
keepWhiteSpace: true,
|
||||||
sink,
|
sink,
|
||||||
viewBox,
|
viewBox,
|
||||||
});
|
});
|
||||||
@ -1218,20 +1219,26 @@ class Annotation {
|
|||||||
|
|
||||||
if (text.length > 1 || text[0]) {
|
if (text.length > 1 || text[0]) {
|
||||||
const appearanceDict = this.appearance.dict;
|
const appearanceDict = this.appearance.dict;
|
||||||
const bbox = appearanceDict.getArray("BBox") || [0, 0, 1, 1];
|
this.data.textPosition = this._transformPoint(
|
||||||
const matrix = appearanceDict.getArray("Matrix") || [1, 0, 0, 1, 0, 0];
|
firstPosition,
|
||||||
const rect = this.data.rect;
|
appearanceDict.getArray("BBox"),
|
||||||
const transform = getTransformMatrix(rect, bbox, matrix);
|
appearanceDict.getArray("Matrix")
|
||||||
transform[4] -= rect[0];
|
);
|
||||||
transform[5] -= rect[1];
|
|
||||||
firstPosition = Util.applyTransform(firstPosition, transform);
|
|
||||||
firstPosition = Util.applyTransform(firstPosition, matrix);
|
|
||||||
|
|
||||||
this.data.textPosition = firstPosition;
|
|
||||||
this.data.textContent = text;
|
this.data.textContent = text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_transformPoint(coords, bbox, matrix) {
|
||||||
|
const { rect } = this.data;
|
||||||
|
bbox ||= [0, 0, 1, 1];
|
||||||
|
matrix ||= [1, 0, 0, 1, 0, 0];
|
||||||
|
const transform = getTransformMatrix(rect, bbox, matrix);
|
||||||
|
transform[4] -= rect[0];
|
||||||
|
transform[5] -= rect[1];
|
||||||
|
coords = Util.applyTransform(coords, transform);
|
||||||
|
return Util.applyTransform(coords, matrix);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get field data for usage in JS sandbox.
|
* Get field data for usage in JS sandbox.
|
||||||
*
|
*
|
||||||
@ -3767,7 +3774,9 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
|||||||
const { evaluatorOptions, xref } = params;
|
const { evaluatorOptions, xref } = params;
|
||||||
this.data.annotationType = AnnotationType.FREETEXT;
|
this.data.annotationType = AnnotationType.FREETEXT;
|
||||||
this.setDefaultAppearance(params);
|
this.setDefaultAppearance(params);
|
||||||
if (this.appearance) {
|
this._hasAppearance = !!this.appearance;
|
||||||
|
|
||||||
|
if (this._hasAppearance) {
|
||||||
const { fontColor, fontSize } = parseAppearanceStream(
|
const { fontColor, fontSize } = parseAppearanceStream(
|
||||||
this.appearance,
|
this.appearance,
|
||||||
evaluatorOptions,
|
evaluatorOptions,
|
||||||
@ -3775,29 +3784,40 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
|||||||
);
|
);
|
||||||
this.data.defaultAppearanceData.fontColor = fontColor;
|
this.data.defaultAppearanceData.fontColor = fontColor;
|
||||||
this.data.defaultAppearanceData.fontSize = fontSize || 10;
|
this.data.defaultAppearanceData.fontSize = fontSize || 10;
|
||||||
} else if (this._isOffscreenCanvasSupported) {
|
} else {
|
||||||
const strokeAlpha = params.dict.get("CA");
|
|
||||||
const fakeUnicodeFont = new FakeUnicodeFont(xref, "sans-serif");
|
|
||||||
this.data.defaultAppearanceData.fontSize ||= 10;
|
this.data.defaultAppearanceData.fontSize ||= 10;
|
||||||
const { fontColor, fontSize } = this.data.defaultAppearanceData;
|
const { fontColor, fontSize } = this.data.defaultAppearanceData;
|
||||||
this.appearance = fakeUnicodeFont.createAppearance(
|
if (this._contents.str) {
|
||||||
this._contents.str,
|
this.data.textContent = this._contents.str.split(/\r\n?|\n/);
|
||||||
this.rectangle,
|
const { coords, bbox, matrix } = FakeUnicodeFont.getFirstPositionInfo(
|
||||||
this.rotation,
|
this.rectangle,
|
||||||
fontSize,
|
this.rotation,
|
||||||
fontColor,
|
fontSize
|
||||||
strokeAlpha
|
);
|
||||||
);
|
this.data.textPosition = this._transformPoint(coords, bbox, matrix);
|
||||||
this._streams.push(this.appearance, FakeUnicodeFont.toUnicodeStream);
|
}
|
||||||
} else {
|
if (this._isOffscreenCanvasSupported) {
|
||||||
warn(
|
const strokeAlpha = params.dict.get("CA");
|
||||||
"FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly."
|
const fakeUnicodeFont = new FakeUnicodeFont(xref, "sans-serif");
|
||||||
);
|
this.appearance = fakeUnicodeFont.createAppearance(
|
||||||
|
this._contents.str,
|
||||||
|
this.rectangle,
|
||||||
|
this.rotation,
|
||||||
|
fontSize,
|
||||||
|
fontColor,
|
||||||
|
strokeAlpha
|
||||||
|
);
|
||||||
|
this._streams.push(this.appearance, FakeUnicodeFont.toUnicodeStream);
|
||||||
|
} else {
|
||||||
|
warn(
|
||||||
|
"FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly."
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasTextContent() {
|
get hasTextContent() {
|
||||||
return !!this.appearance;
|
return this._hasAppearance;
|
||||||
}
|
}
|
||||||
|
|
||||||
static createNewDict(annotation, xref, { apRef, ap }) {
|
static createNewDict(annotation, xref, { apRef, ap }) {
|
||||||
|
@ -390,6 +390,26 @@ endcmap CMapName currentdict /CMap defineresource pop end end`;
|
|||||||
return this.resources;
|
return this.resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getFirstPositionInfo(rect, rotation, fontSize) {
|
||||||
|
// Get the position of the first char in the rect.
|
||||||
|
const [x1, y1, x2, y2] = rect;
|
||||||
|
let w = x2 - x1;
|
||||||
|
let h = y2 - y1;
|
||||||
|
|
||||||
|
if (rotation % 180 !== 0) {
|
||||||
|
[w, h] = [h, w];
|
||||||
|
}
|
||||||
|
const lineHeight = LINE_FACTOR * fontSize;
|
||||||
|
const lineDescent = LINE_DESCENT_FACTOR * fontSize;
|
||||||
|
|
||||||
|
return {
|
||||||
|
coords: [0, h + lineDescent - lineHeight],
|
||||||
|
bbox: [0, 0, w, h],
|
||||||
|
matrix:
|
||||||
|
rotation !== 0 ? getRotationMatrix(rotation, h, lineHeight) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
createAppearance(text, rect, rotation, fontSize, bgColor, strokeAlpha) {
|
createAppearance(text, rect, rotation, fontSize, bgColor, strokeAlpha) {
|
||||||
const ctx = this._createContext();
|
const ctx = this._createContext();
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
@ -2281,6 +2281,7 @@ class PartialEvaluator {
|
|||||||
viewBox,
|
viewBox,
|
||||||
markedContentData = null,
|
markedContentData = null,
|
||||||
disableNormalization = false,
|
disableNormalization = false,
|
||||||
|
keepWhiteSpace = false,
|
||||||
}) {
|
}) {
|
||||||
// Ensure that `resources`/`stateManager` is correctly initialized,
|
// Ensure that `resources`/`stateManager` is correctly initialized,
|
||||||
// even if the provided parameter is e.g. `null`.
|
// even if the provided parameter is e.g. `null`.
|
||||||
@ -2347,11 +2348,12 @@ class PartialEvaluator {
|
|||||||
twoLastChars[twoLastCharsPos] = char;
|
twoLastChars[twoLastCharsPos] = char;
|
||||||
twoLastCharsPos = nextPos;
|
twoLastCharsPos = nextPos;
|
||||||
|
|
||||||
return ret;
|
return !keepWhiteSpace && ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldAddWhitepsace() {
|
function shouldAddWhitepsace() {
|
||||||
return (
|
return (
|
||||||
|
!keepWhiteSpace &&
|
||||||
twoLastChars[twoLastCharsPos] !== " " &&
|
twoLastChars[twoLastCharsPos] !== " " &&
|
||||||
twoLastChars[(twoLastCharsPos + 1) % 2] === " "
|
twoLastChars[(twoLastCharsPos + 1) % 2] === " "
|
||||||
);
|
);
|
||||||
@ -2836,7 +2838,7 @@ class PartialEvaluator {
|
|||||||
}
|
}
|
||||||
let scaledDim = glyphWidth * scale;
|
let scaledDim = glyphWidth * scale;
|
||||||
|
|
||||||
if (category.isWhitespace) {
|
if (!keepWhiteSpace && category.isWhitespace) {
|
||||||
// Don't push a " " in the textContentItem
|
// Don't push a " " in the textContentItem
|
||||||
// (except when it's between two non-spaces chars),
|
// (except when it's between two non-spaces chars),
|
||||||
// it will be done (if required) in next call to
|
// it will be done (if required) in next call to
|
||||||
@ -3272,6 +3274,7 @@ class PartialEvaluator {
|
|||||||
viewBox,
|
viewBox,
|
||||||
markedContentData,
|
markedContentData,
|
||||||
disableNormalization,
|
disableNormalization,
|
||||||
|
keepWhiteSpace,
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
if (!sinkWrapper.enqueueInvoked) {
|
if (!sinkWrapper.enqueueInvoked) {
|
||||||
|
@ -648,6 +648,14 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#serializeContent() {
|
||||||
|
return this.#content.replaceAll("\xa0", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
static #deserializeContent(content) {
|
||||||
|
return content.replaceAll(" ", "\xa0");
|
||||||
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
get contentDiv() {
|
get contentDiv() {
|
||||||
return this.editorDiv;
|
return this.editorDiv;
|
||||||
@ -690,10 +698,9 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
const editor = super.deserialize(data, parent, uiManager);
|
const editor = super.deserialize(data, parent, uiManager);
|
||||||
|
|
||||||
editor.#fontSize = data.fontSize;
|
editor.#fontSize = data.fontSize;
|
||||||
editor.#color = Util.makeHexColor(...data.color);
|
editor.#color = Util.makeHexColor(...data.color);
|
||||||
editor.#content = data.value;
|
editor.#content = FreeTextEditor.#deserializeContent(data.value);
|
||||||
editor.annotationElementId = data.id || null;
|
editor.annotationElementId = data.id || null;
|
||||||
editor.#initialData = initialData;
|
editor.#initialData = initialData;
|
||||||
|
|
||||||
@ -726,7 +733,7 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
annotationType: AnnotationEditorType.FREETEXT,
|
annotationType: AnnotationEditorType.FREETEXT,
|
||||||
color,
|
color,
|
||||||
fontSize: this.#fontSize,
|
fontSize: this.#fontSize,
|
||||||
value: this.#content,
|
value: this.#serializeContent(),
|
||||||
pageIndex: this.pageIndex,
|
pageIndex: this.pageIndex,
|
||||||
rect,
|
rect,
|
||||||
rotation: this.rotation,
|
rotation: this.rotation,
|
||||||
|
@ -209,11 +209,11 @@ describe("FreeText Editor", () => {
|
|||||||
await waitForStorageEntries(page, 2);
|
await waitForStorageEntries(page, 2);
|
||||||
|
|
||||||
const content = await page.$eval(getEditorSelector(0), el =>
|
const content = await page.$eval(getEditorSelector(0), el =>
|
||||||
el.innerText.trimEnd()
|
el.innerText.trimEnd().replaceAll("\xa0", " ")
|
||||||
);
|
);
|
||||||
|
|
||||||
let pastedContent = await page.$eval(getEditorSelector(1), el =>
|
let pastedContent = await page.$eval(getEditorSelector(1), el =>
|
||||||
el.innerText.trimEnd()
|
el.innerText.trimEnd().replaceAll("\xa0", " ")
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);
|
expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);
|
||||||
@ -225,7 +225,7 @@ describe("FreeText Editor", () => {
|
|||||||
await waitForStorageEntries(page, 3);
|
await waitForStorageEntries(page, 3);
|
||||||
|
|
||||||
pastedContent = await page.$eval(getEditorSelector(2), el =>
|
pastedContent = await page.$eval(getEditorSelector(2), el =>
|
||||||
el.innerText.trimEnd()
|
el.innerText.trimEnd().replaceAll("\xa0", " ")
|
||||||
);
|
);
|
||||||
expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);
|
expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);
|
||||||
}
|
}
|
||||||
@ -3182,4 +3182,68 @@ describe("FreeText Editor", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Consecutive white spaces in Freetext without appearance", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
pages = await loadAndWait("bug1871353.pdf", ".annotationEditorLayer");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must check that consecutive white spaces are preserved when a freetext is edited", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
await switchToFreeText(page);
|
||||||
|
await page.click(getEditorSelector(0), { count: 2 });
|
||||||
|
await page.type(`${getEditorSelector(0)} .internal`, "C");
|
||||||
|
|
||||||
|
await page.click("#editorFreeText");
|
||||||
|
await page.waitForSelector(
|
||||||
|
`.annotationEditorLayer:not(.freetextEditing)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const [value] = await getSerialized(page, x => x.value);
|
||||||
|
expect(value)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual("CA B");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Consecutive white spaces in Freetext with appearance", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
pages = await loadAndWait("bug1871353.1.pdf", ".annotationEditorLayer");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must check that consecutive white spaces are preserved when a freetext is edited", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
await switchToFreeText(page);
|
||||||
|
await page.click(getEditorSelector(0), { count: 2 });
|
||||||
|
await page.type(`${getEditorSelector(0)} .internal`, "Z");
|
||||||
|
|
||||||
|
await page.click("#editorFreeText");
|
||||||
|
await page.waitForSelector(
|
||||||
|
`.annotationEditorLayer:not(.freetextEditing)`
|
||||||
|
);
|
||||||
|
|
||||||
|
const [value] = await getSerialized(page, x => x.value);
|
||||||
|
expect(value)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual("ZX Y");
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
2
test/pdfs/.gitignore
vendored
2
test/pdfs/.gitignore
vendored
@ -621,3 +621,5 @@
|
|||||||
!bug1863910.pdf
|
!bug1863910.pdf
|
||||||
!bug1865341.pdf
|
!bug1865341.pdf
|
||||||
!bug1872721.pdf
|
!bug1872721.pdf
|
||||||
|
!bug1871353.pdf
|
||||||
|
!bug1871353.1.pdf
|
||||||
|
BIN
test/pdfs/bug1871353.1.pdf
Normal file
BIN
test/pdfs/bug1871353.1.pdf
Normal file
Binary file not shown.
BIN
test/pdfs/bug1871353.pdf
Normal file
BIN
test/pdfs/bug1871353.pdf
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user