PageRenderTime 145ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/backend/web/filemanager/js/ViewerJS/text_layer_builder.js

https://gitlab.com/angrydevops/workerpro
JavaScript | 389 lines | 284 code | 44 blank | 61 comment | 57 complexity | 1811c4b50489cf90a09896f6aeff91fc MD5 | raw file
  1. /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  2. /* Copyright 2012 Mozilla Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. /* globals CustomStyle, scrollIntoView, PDFJS */
  17. 'use strict';
  18. var FIND_SCROLL_OFFSET_TOP = -50;
  19. var FIND_SCROLL_OFFSET_LEFT = -400;
  20. var MAX_TEXT_DIVS_TO_RENDER = 100000;
  21. var RENDER_DELAY = 200; // ms
  22. var NonWhitespaceRegexp = /\S/;
  23. function isAllWhitespace(str) {
  24. return !NonWhitespaceRegexp.test(str);
  25. }
  26. /**
  27. * @typedef {Object} TextLayerBuilderOptions
  28. * @property {HTMLDivElement} textLayerDiv - The text layer container.
  29. * @property {number} pageIndex - The page index.
  30. * @property {PageViewport} viewport - The viewport of the text layer.
  31. * @property {ILastScrollSource} lastScrollSource - The object that records when
  32. * last time scroll happened.
  33. * @property {boolean} isViewerInPresentationMode
  34. * @property {PDFFindController} findController
  35. */
  36. /**
  37. * TextLayerBuilder provides text-selection functionality for the PDF.
  38. * It does this by creating overlay divs over the PDF text. These divs
  39. * contain text that matches the PDF text they are overlaying. This object
  40. * also provides a way to highlight text that is being searched for.
  41. * @class
  42. */
  43. var TextLayerBuilder = (function TextLayerBuilderClosure() {
  44. function TextLayerBuilder(options) {
  45. this.textLayerDiv = options.textLayerDiv;
  46. this.layoutDone = false;
  47. this.divContentDone = false;
  48. this.pageIdx = options.pageIndex;
  49. this.matches = [];
  50. this.lastScrollSource = options.lastScrollSource || null;
  51. this.viewport = options.viewport;
  52. this.isViewerInPresentationMode = options.isViewerInPresentationMode;
  53. this.textDivs = [];
  54. this.findController = options.findController || null;
  55. }
  56. TextLayerBuilder.prototype = {
  57. renderLayer: function TextLayerBuilder_renderLayer() {
  58. var textLayerFrag = document.createDocumentFragment();
  59. var textDivs = this.textDivs;
  60. var textDivsLength = textDivs.length;
  61. var canvas = document.createElement('canvas');
  62. var ctx = canvas.getContext('2d');
  63. // No point in rendering many divs as it would make the browser
  64. // unusable even after the divs are rendered.
  65. if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) {
  66. return;
  67. }
  68. var lastFontSize;
  69. var lastFontFamily;
  70. for (var i = 0; i < textDivsLength; i++) {
  71. var textDiv = textDivs[i];
  72. if (textDiv.dataset.isWhitespace !== undefined) {
  73. continue;
  74. }
  75. var fontSize = textDiv.style.fontSize;
  76. var fontFamily = textDiv.style.fontFamily;
  77. // Only build font string and set to context if different from last.
  78. if (fontSize !== lastFontSize || fontFamily !== lastFontFamily) {
  79. ctx.font = fontSize + ' ' + fontFamily;
  80. lastFontSize = fontSize;
  81. lastFontFamily = fontFamily;
  82. }
  83. var width = ctx.measureText(textDiv.textContent).width;
  84. if (width > 0) {
  85. textLayerFrag.appendChild(textDiv);
  86. var transform;
  87. if (textDiv.dataset.canvasWidth !== undefined) {
  88. // Dataset values come of type string.
  89. var textScale = textDiv.dataset.canvasWidth / width;
  90. transform = 'scaleX(' + textScale + ')';
  91. } else {
  92. transform = '';
  93. }
  94. var rotation = textDiv.dataset.angle;
  95. if (rotation) {
  96. transform = 'rotate(' + rotation + 'deg) ' + transform;
  97. }
  98. if (transform) {
  99. CustomStyle.setProp('transform' , textDiv, transform);
  100. }
  101. }
  102. }
  103. this.textLayerDiv.appendChild(textLayerFrag);
  104. this.renderingDone = true;
  105. this.updateMatches();
  106. },
  107. setupRenderLayoutTimer:
  108. function TextLayerBuilder_setupRenderLayoutTimer() {
  109. // Schedule renderLayout() if the user has been scrolling,
  110. // otherwise run it right away.
  111. var self = this;
  112. var lastScroll = (this.lastScrollSource === null ?
  113. 0 : this.lastScrollSource.lastScroll);
  114. if (Date.now() - lastScroll > RENDER_DELAY) { // Render right away
  115. this.renderLayer();
  116. } else { // Schedule
  117. if (this.renderTimer) {
  118. clearTimeout(this.renderTimer);
  119. }
  120. this.renderTimer = setTimeout(function() {
  121. self.setupRenderLayoutTimer();
  122. }, RENDER_DELAY);
  123. }
  124. },
  125. appendText: function TextLayerBuilder_appendText(geom, styles) {
  126. var style = styles[geom.fontName];
  127. var textDiv = document.createElement('div');
  128. this.textDivs.push(textDiv);
  129. if (isAllWhitespace(geom.str)) {
  130. textDiv.dataset.isWhitespace = true;
  131. return;
  132. }
  133. var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform);
  134. var angle = Math.atan2(tx[1], tx[0]);
  135. if (style.vertical) {
  136. angle += Math.PI / 2;
  137. }
  138. var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3]));
  139. var fontAscent = fontHeight;
  140. if (style.ascent) {
  141. fontAscent = style.ascent * fontAscent;
  142. } else if (style.descent) {
  143. fontAscent = (1 + style.descent) * fontAscent;
  144. }
  145. var left;
  146. var top;
  147. if (angle === 0) {
  148. left = tx[4];
  149. top = tx[5] - fontAscent;
  150. } else {
  151. left = tx[4] + (fontAscent * Math.sin(angle));
  152. top = tx[5] - (fontAscent * Math.cos(angle));
  153. }
  154. textDiv.style.left = left + 'px';
  155. textDiv.style.top = top + 'px';
  156. textDiv.style.fontSize = fontHeight + 'px';
  157. textDiv.style.fontFamily = style.fontFamily;
  158. textDiv.textContent = geom.str;
  159. // |fontName| is only used by the Font Inspector. This test will succeed
  160. // when e.g. the Font Inspector is off but the Stepper is on, but it's
  161. // not worth the effort to do a more accurate test.
  162. if (PDFJS.pdfBug) {
  163. textDiv.dataset.fontName = geom.fontName;
  164. }
  165. // Storing into dataset will convert number into string.
  166. if (angle !== 0) {
  167. textDiv.dataset.angle = angle * (180 / Math.PI);
  168. }
  169. // We don't bother scaling single-char text divs, because it has very
  170. // little effect on text highlighting. This makes scrolling on docs with
  171. // lots of such divs a lot faster.
  172. if (textDiv.textContent.length > 1) {
  173. if (style.vertical) {
  174. textDiv.dataset.canvasWidth = geom.height * this.viewport.scale;
  175. } else {
  176. textDiv.dataset.canvasWidth = geom.width * this.viewport.scale;
  177. }
  178. }
  179. },
  180. setTextContent: function TextLayerBuilder_setTextContent(textContent) {
  181. this.textContent = textContent;
  182. var textItems = textContent.items;
  183. for (var i = 0, len = textItems.length; i < len; i++) {
  184. this.appendText(textItems[i], textContent.styles);
  185. }
  186. this.divContentDone = true;
  187. this.setupRenderLayoutTimer();
  188. },
  189. convertMatches: function TextLayerBuilder_convertMatches(matches) {
  190. var i = 0;
  191. var iIndex = 0;
  192. var bidiTexts = this.textContent.items;
  193. var end = bidiTexts.length - 1;
  194. var queryLen = (this.findController === null ?
  195. 0 : this.findController.state.query.length);
  196. var ret = [];
  197. for (var m = 0, len = matches.length; m < len; m++) {
  198. // Calculate the start position.
  199. var matchIdx = matches[m];
  200. // Loop over the divIdxs.
  201. while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) {
  202. iIndex += bidiTexts[i].str.length;
  203. i++;
  204. }
  205. if (i === bidiTexts.length) {
  206. console.error('Could not find a matching mapping');
  207. }
  208. var match = {
  209. begin: {
  210. divIdx: i,
  211. offset: matchIdx - iIndex
  212. }
  213. };
  214. // Calculate the end position.
  215. matchIdx += queryLen;
  216. // Somewhat the same array as above, but use > instead of >= to get
  217. // the end position right.
  218. while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) {
  219. iIndex += bidiTexts[i].str.length;
  220. i++;
  221. }
  222. match.end = {
  223. divIdx: i,
  224. offset: matchIdx - iIndex
  225. };
  226. ret.push(match);
  227. }
  228. return ret;
  229. },
  230. renderMatches: function TextLayerBuilder_renderMatches(matches) {
  231. // Early exit if there is nothing to render.
  232. if (matches.length === 0) {
  233. return;
  234. }
  235. var bidiTexts = this.textContent.items;
  236. var textDivs = this.textDivs;
  237. var prevEnd = null;
  238. var isSelectedPage = (this.findController === null ?
  239. false : (this.pageIdx === this.findController.selected.pageIdx));
  240. var selectedMatchIdx = (this.findController === null ?
  241. -1 : this.findController.selected.matchIdx);
  242. var highlightAll = (this.findController === null ?
  243. false : this.findController.state.highlightAll);
  244. var infinity = {
  245. divIdx: -1,
  246. offset: undefined
  247. };
  248. function beginText(begin, className) {
  249. var divIdx = begin.divIdx;
  250. textDivs[divIdx].textContent = '';
  251. appendTextToDiv(divIdx, 0, begin.offset, className);
  252. }
  253. function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
  254. var div = textDivs[divIdx];
  255. var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset);
  256. var node = document.createTextNode(content);
  257. if (className) {
  258. var span = document.createElement('span');
  259. span.className = className;
  260. span.appendChild(node);
  261. div.appendChild(span);
  262. return;
  263. }
  264. div.appendChild(node);
  265. }
  266. var i0 = selectedMatchIdx, i1 = i0 + 1;
  267. if (highlightAll) {
  268. i0 = 0;
  269. i1 = matches.length;
  270. } else if (!isSelectedPage) {
  271. // Not highlighting all and this isn't the selected page, so do nothing.
  272. return;
  273. }
  274. for (var i = i0; i < i1; i++) {
  275. var match = matches[i];
  276. var begin = match.begin;
  277. var end = match.end;
  278. var isSelected = (isSelectedPage && i === selectedMatchIdx);
  279. var highlightSuffix = (isSelected ? ' selected' : '');
  280. if (isSelected && !this.isViewerInPresentationMode) {
  281. scrollIntoView(textDivs[begin.divIdx],
  282. { top: FIND_SCROLL_OFFSET_TOP,
  283. left: FIND_SCROLL_OFFSET_LEFT });
  284. }
  285. // Match inside new div.
  286. if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
  287. // If there was a previous div, then add the text at the end.
  288. if (prevEnd !== null) {
  289. appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
  290. }
  291. // Clear the divs and set the content until the starting point.
  292. beginText(begin);
  293. } else {
  294. appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
  295. }
  296. if (begin.divIdx === end.divIdx) {
  297. appendTextToDiv(begin.divIdx, begin.offset, end.offset,
  298. 'highlight' + highlightSuffix);
  299. } else {
  300. appendTextToDiv(begin.divIdx, begin.offset, infinity.offset,
  301. 'highlight begin' + highlightSuffix);
  302. for (var n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
  303. textDivs[n0].className = 'highlight middle' + highlightSuffix;
  304. }
  305. beginText(end, 'highlight end' + highlightSuffix);
  306. }
  307. prevEnd = end;
  308. }
  309. if (prevEnd) {
  310. appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
  311. }
  312. },
  313. updateMatches: function TextLayerBuilder_updateMatches() {
  314. // Only show matches when all rendering is done.
  315. if (!this.renderingDone) {
  316. return;
  317. }
  318. // Clear all matches.
  319. var matches = this.matches;
  320. var textDivs = this.textDivs;
  321. var bidiTexts = this.textContent.items;
  322. var clearedUntilDivIdx = -1;
  323. // Clear all current matches.
  324. for (var i = 0, len = matches.length; i < len; i++) {
  325. var match = matches[i];
  326. var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
  327. for (var n = begin, end = match.end.divIdx; n <= end; n++) {
  328. var div = textDivs[n];
  329. div.textContent = bidiTexts[n].str;
  330. div.className = '';
  331. }
  332. clearedUntilDivIdx = match.end.divIdx + 1;
  333. }
  334. if (this.findController === null || !this.findController.active) {
  335. return;
  336. }
  337. // Convert the matches on the page controller into the match format
  338. // used for the textLayer.
  339. this.matches = this.convertMatches(this.findController === null ?
  340. [] : (this.findController.pageMatches[this.pageIdx] || []));
  341. this.renderMatches(this.matches);
  342. }
  343. };
  344. return TextLayerBuilder;
  345. })();