Preserve the whitespaces when getting text from FreeText annotations (bug 1871353)
When the text of an annotation is extracted in using getTextContent, consecutive white spaces are just replaced by one space and. So this patch add an option to make sure that white spaces are preserved when appearance is parsed. For the case where there's no appearance, we can have a fast path to get the correct string from the Content entry. When an existing FreeText is edited, space (0x20) are replaced by non-breakable (0xa0) ones to make to see all of them on screen.
This commit is contained in:
parent
1019b9f821
commit
7839e7b495
@ -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…
Reference in New Issue
Block a user