laraview/text_layer_builder.js   D
last analyzed

Complexity

Total Complexity 69
Complexity/F 4.31

Size

Lines of Code 402
Function Count 16

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 69
c 3
b 0
f 0
dl 0
loc 402
rs 4.1944
cc 0
nc 1
mnd 3
bc 62
fnc 16
bpm 3.875
cpm 4.3125
noi 2

4 Functions

Rating   Name   Duplication   Size   Complexity  
A DefaultTextLayerFactory.createTextLayerBuilder 0 7 1
A text_layer_builder.js ➔ isAllWhitespace 0 3 1
A text_layer_builder.js ➔ DefaultTextLayerFactory 0 1 1
B text_layer_builder.js ➔ TextLayerBuilderClosure 0 356 1

How to fix   Complexity   

Complexity

Complex classes like laraview/text_layer_builder.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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, PDFJS */
17
18
'use strict';
19
20
var MAX_TEXT_DIVS_TO_RENDER = 100000;
21
22
var NonWhitespaceRegexp = /\S/;
23
24
function isAllWhitespace(str) {
25
  return !NonWhitespaceRegexp.test(str);
26
}
27
28
/**
29
 * @typedef {Object} TextLayerBuilderOptions
30
 * @property {HTMLDivElement} textLayerDiv - The text layer container.
31
 * @property {number} pageIndex - The page index.
32
 * @property {PageViewport} viewport - The viewport of the text layer.
33
 * @property {PDFFindController} findController
34
 */
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.renderingDone = false;
47
    this.divContentDone = false;
48
    this.pageIdx = options.pageIndex;
49
    this.pageNumber = this.pageIdx + 1;
50
    this.matches = [];
51
    this.viewport = options.viewport;
52
    this.textDivs = [];
53
    this.findController = options.findController || null;
54
  }
55
56
  TextLayerBuilder.prototype = {
57
    _finishRendering: function TextLayerBuilder_finishRendering() {
58
      this.renderingDone = true;
59
60
      var event = document.createEvent('CustomEvent');
61
      event.initCustomEvent('textlayerrendered', true, true, {
62
        pageNumber: this.pageNumber
63
      });
64
      this.textLayerDiv.dispatchEvent(event);
65
    },
66
67
    renderLayer: function TextLayerBuilder_renderLayer() {
68
      var textLayerFrag = document.createDocumentFragment();
69
      var textDivs = this.textDivs;
70
      var textDivsLength = textDivs.length;
71
      var canvas = document.createElement('canvas');
72
      var ctx = canvas.getContext('2d');
73
74
      // No point in rendering many divs as it would make the browser
75
      // unusable even after the divs are rendered.
76
      if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) {
77
        this._finishRendering();
78
        return;
79
      }
80
81
      var lastFontSize;
82
      var lastFontFamily;
83
      for (var i = 0; i < textDivsLength; i++) {
84
        var textDiv = textDivs[i];
85
        if (textDiv.dataset.isWhitespace !== undefined) {
86
          continue;
87
        }
88
89
        var fontSize = textDiv.style.fontSize;
90
        var fontFamily = textDiv.style.fontFamily;
91
92
        // Only build font string and set to context if different from last.
93
        if (fontSize !== lastFontSize || fontFamily !== lastFontFamily) {
0 ignored issues
show
Bug introduced by
The variable lastFontSize seems to not be initialized for all possible execution paths.
Loading history...
Bug introduced by
The variable lastFontFamily seems to not be initialized for all possible execution paths.
Loading history...
94
          ctx.font = fontSize + ' ' + fontFamily;
95
          lastFontSize = fontSize;
96
          lastFontFamily = fontFamily;
97
        }
98
99
        var width = ctx.measureText(textDiv.textContent).width;
100
        if (width > 0) {
101
          textLayerFrag.appendChild(textDiv);
102
          var transform;
103
          if (textDiv.dataset.canvasWidth !== undefined) {
104
            // Dataset values come of type string.
105
            var textScale = textDiv.dataset.canvasWidth / width;
106
            transform = 'scaleX(' + textScale + ')';
107
          } else {
108
            transform = '';
109
          }
110
          var rotation = textDiv.dataset.angle;
111
          if (rotation) {
112
            transform = 'rotate(' + rotation + 'deg) ' + transform;
113
          }
114
          if (transform) {
115
            CustomStyle.setProp('transform' , textDiv, transform);
116
          }
117
        }
118
      }
119
120
      this.textLayerDiv.appendChild(textLayerFrag);
121
      this._finishRendering();
122
      this.updateMatches();
123
    },
124
125
    /**
126
     * Renders the text layer.
127
     * @param {number} timeout (optional) if specified, the rendering waits
128
     *   for specified amount of ms.
129
     */
130
    render: function TextLayerBuilder_render(timeout) {
131
      if (!this.divContentDone || this.renderingDone) {
132
        return;
133
      }
134
135
      if (this.renderTimer) {
136
        clearTimeout(this.renderTimer);
137
        this.renderTimer = null;
138
      }
139
140
      if (!timeout) { // Render right away
141
        this.renderLayer();
142
      } else { // Schedule
143
        var self = this;
144
        this.renderTimer = setTimeout(function() {
145
          self.renderLayer();
146
          self.renderTimer = null;
147
        }, timeout);
148
      }
149
    },
150
151
    appendText: function TextLayerBuilder_appendText(geom, styles) {
152
      var style = styles[geom.fontName];
153
      var textDiv = document.createElement('div');
154
      this.textDivs.push(textDiv);
155
      if (isAllWhitespace(geom.str)) {
156
        textDiv.dataset.isWhitespace = true;
157
        return;
158
      }
159
      var tx = PDFJS.Util.transform(this.viewport.transform, geom.transform);
160
      var angle = Math.atan2(tx[1], tx[0]);
161
      if (style.vertical) {
162
        angle += Math.PI / 2;
163
      }
164
      var fontHeight = Math.sqrt((tx[2] * tx[2]) + (tx[3] * tx[3]));
165
      var fontAscent = fontHeight;
166
      if (style.ascent) {
167
        fontAscent = style.ascent * fontAscent;
168
      } else if (style.descent) {
169
        fontAscent = (1 + style.descent) * fontAscent;
170
      }
171
172
      var left;
173
      var top;
174
      if (angle === 0) {
175
        left = tx[4];
176
        top = tx[5] - fontAscent;
177
      } else {
178
        left = tx[4] + (fontAscent * Math.sin(angle));
179
        top = tx[5] - (fontAscent * Math.cos(angle));
180
      }
181
      textDiv.style.left = left + 'px';
182
      textDiv.style.top = top + 'px';
183
      textDiv.style.fontSize = fontHeight + 'px';
184
      textDiv.style.fontFamily = style.fontFamily;
185
186
      textDiv.textContent = geom.str;
187
      // |fontName| is only used by the Font Inspector. This test will succeed
188
      // when e.g. the Font Inspector is off but the Stepper is on, but it's
189
      // not worth the effort to do a more accurate test.
190
      if (PDFJS.pdfBug) {
191
        textDiv.dataset.fontName = geom.fontName;
192
      }
193
      // Storing into dataset will convert number into string.
194
      if (angle !== 0) {
195
        textDiv.dataset.angle = angle * (180 / Math.PI);
196
      }
197
      // We don't bother scaling single-char text divs, because it has very
198
      // little effect on text highlighting. This makes scrolling on docs with
199
      // lots of such divs a lot faster.
200
      if (textDiv.textContent.length > 1) {
201
        if (style.vertical) {
202
          textDiv.dataset.canvasWidth = geom.height * this.viewport.scale;
203
        } else {
204
          textDiv.dataset.canvasWidth = geom.width * this.viewport.scale;
205
        }
206
      }
207
    },
208
209
    setTextContent: function TextLayerBuilder_setTextContent(textContent) {
210
      this.textContent = textContent;
211
212
      var textItems = textContent.items;
213
      for (var i = 0, len = textItems.length; i < len; i++) {
214
        this.appendText(textItems[i], textContent.styles);
215
      }
216
      this.divContentDone = true;
217
    },
218
219
    convertMatches: function TextLayerBuilder_convertMatches(matches) {
220
      var i = 0;
221
      var iIndex = 0;
222
      var bidiTexts = this.textContent.items;
223
      var end = bidiTexts.length - 1;
224
      var queryLen = (this.findController === null ?
225
                      0 : this.findController.state.query.length);
226
      var ret = [];
227
228
      for (var m = 0, len = matches.length; m < len; m++) {
229
        // Calculate the start position.
230
        var matchIdx = matches[m];
231
232
        // Loop over the divIdxs.
233
        while (i !== end && matchIdx >= (iIndex + bidiTexts[i].str.length)) {
234
          iIndex += bidiTexts[i].str.length;
235
          i++;
236
        }
237
238
        if (i === bidiTexts.length) {
239
          console.error('Could not find a matching mapping');
240
        }
241
242
        var match = {
243
          begin: {
244
            divIdx: i,
245
            offset: matchIdx - iIndex
246
          }
247
        };
248
249
        // Calculate the end position.
250
        matchIdx += queryLen;
251
252
        // Somewhat the same array as above, but use > instead of >= to get
253
        // the end position right.
254
        while (i !== end && matchIdx > (iIndex + bidiTexts[i].str.length)) {
255
          iIndex += bidiTexts[i].str.length;
256
          i++;
257
        }
258
259
        match.end = {
260
          divIdx: i,
261
          offset: matchIdx - iIndex
262
        };
263
        ret.push(match);
264
      }
265
266
      return ret;
267
    },
268
269
    renderMatches: function TextLayerBuilder_renderMatches(matches) {
270
      // Early exit if there is nothing to render.
271
      if (matches.length === 0) {
272
        return;
273
      }
274
275
      var bidiTexts = this.textContent.items;
276
      var textDivs = this.textDivs;
277
      var prevEnd = null;
278
      var pageIdx = this.pageIdx;
279
      var isSelectedPage = (this.findController === null ?
280
        false : (pageIdx === this.findController.selected.pageIdx));
281
      var selectedMatchIdx = (this.findController === null ?
282
                              -1 : this.findController.selected.matchIdx);
283
      var highlightAll = (this.findController === null ?
284
                          false : this.findController.state.highlightAll);
285
      var infinity = {
286
        divIdx: -1,
287
        offset: undefined
288
      };
289
290
      function beginText(begin, className) {
291
        var divIdx = begin.divIdx;
292
        textDivs[divIdx].textContent = '';
293
        appendTextToDiv(divIdx, 0, begin.offset, className);
294
      }
295
296
      function appendTextToDiv(divIdx, fromOffset, toOffset, className) {
297
        var div = textDivs[divIdx];
298
        var content = bidiTexts[divIdx].str.substring(fromOffset, toOffset);
299
        var node = document.createTextNode(content);
300
        if (className) {
301
          var span = document.createElement('span');
302
          span.className = className;
303
          span.appendChild(node);
304
          div.appendChild(span);
305
          return;
306
        }
307
        div.appendChild(node);
308
      }
309
310
      var i0 = selectedMatchIdx, i1 = i0 + 1;
311
      if (highlightAll) {
312
        i0 = 0;
313
        i1 = matches.length;
314
      } else if (!isSelectedPage) {
315
        // Not highlighting all and this isn't the selected page, so do nothing.
316
        return;
317
      }
318
319
      for (var i = i0; i < i1; i++) {
320
        var match = matches[i];
321
        var begin = match.begin;
322
        var end = match.end;
323
        var isSelected = (isSelectedPage && i === selectedMatchIdx);
324
        var highlightSuffix = (isSelected ? ' selected' : '');
325
326
        if (this.findController) {
327
          this.findController.updateMatchPosition(pageIdx, i, textDivs,
328
                                                  begin.divIdx, end.divIdx);
329
        }
330
331
        // Match inside new div.
332
        if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
333
          // If there was a previous div, then add the text at the end.
334
          if (prevEnd !== null) {
335
            appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
336
          }
337
          // Clear the divs and set the content until the starting point.
338
          beginText(begin);
339
        } else {
340
          appendTextToDiv(prevEnd.divIdx, prevEnd.offset, begin.offset);
341
        }
342
343
        if (begin.divIdx === end.divIdx) {
344
          appendTextToDiv(begin.divIdx, begin.offset, end.offset,
345
                          'highlight' + highlightSuffix);
346
        } else {
347
          appendTextToDiv(begin.divIdx, begin.offset, infinity.offset,
348
                          'highlight begin' + highlightSuffix);
349
          for (var n0 = begin.divIdx + 1, n1 = end.divIdx; n0 < n1; n0++) {
350
            textDivs[n0].className = 'highlight middle' + highlightSuffix;
351
          }
352
          beginText(end, 'highlight end' + highlightSuffix);
353
        }
354
        prevEnd = end;
355
      }
356
357
      if (prevEnd) {
358
        appendTextToDiv(prevEnd.divIdx, prevEnd.offset, infinity.offset);
359
      }
360
    },
361
362
    updateMatches: function TextLayerBuilder_updateMatches() {
363
      // Only show matches when all rendering is done.
364
      if (!this.renderingDone) {
365
        return;
366
      }
367
368
      // Clear all matches.
369
      var matches = this.matches;
370
      var textDivs = this.textDivs;
371
      var bidiTexts = this.textContent.items;
372
      var clearedUntilDivIdx = -1;
373
374
      // Clear all current matches.
375
      for (var i = 0, len = matches.length; i < len; i++) {
376
        var match = matches[i];
377
        var begin = Math.max(clearedUntilDivIdx, match.begin.divIdx);
378
        for (var n = begin, end = match.end.divIdx; n <= end; n++) {
379
          var div = textDivs[n];
380
          div.textContent = bidiTexts[n].str;
381
          div.className = '';
382
        }
383
        clearedUntilDivIdx = match.end.divIdx + 1;
384
      }
385
386
      if (this.findController === null || !this.findController.active) {
387
        return;
388
      }
389
390
      // Convert the matches on the page controller into the match format
391
      // used for the textLayer.
392
      this.matches = this.convertMatches(this.findController === null ?
393
        [] : (this.findController.pageMatches[this.pageIdx] || []));
394
      this.renderMatches(this.matches);
395
    }
396
  };
397
  return TextLayerBuilder;
398
})();
399
400
/**
401
 * @constructor
402
 * @implements IPDFTextLayerFactory
403
 */
404
function DefaultTextLayerFactory() {}
405
DefaultTextLayerFactory.prototype = {
406
  /**
407
   * @param {HTMLDivElement} textLayerDiv
408
   * @param {number} pageIndex
409
   * @param {PageViewport} viewport
410
   * @returns {TextLayerBuilder}
411
   */
412
  createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) {
413
    return new TextLayerBuilder({
414
      textLayerDiv: textLayerDiv,
415
      pageIndex: pageIndex,
416
      viewport: viewport
417
    });
418
  }
419
};
420