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,
|
||||
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 }) {
|
||||
|
@ -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 = [];
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
2
test/pdfs/.gitignore
vendored
2
test/pdfs/.gitignore
vendored
@ -621,3 +621,5 @@
|
||||
!bug1863910.pdf
|
||||
!bug1865341.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