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,18 +1219,24 @@ 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"), | ||||||
|  |         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); |     const transform = getTransformMatrix(rect, bbox, matrix); | ||||||
|     transform[4] -= rect[0]; |     transform[4] -= rect[0]; | ||||||
|     transform[5] -= rect[1]; |     transform[5] -= rect[1]; | ||||||
|       firstPosition = Util.applyTransform(firstPosition, transform); |     coords = Util.applyTransform(coords, transform); | ||||||
|       firstPosition = Util.applyTransform(firstPosition, matrix); |     return Util.applyTransform(coords, matrix); | ||||||
| 
 |  | ||||||
|       this.data.textPosition = firstPosition; |  | ||||||
|       this.data.textContent = text; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
| @ -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,11 +3784,21 @@ 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; | ||||||
|  |       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.appearance = fakeUnicodeFont.createAppearance( | ||||||
|           this._contents.str, |           this._contents.str, | ||||||
|           this.rectangle, |           this.rectangle, | ||||||
| @ -3795,9 +3814,10 @@ class FreeTextAnnotation extends MarkupAnnotation { | |||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   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