text_layer_builder.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. /* Copyright 2012 Mozilla Foundation
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. // eslint-disable-next-line max-len
  16. /** @typedef {import("../src/display/display_utils").PageViewport} PageViewport */
  17. /** @typedef {import("../src/display/api").TextContent} TextContent */
  18. /** @typedef {import("./text_highlighter").TextHighlighter} TextHighlighter */
  19. // eslint-disable-next-line max-len
  20. /** @typedef {import("./text_accessibility.js").TextAccessibilityManager} TextAccessibilityManager */
  21. import { renderTextLayer, updateTextLayer } from "pdfjs-lib";
  22. /**
  23. * @typedef {Object} TextLayerBuilderOptions
  24. * @property {TextHighlighter} highlighter - Optional object that will handle
  25. * highlighting text from the find controller.
  26. * @property {TextAccessibilityManager} [accessibilityManager]
  27. * @property {boolean} [isOffscreenCanvasSupported] - Allows to use an
  28. * OffscreenCanvas if needed.
  29. */
  30. /**
  31. * The text layer builder provides text selection functionality for the PDF.
  32. * It does this by creating overlay divs over the PDF's text. These divs
  33. * contain text that matches the PDF text they are overlaying.
  34. */
  35. class TextLayerBuilder {
  36. #rotation = 0;
  37. #scale = 0;
  38. #textContentSource = null;
  39. constructor({
  40. highlighter = null,
  41. accessibilityManager = null,
  42. isOffscreenCanvasSupported = true,
  43. }) {
  44. this.textContentItemsStr = [];
  45. this.renderingDone = false;
  46. this.textDivs = [];
  47. this.textDivProperties = new WeakMap();
  48. this.textLayerRenderTask = null;
  49. this.highlighter = highlighter;
  50. this.accessibilityManager = accessibilityManager;
  51. this.isOffscreenCanvasSupported = isOffscreenCanvasSupported;
  52. this.div = document.createElement("div");
  53. this.div.className = "textLayer";
  54. this.hide();
  55. }
  56. #finishRendering() {
  57. this.renderingDone = true;
  58. const endOfContent = document.createElement("div");
  59. endOfContent.className = "endOfContent";
  60. this.div.append(endOfContent);
  61. this.#bindMouse();
  62. }
  63. get numTextDivs() {
  64. return this.textDivs.length;
  65. }
  66. /**
  67. * Renders the text layer.
  68. * @param {PageViewport} viewport
  69. */
  70. async render(viewport) {
  71. if (!this.#textContentSource) {
  72. throw new Error('No "textContentSource" parameter specified.');
  73. }
  74. const scale = viewport.scale * (globalThis.devicePixelRatio || 1);
  75. const { rotation } = viewport;
  76. if (this.renderingDone) {
  77. const mustRotate = rotation !== this.#rotation;
  78. const mustRescale = scale !== this.#scale;
  79. if (mustRotate || mustRescale) {
  80. this.hide();
  81. updateTextLayer({
  82. container: this.div,
  83. viewport,
  84. textDivs: this.textDivs,
  85. textDivProperties: this.textDivProperties,
  86. isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
  87. mustRescale,
  88. mustRotate,
  89. });
  90. this.#scale = scale;
  91. this.#rotation = rotation;
  92. }
  93. this.show();
  94. return;
  95. }
  96. this.cancel();
  97. this.highlighter?.setTextMapping(this.textDivs, this.textContentItemsStr);
  98. this.accessibilityManager?.setTextMapping(this.textDivs);
  99. this.textLayerRenderTask = renderTextLayer({
  100. textContentSource: this.#textContentSource,
  101. container: this.div,
  102. viewport,
  103. textDivs: this.textDivs,
  104. textDivProperties: this.textDivProperties,
  105. textContentItemsStr: this.textContentItemsStr,
  106. isOffscreenCanvasSupported: this.isOffscreenCanvasSupported,
  107. });
  108. await this.textLayerRenderTask.promise;
  109. this.#finishRendering();
  110. this.#scale = scale;
  111. this.#rotation = rotation;
  112. this.show();
  113. this.accessibilityManager?.enable();
  114. }
  115. hide() {
  116. if (!this.div.hidden) {
  117. // We turn off the highlighter in order to avoid to scroll into view an
  118. // element of the text layer which could be hidden.
  119. this.highlighter?.disable();
  120. this.div.hidden = true;
  121. }
  122. }
  123. show() {
  124. if (this.div.hidden && this.renderingDone) {
  125. this.div.hidden = false;
  126. this.highlighter?.enable();
  127. }
  128. }
  129. /**
  130. * Cancel rendering of the text layer.
  131. */
  132. cancel() {
  133. if (this.textLayerRenderTask) {
  134. this.textLayerRenderTask.cancel();
  135. this.textLayerRenderTask = null;
  136. }
  137. this.highlighter?.disable();
  138. this.accessibilityManager?.disable();
  139. this.textContentItemsStr.length = 0;
  140. this.textDivs.length = 0;
  141. this.textDivProperties = new WeakMap();
  142. }
  143. /**
  144. * @param {ReadableStream | TextContent} source
  145. */
  146. setTextContentSource(source) {
  147. this.cancel();
  148. this.#textContentSource = source;
  149. }
  150. /**
  151. * Improves text selection by adding an additional div where the mouse was
  152. * clicked. This reduces flickering of the content if the mouse is slowly
  153. * dragged up or down.
  154. */
  155. #bindMouse() {
  156. const { div } = this;
  157. div.addEventListener("mousedown", evt => {
  158. const end = div.querySelector(".endOfContent");
  159. if (!end) {
  160. return;
  161. }
  162. if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
  163. // On non-Firefox browsers, the selection will feel better if the height
  164. // of the `endOfContent` div is adjusted to start at mouse click
  165. // location. This avoids flickering when the selection moves up.
  166. // However it does not work when selection is started on empty space.
  167. let adjustTop = evt.target !== div;
  168. if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
  169. adjustTop &&=
  170. getComputedStyle(end).getPropertyValue("-moz-user-select") !==
  171. "none";
  172. }
  173. if (adjustTop) {
  174. const divBounds = div.getBoundingClientRect();
  175. const r = Math.max(0, (evt.pageY - divBounds.top) / divBounds.height);
  176. end.style.top = (r * 100).toFixed(2) + "%";
  177. }
  178. }
  179. end.classList.add("active");
  180. });
  181. div.addEventListener("mouseup", () => {
  182. const end = div.querySelector(".endOfContent");
  183. if (!end) {
  184. return;
  185. }
  186. if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
  187. end.style.top = "";
  188. }
  189. end.classList.remove("active");
  190. });
  191. }
  192. }
  193. export { TextLayerBuilder };