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, 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 }) {

View File

@ -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 = [];

View File

@ -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) {

View File

@ -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,

View File

@ -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");
})
);
});
});
}); });

View File

@ -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

Binary file not shown.

BIN
test/pdfs/bug1871353.pdf Normal file

Binary file not shown.