Merge pull request #17458 from calixteman/bug1871353

Preserve the whitespaces when getting text from FreeText annotations (bug 1871353)
This commit is contained in:
calixteman 2024-01-05 14:21:27 +01:00 committed by GitHub
commit 130a0fef3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 152 additions and 36 deletions

View File

@ -1207,6 +1207,7 @@ class Annotation {
task,
resources,
includeMarkedContent: true,
keepWhiteSpace: true,
sink,
viewBox,
});
@ -1218,20 +1219,26 @@ class Annotation {
if (text.length > 1 || text[0]) {
const appearanceDict = this.appearance.dict;
const bbox = appearanceDict.getArray("BBox") || [0, 0, 1, 1];
const matrix = appearanceDict.getArray("Matrix") || [1, 0, 0, 1, 0, 0];
const rect = this.data.rect;
const transform = getTransformMatrix(rect, bbox, 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.textPosition = this._transformPoint(
firstPosition,
appearanceDict.getArray("BBox"),
appearanceDict.getArray("Matrix")
);
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.
*
@ -3767,7 +3774,9 @@ class FreeTextAnnotation extends MarkupAnnotation {
const { evaluatorOptions, xref } = params;
this.data.annotationType = AnnotationType.FREETEXT;
this.setDefaultAppearance(params);
if (this.appearance) {
this._hasAppearance = !!this.appearance;
if (this._hasAppearance) {
const { fontColor, fontSize } = parseAppearanceStream(
this.appearance,
evaluatorOptions,
@ -3775,29 +3784,40 @@ class FreeTextAnnotation extends MarkupAnnotation {
);
this.data.defaultAppearanceData.fontColor = fontColor;
this.data.defaultAppearanceData.fontSize = fontSize || 10;
} else if (this._isOffscreenCanvasSupported) {
const strokeAlpha = params.dict.get("CA");
const fakeUnicodeFont = new FakeUnicodeFont(xref, "sans-serif");
} else {
this.data.defaultAppearanceData.fontSize ||= 10;
const { fontColor, fontSize } = this.data.defaultAppearanceData;
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."
);
if (this._contents.str) {
this.data.textContent = this._contents.str.split(/\r\n?|\n/);
const { coords, bbox, matrix } = FakeUnicodeFont.getFirstPositionInfo(
this.rectangle,
this.rotation,
fontSize
);
this.data.textPosition = this._transformPoint(coords, bbox, matrix);
}
if (this._isOffscreenCanvasSupported) {
const strokeAlpha = params.dict.get("CA");
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() {
return !!this.appearance;
return this._hasAppearance;
}
static createNewDict(annotation, xref, { apRef, ap }) {

View File

@ -390,6 +390,26 @@ endcmap CMapName currentdict /CMap defineresource pop end end`;
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) {
const ctx = this._createContext();
const lines = [];

View File

@ -2281,6 +2281,7 @@ class PartialEvaluator {
viewBox,
markedContentData = null,
disableNormalization = false,
keepWhiteSpace = false,
}) {
// Ensure that `resources`/`stateManager` is correctly initialized,
// even if the provided parameter is e.g. `null`.
@ -2347,11 +2348,12 @@ class PartialEvaluator {
twoLastChars[twoLastCharsPos] = char;
twoLastCharsPos = nextPos;
return ret;
return !keepWhiteSpace && ret;
}
function shouldAddWhitepsace() {
return (
!keepWhiteSpace &&
twoLastChars[twoLastCharsPos] !== " " &&
twoLastChars[(twoLastCharsPos + 1) % 2] === " "
);
@ -2836,7 +2838,7 @@ class PartialEvaluator {
}
let scaledDim = glyphWidth * scale;
if (category.isWhitespace) {
if (!keepWhiteSpace && category.isWhitespace) {
// Don't push a " " in the textContentItem
// (except when it's between two non-spaces chars),
// it will be done (if required) in next call to
@ -3272,6 +3274,7 @@ class PartialEvaluator {
viewBox,
markedContentData,
disableNormalization,
keepWhiteSpace,
})
.then(function () {
if (!sinkWrapper.enqueueInvoked) {

View File

@ -648,6 +648,14 @@ class FreeTextEditor extends AnnotationEditor {
}
}
#serializeContent() {
return this.#content.replaceAll("\xa0", " ");
}
static #deserializeContent(content) {
return content.replaceAll(" ", "\xa0");
}
/** @inheritdoc */
get contentDiv() {
return this.editorDiv;
@ -690,10 +698,9 @@ class FreeTextEditor extends AnnotationEditor {
};
}
const editor = super.deserialize(data, parent, uiManager);
editor.#fontSize = data.fontSize;
editor.#color = Util.makeHexColor(...data.color);
editor.#content = data.value;
editor.#content = FreeTextEditor.#deserializeContent(data.value);
editor.annotationElementId = data.id || null;
editor.#initialData = initialData;
@ -726,7 +733,7 @@ class FreeTextEditor extends AnnotationEditor {
annotationType: AnnotationEditorType.FREETEXT,
color,
fontSize: this.#fontSize,
value: this.#content,
value: this.#serializeContent(),
pageIndex: this.pageIndex,
rect,
rotation: this.rotation,

View File

@ -209,11 +209,11 @@ describe("FreeText Editor", () => {
await waitForStorageEntries(page, 2);
const content = await page.$eval(getEditorSelector(0), el =>
el.innerText.trimEnd()
el.innerText.trimEnd().replaceAll("\xa0", " ")
);
let pastedContent = await page.$eval(getEditorSelector(1), el =>
el.innerText.trimEnd()
el.innerText.trimEnd().replaceAll("\xa0", " ")
);
expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);
@ -225,7 +225,7 @@ describe("FreeText Editor", () => {
await waitForStorageEntries(page, 3);
pastedContent = await page.$eval(getEditorSelector(2), el =>
el.innerText.trimEnd()
el.innerText.trimEnd().replaceAll("\xa0", " ")
);
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");
})
);
});
});
});

View File

@ -621,3 +621,5 @@
!bug1863910.pdf
!bug1865341.pdf
!bug1872721.pdf
!bug1871353.pdf
!bug1871353.1.pdf

BIN
test/pdfs/bug1871353.1.pdf Normal file

Binary file not shown.

BIN
test/pdfs/bug1871353.pdf Normal file

Binary file not shown.