Bläddra i källkod

PDF.js version 1.5.413 - See mozilla/pdf.js@6bb95e3129a58b5c0e2cb1440914f79eb984e498

Pdf Bot 8 år sedan
förälder
incheckning
7a01cdddad
6 ändrade filer med 811 tillägg och 58 borttagningar
  1. 1 1
      bower.json
  2. 383 21
      build/pdf.combined.js
  3. 383 21
      build/pdf.js
  4. 2 2
      build/pdf.worker.js
  5. 1 1
      package.json
  6. 41 12
      web/pdf_viewer.js

+ 1 - 1
bower.json

@@ -1,6 +1,6 @@
 {
   "name": "pdfjs-dist",
-  "version": "1.5.410",
+  "version": "1.5.413",
   "main": [
     "build/pdf.js",
     "build/pdf.worker.js"

+ 383 - 21
build/pdf.combined.js

@@ -28,8 +28,8 @@ factory((root.pdfjsDistBuildPdfCombined = {}));
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.410';
-var pdfjsBuild = '61a576c';
+var pdfjsVersion = '1.5.413';
+var pdfjsBuild = '6bb95e3';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
@@ -24984,7 +24984,8 @@ var renderTextLayer = (function renderTextLayerClosure() {
     return !NonWhitespaceRegexp.test(str);
   }
 
-  function appendText(textDivs, viewport, geom, styles) {
+  function appendText(textDivs, viewport, geom, styles, bounds,
+                      enhanceTextSelection) {
     var style = styles[geom.fontName];
     var textDiv = document.createElement('div');
     textDivs.push(textDiv);
@@ -25040,6 +25041,34 @@ var renderTextLayer = (function renderTextLayerClosure() {
         textDiv.dataset.canvasWidth = geom.width * viewport.scale;
       }
     }
+    if (enhanceTextSelection) {
+      var angleCos = 1, angleSin = 0;
+      if (angle !== 0) {
+        angleCos = Math.cos(angle);
+        angleSin = Math.sin(angle);
+      }
+      var divWidth = (style.vertical ? geom.height : geom.width) *
+                     viewport.scale;
+      var divHeight = fontHeight;
+
+      var m, b;
+      if (angle !== 0) {
+        m = [angleCos, angleSin, -angleSin, angleCos, left, top];
+        b = Util.getAxialAlignedBoundingBox([0, 0, divWidth, divHeight], m);
+      } else {
+        b = [left, top, left + divWidth, top + divHeight];
+      }
+
+      bounds.push({
+        left: b[0],
+        top: b[1],
+        right: b[2],
+        bottom: b[3],
+        div: textDiv,
+        size: [divWidth, divHeight],
+        m: m
+      });
+    }
   }
 
   function render(task) {
@@ -25054,6 +25083,7 @@ var renderTextLayer = (function renderTextLayerClosure() {
     // No point in rendering many divs as it would make the browser
     // unusable even after the divs are rendered.
     if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) {
+      task._renderingDone = true;
       capability.resolve();
       return;
     }
@@ -25080,24 +25110,290 @@ var renderTextLayer = (function renderTextLayerClosure() {
       }
 
       var width = ctx.measureText(textDiv.textContent).width;
+      textDiv.dataset.originalWidth = width;
       textLayerFrag.appendChild(textDiv);
-      var transform;
-      if (textDiv.dataset.canvasWidth !== undefined && width > 0) {
-        // Dataset values come of type string.
-        var textScale = textDiv.dataset.canvasWidth / width;
-        transform = 'scaleX(' + textScale + ')';
-      } else {
-        transform = '';
+       var transform;
+       if (textDiv.dataset.canvasWidth !== undefined && width > 0) {
+        //  Dataset values come of type string.
+         var textScale = textDiv.dataset.canvasWidth / width;
+         transform = 'scaleX(' + textScale + ')';
+       } else {
+         transform = '';
+       }
+       var rotation = textDiv.dataset.angle;
+       if (rotation) {
+         transform = 'rotate(' + rotation + 'deg) ' + transform;
+       }
+       if (transform) {
+         textDiv.dataset.originalTransform = transform;
+         CustomStyle.setProp('transform' , textDiv, transform);
+       }
+    }
+    task._renderingDone = true;
+    capability.resolve();
+  }
+
+  function expand(bounds, viewport) {
+    var expanded = expandBounds(viewport.width, viewport.height, bounds);
+    for (var i = 0; i < expanded.length; i++) {
+      var div = bounds[i].div;
+      if (!div.dataset.angle) {
+        div.dataset.paddingLeft = bounds[i].left - expanded[i].left;
+        div.dataset.paddingTop = bounds[i].top - expanded[i].top;
+        div.dataset.paddingRight = expanded[i].right - bounds[i].right;
+        div.dataset.paddingBottom = expanded[i].bottom - bounds[i].bottom;
+        continue;
       }
-      var rotation = textDiv.dataset.angle;
-      if (rotation) {
-        transform = 'rotate(' + rotation + 'deg) ' + transform;
+      // Box is rotated -- trying to find padding so rotated div will not
+      // exceed its expanded bounds.
+      var e = expanded[i], b = bounds[i];
+      var m = b.m, c = m[0], s = m[1];
+      // Finding intersections with expanded box.
+      var points = [[0, 0], [0, b.size[1]], [b.size[0], 0], b.size];
+      var ts = new Float64Array(64);
+      points.forEach(function (p, i) {
+        var t = Util.applyTransform(p, m);
+        ts[i + 0] = c && (e.left - t[0]) / c;
+        ts[i + 4] = s && (e.top - t[1]) / s;
+        ts[i + 8] = c && (e.right - t[0]) / c;
+        ts[i + 12] = s && (e.bottom - t[1]) / s;
+
+        ts[i + 16] = s && (e.left - t[0]) / -s;
+        ts[i + 20] = c && (e.top - t[1]) / c;
+        ts[i + 24] = s && (e.right - t[0]) / -s;
+        ts[i + 28] = c && (e.bottom - t[1]) / c;
+
+        ts[i + 32] = c && (e.left - t[0]) / -c;
+        ts[i + 36] = s && (e.top - t[1]) / -s;
+        ts[i + 40] = c && (e.right - t[0]) / -c;
+        ts[i + 44] = s && (e.bottom - t[1]) / -s;
+
+        ts[i + 48] = s && (e.left - t[0]) / s;
+        ts[i + 52] = c && (e.top - t[1]) / -c;
+        ts[i + 56] = s && (e.right - t[0]) / s;
+        ts[i + 60] = c && (e.bottom - t[1]) / -c;
+      });
+      var findPositiveMin = function (ts, offset, count) {
+        var result = 0;
+          for (var i = 0; i < count; i++) {
+            var t = ts[offset++];
+            if (t > 0) {
+              result = result ? Math.min(t, result) : t;
+            }
+          }
+          return result;
+      };
+      // Not based on math, but to simplify calculations, using cos and sin
+      // absolute values to not exceed the box (it can but insignificantly).
+      var boxScale = 1 + Math.min(Math.abs(c), Math.abs(s));
+      div.dataset.paddingLeft = findPositiveMin(ts, 32, 16) / boxScale;
+      div.dataset.paddingTop = findPositiveMin(ts, 48, 16) / boxScale;
+      div.dataset.paddingRight = findPositiveMin(ts, 0, 16) / boxScale;
+      div.dataset.paddingBottom = findPositiveMin(ts, 16, 16) / boxScale;
+    }
+  }
+
+  function expandBounds(width, height, boxes) {
+    var bounds = boxes.map(function (box, i) {
+      return {
+        x1: box.left,
+        y1: box.top,
+        x2: box.right,
+        y2: box.bottom,
+        index: i,
+        x1New: undefined,
+        x2New: undefined
+      };
+    });
+    expandBoundsLTR(width, bounds);
+    var expanded = new Array(boxes.length);
+    bounds.forEach(function (b) {
+      var i = b.index;
+      expanded[i] = {
+        left: b.x1New,
+        top: 0,
+        right: b.x2New,
+        bottom: 0
+      };
+    });
+
+    // Rotating on 90 degrees and extending extended boxes. Reusing the bounds
+    // array and objects.
+    boxes.map(function (box, i) {
+      var e = expanded[i], b = bounds[i];
+      b.x1 = box.top;
+      b.y1 = width - e.right;
+      b.x2 = box.bottom;
+      b.y2 = width - e.left;
+      b.index = i;
+      b.x1New = undefined;
+      b.x2New = undefined;
+    });
+    expandBoundsLTR(height, bounds);
+
+    bounds.forEach(function (b) {
+      var i = b.index;
+      expanded[i].top = b.x1New;
+      expanded[i].bottom = b.x2New;
+    });
+    return expanded;
+  }
+
+  function expandBoundsLTR(width, bounds) {
+    // Sorting by x1 coordinate and walk by the bounds in the same order.
+    bounds.sort(function (a, b) { return a.x1 - b.x1 || a.index - b.index; });
+
+    // First we see on the horizon is a fake boundary.
+    var fakeBoundary = {
+      x1: -Infinity,
+      y1: -Infinity,
+      x2: 0,
+      y2: Infinity,
+      index: -1,
+      x1New: 0,
+      x2New: 0
+    };
+    var horizon = [{
+      start: -Infinity,
+      end: Infinity,
+      boundary: fakeBoundary
+    }];
+
+    bounds.forEach(function (boundary) {
+      // Searching for the affected part of horizon.
+      // TODO red-black tree or simple binary search
+      var i = 0;
+      while (i < horizon.length && horizon[i].end <= boundary.y1) {
+        i++;
       }
-      if (transform) {
-        CustomStyle.setProp('transform' , textDiv, transform);
+      var j = horizon.length - 1;
+      while(j >= 0 && horizon[j].start >= boundary.y2) {
+        j--;
+      }
+
+      var horizonPart, affectedBoundary;
+      var q, k, maxXNew = -Infinity;
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        var xNew;
+        if (affectedBoundary.x2 > boundary.x1) {
+          // In the middle of the previous element, new x shall be at the
+          // boundary start. Extending if further if the affected bondary
+          // placed on top of the current one.
+          xNew = affectedBoundary.index > boundary.index ?
+            affectedBoundary.x1New : boundary.x1;
+        } else if (affectedBoundary.x2New === undefined) {
+          // We have some space in between, new x in middle will be a fair
+          // choice.
+          xNew = (affectedBoundary.x2 + boundary.x1) / 2;
+        } else {
+          // Affected boundary has x2new set, using it as new x.
+          xNew = affectedBoundary.x2New;
+        }
+        if (xNew > maxXNew) {
+          maxXNew = xNew;
+        }
       }
-    }
-    capability.resolve();
+
+      // Set new x1 for current boundary.
+      boundary.x1New = maxXNew;
+
+      // Adjusts new x2 for the affected boundaries.
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        if (affectedBoundary.x2New === undefined) {
+          // Was not set yet, choosing new x if possible.
+          if (affectedBoundary.x2 > boundary.x1) {
+            // Current and affected boundaries intersect. If affected boundary
+            // is placed on top of the current, shrinking the affected.
+            if (affectedBoundary.index > boundary.index) {
+              affectedBoundary.x2New = affectedBoundary.x2;
+            }
+          } else {
+            affectedBoundary.x2New = maxXNew;
+          }
+        } else if (affectedBoundary.x2New > maxXNew) {
+          // Affected boundary is touching new x, pushing it back.
+          affectedBoundary.x2New = Math.max(maxXNew, affectedBoundary.x2);
+        }
+      }
+
+      // Fixing the horizon.
+      var changedHorizon = [], lastBoundary = null;
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        // Checking which boundary will be visible.
+        var useBoundary = affectedBoundary.x2 > boundary.x2 ?
+          affectedBoundary : boundary;
+        if (lastBoundary === useBoundary) {
+          // Merging with previous.
+          changedHorizon[changedHorizon.length - 1].end = horizonPart.end;
+        } else {
+          changedHorizon.push({
+            start: horizonPart.start,
+            end: horizonPart.end,
+            boundary: useBoundary
+          });
+          lastBoundary = useBoundary;
+        }
+      }
+      if (horizon[i].start < boundary.y1) {
+        changedHorizon[0].start = boundary.y1;
+        changedHorizon.unshift({
+          start: horizon[i].start,
+          end: boundary.y1,
+          boundary: horizon[i].boundary
+        });
+      }
+      if (boundary.y2 < horizon[j].end) {
+        changedHorizon[changedHorizon.length - 1].end = boundary.y2;
+        changedHorizon.push({
+          start: boundary.y2,
+          end: horizon[j].end,
+          boundary: horizon[j].boundary
+        });
+      }
+
+      // Set x2 new of boundary that is no longer visible (see overlapping case
+      // above).
+      // TODO more efficient, e.g. via reference counting.
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        if (affectedBoundary.x2New !== undefined) {
+          continue;
+        }
+        var used = false;
+        for (k = i - 1; !used && k >= 0 &&
+        horizon[k].start >= affectedBoundary.y1; k--) {
+          used = horizon[k].boundary === affectedBoundary;
+        }
+        for (k = j + 1; !used && k < horizon.length &&
+        horizon[k].end <= affectedBoundary.y2; k++) {
+          used = horizon[k].boundary === affectedBoundary;
+        }
+        for (k = 0; !used && k < changedHorizon.length; k++) {
+          used = changedHorizon[k].boundary === affectedBoundary;
+        }
+        if (!used) {
+          affectedBoundary.x2New = maxXNew;
+        }
+      }
+
+      Array.prototype.splice.apply(horizon,
+        [i, j - i + 1].concat(changedHorizon));
+    });
+
+    // Set new x2 for all unset boundaries.
+    horizon.forEach(function (horizonPart) {
+      var affectedBoundary = horizonPart.boundary;
+      if (affectedBoundary.x2New === undefined) {
+        affectedBoundary.x2New = Math.max(width, affectedBoundary.x2);
+      }
+    });
   }
 
   /**
@@ -25107,17 +25403,23 @@ var renderTextLayer = (function renderTextLayerClosure() {
    * @param {HTMLElement} container
    * @param {PageViewport} viewport
    * @param {Array} textDivs
+   * @param {boolean} enhanceTextSelection
    * @private
    */
-  function TextLayerRenderTask(textContent, container, viewport, textDivs) {
+  function TextLayerRenderTask(textContent, container, viewport, textDivs,
+                               enhanceTextSelection) {
     this._textContent = textContent;
     this._container = container;
     this._viewport = viewport;
     textDivs = textDivs || [];
     this._textDivs = textDivs;
+    this._renderingDone = false;
     this._canceled = false;
     this._capability = createPromiseCapability();
     this._renderTimer = null;
+    this._bounds = [];
+    this._enhanceTextSelection = !!enhanceTextSelection;
+    this._expanded = false;
   }
   TextLayerRenderTask.prototype = {
     get promise() {
@@ -25138,8 +25440,11 @@ var renderTextLayer = (function renderTextLayerClosure() {
       var styles = this._textContent.styles;
       var textDivs = this._textDivs;
       var viewport = this._viewport;
+      var enhanceTextSelection = this._enhanceTextSelection;
+
       for (var i = 0, len = textItems.length; i < len; i++) {
-        appendText(textDivs, viewport, textItems[i], styles);
+        appendText(textDivs, viewport, textItems[i], styles, this._bounds,
+                   enhanceTextSelection);
       }
 
       if (!timeout) { // Render right away
@@ -25151,7 +25456,63 @@ var renderTextLayer = (function renderTextLayerClosure() {
           self._renderTimer = null;
         }, timeout);
       }
-    }
+    },
+
+    expandTextDivs: function TextLayer_expandTextDivs(expandDivs) {
+      if (!this._enhanceTextSelection || !this._renderingDone) {
+        return;
+      }
+      if (!this._expanded) {
+        expand(this._bounds, this._viewport);
+        this._expanded = true;
+        this._bounds.length = 0;
+      }
+      if (expandDivs) {
+        for (var i = 0, ii = this._textDivs.length; i < ii; i++) {
+          var div = this._textDivs[i];
+          var transform;
+          var width = div.dataset.originalWidth;
+          if (div.dataset.canvasWidth !== undefined && width > 0) {
+            // Dataset values come of type string.
+            var textScale = div.dataset.canvasWidth / width;
+            transform = 'scaleX(' + textScale + ')';
+          } else {
+            transform = '';
+          }
+          var rotation = div.dataset.angle;
+          if (rotation) {
+            transform = 'rotate(' + rotation + 'deg) ' + transform;
+          }
+          if (div.dataset.paddingLeft) {
+            div.style.paddingLeft =
+              (div.dataset.paddingLeft / textScale) + 'px';
+            transform += ' translateX(' +
+              (-div.dataset.paddingLeft / textScale) + 'px)';
+          }
+          if (div.dataset.paddingTop) {
+            div.style.paddingTop = div.dataset.paddingTop + 'px';
+            transform += ' translateY(' + (-div.dataset.paddingTop) + 'px)';
+          }
+          if (div.dataset.paddingRight) {
+            div.style.paddingRight =
+            div.dataset.paddingRight / textScale + 'px';
+          }
+          if (div.dataset.paddingBottom) {
+            div.style.paddingBottom = div.dataset.paddingBottom + 'px';
+          }
+          if (transform) {
+            CustomStyle.setProp('transform' , div, transform);
+          }
+        }
+      } else {
+        for (i = 0, ii = this._textDivs.length; i < ii; i++) {
+          div = this._textDivs[i];
+          div.style.padding = 0;
+          transform = div.dataset.originalTransform || '';
+          CustomStyle.setProp('transform', div, transform);
+        }
+      }
+    },
   };
 
 
@@ -25165,7 +25526,8 @@ var renderTextLayer = (function renderTextLayerClosure() {
     var task = new TextLayerRenderTask(renderParameters.textContent,
                                        renderParameters.container,
                                        renderParameters.viewport,
-                                       renderParameters.textDivs);
+                                       renderParameters.textDivs,
+                                       renderParameters.enhanceTextSelection);
     task._render(renderParameters.timeout);
     return task;
   }

+ 383 - 21
build/pdf.js

@@ -28,8 +28,8 @@ factory((root.pdfjsDistBuildPdf = {}));
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.410';
-var pdfjsBuild = '61a576c';
+var pdfjsVersion = '1.5.413';
+var pdfjsBuild = '6bb95e3';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
@@ -5189,7 +5189,8 @@ var renderTextLayer = (function renderTextLayerClosure() {
     return !NonWhitespaceRegexp.test(str);
   }
 
-  function appendText(textDivs, viewport, geom, styles) {
+  function appendText(textDivs, viewport, geom, styles, bounds,
+                      enhanceTextSelection) {
     var style = styles[geom.fontName];
     var textDiv = document.createElement('div');
     textDivs.push(textDiv);
@@ -5245,6 +5246,34 @@ var renderTextLayer = (function renderTextLayerClosure() {
         textDiv.dataset.canvasWidth = geom.width * viewport.scale;
       }
     }
+    if (enhanceTextSelection) {
+      var angleCos = 1, angleSin = 0;
+      if (angle !== 0) {
+        angleCos = Math.cos(angle);
+        angleSin = Math.sin(angle);
+      }
+      var divWidth = (style.vertical ? geom.height : geom.width) *
+                     viewport.scale;
+      var divHeight = fontHeight;
+
+      var m, b;
+      if (angle !== 0) {
+        m = [angleCos, angleSin, -angleSin, angleCos, left, top];
+        b = Util.getAxialAlignedBoundingBox([0, 0, divWidth, divHeight], m);
+      } else {
+        b = [left, top, left + divWidth, top + divHeight];
+      }
+
+      bounds.push({
+        left: b[0],
+        top: b[1],
+        right: b[2],
+        bottom: b[3],
+        div: textDiv,
+        size: [divWidth, divHeight],
+        m: m
+      });
+    }
   }
 
   function render(task) {
@@ -5259,6 +5288,7 @@ var renderTextLayer = (function renderTextLayerClosure() {
     // No point in rendering many divs as it would make the browser
     // unusable even after the divs are rendered.
     if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) {
+      task._renderingDone = true;
       capability.resolve();
       return;
     }
@@ -5286,24 +5316,290 @@ var renderTextLayer = (function renderTextLayerClosure() {
       }
 
       var width = ctx.measureText(textDiv.textContent).width;
+      textDiv.dataset.originalWidth = width;
       textLayerFrag.appendChild(textDiv);
-      var transform;
-      if (textDiv.dataset.canvasWidth !== undefined && width > 0) {
-        // Dataset values come of type string.
-        var textScale = textDiv.dataset.canvasWidth / width;
-        transform = 'scaleX(' + textScale + ')';
-      } else {
-        transform = '';
+       var transform;
+       if (textDiv.dataset.canvasWidth !== undefined && width > 0) {
+        //  Dataset values come of type string.
+         var textScale = textDiv.dataset.canvasWidth / width;
+         transform = 'scaleX(' + textScale + ')';
+       } else {
+         transform = '';
+       }
+       var rotation = textDiv.dataset.angle;
+       if (rotation) {
+         transform = 'rotate(' + rotation + 'deg) ' + transform;
+       }
+       if (transform) {
+         textDiv.dataset.originalTransform = transform;
+         CustomStyle.setProp('transform' , textDiv, transform);
+       }
+    }
+    task._renderingDone = true;
+    capability.resolve();
+  }
+
+  function expand(bounds, viewport) {
+    var expanded = expandBounds(viewport.width, viewport.height, bounds);
+    for (var i = 0; i < expanded.length; i++) {
+      var div = bounds[i].div;
+      if (!div.dataset.angle) {
+        div.dataset.paddingLeft = bounds[i].left - expanded[i].left;
+        div.dataset.paddingTop = bounds[i].top - expanded[i].top;
+        div.dataset.paddingRight = expanded[i].right - bounds[i].right;
+        div.dataset.paddingBottom = expanded[i].bottom - bounds[i].bottom;
+        continue;
+      }
+      // Box is rotated -- trying to find padding so rotated div will not
+      // exceed its expanded bounds.
+      var e = expanded[i], b = bounds[i];
+      var m = b.m, c = m[0], s = m[1];
+      // Finding intersections with expanded box.
+      var points = [[0, 0], [0, b.size[1]], [b.size[0], 0], b.size];
+      var ts = new Float64Array(64);
+      points.forEach(function (p, i) {
+        var t = Util.applyTransform(p, m);
+        ts[i + 0] = c && (e.left - t[0]) / c;
+        ts[i + 4] = s && (e.top - t[1]) / s;
+        ts[i + 8] = c && (e.right - t[0]) / c;
+        ts[i + 12] = s && (e.bottom - t[1]) / s;
+
+        ts[i + 16] = s && (e.left - t[0]) / -s;
+        ts[i + 20] = c && (e.top - t[1]) / c;
+        ts[i + 24] = s && (e.right - t[0]) / -s;
+        ts[i + 28] = c && (e.bottom - t[1]) / c;
+
+        ts[i + 32] = c && (e.left - t[0]) / -c;
+        ts[i + 36] = s && (e.top - t[1]) / -s;
+        ts[i + 40] = c && (e.right - t[0]) / -c;
+        ts[i + 44] = s && (e.bottom - t[1]) / -s;
+
+        ts[i + 48] = s && (e.left - t[0]) / s;
+        ts[i + 52] = c && (e.top - t[1]) / -c;
+        ts[i + 56] = s && (e.right - t[0]) / s;
+        ts[i + 60] = c && (e.bottom - t[1]) / -c;
+      });
+      var findPositiveMin = function (ts, offset, count) {
+        var result = 0;
+          for (var i = 0; i < count; i++) {
+            var t = ts[offset++];
+            if (t > 0) {
+              result = result ? Math.min(t, result) : t;
+            }
+          }
+          return result;
+      };
+      // Not based on math, but to simplify calculations, using cos and sin
+      // absolute values to not exceed the box (it can but insignificantly).
+      var boxScale = 1 + Math.min(Math.abs(c), Math.abs(s));
+      div.dataset.paddingLeft = findPositiveMin(ts, 32, 16) / boxScale;
+      div.dataset.paddingTop = findPositiveMin(ts, 48, 16) / boxScale;
+      div.dataset.paddingRight = findPositiveMin(ts, 0, 16) / boxScale;
+      div.dataset.paddingBottom = findPositiveMin(ts, 16, 16) / boxScale;
+    }
+  }
+
+  function expandBounds(width, height, boxes) {
+    var bounds = boxes.map(function (box, i) {
+      return {
+        x1: box.left,
+        y1: box.top,
+        x2: box.right,
+        y2: box.bottom,
+        index: i,
+        x1New: undefined,
+        x2New: undefined
+      };
+    });
+    expandBoundsLTR(width, bounds);
+    var expanded = new Array(boxes.length);
+    bounds.forEach(function (b) {
+      var i = b.index;
+      expanded[i] = {
+        left: b.x1New,
+        top: 0,
+        right: b.x2New,
+        bottom: 0
+      };
+    });
+
+    // Rotating on 90 degrees and extending extended boxes. Reusing the bounds
+    // array and objects.
+    boxes.map(function (box, i) {
+      var e = expanded[i], b = bounds[i];
+      b.x1 = box.top;
+      b.y1 = width - e.right;
+      b.x2 = box.bottom;
+      b.y2 = width - e.left;
+      b.index = i;
+      b.x1New = undefined;
+      b.x2New = undefined;
+    });
+    expandBoundsLTR(height, bounds);
+
+    bounds.forEach(function (b) {
+      var i = b.index;
+      expanded[i].top = b.x1New;
+      expanded[i].bottom = b.x2New;
+    });
+    return expanded;
+  }
+
+  function expandBoundsLTR(width, bounds) {
+    // Sorting by x1 coordinate and walk by the bounds in the same order.
+    bounds.sort(function (a, b) { return a.x1 - b.x1 || a.index - b.index; });
+
+    // First we see on the horizon is a fake boundary.
+    var fakeBoundary = {
+      x1: -Infinity,
+      y1: -Infinity,
+      x2: 0,
+      y2: Infinity,
+      index: -1,
+      x1New: 0,
+      x2New: 0
+    };
+    var horizon = [{
+      start: -Infinity,
+      end: Infinity,
+      boundary: fakeBoundary
+    }];
+
+    bounds.forEach(function (boundary) {
+      // Searching for the affected part of horizon.
+      // TODO red-black tree or simple binary search
+      var i = 0;
+      while (i < horizon.length && horizon[i].end <= boundary.y1) {
+        i++;
       }
-      var rotation = textDiv.dataset.angle;
-      if (rotation) {
-        transform = 'rotate(' + rotation + 'deg) ' + transform;
+      var j = horizon.length - 1;
+      while(j >= 0 && horizon[j].start >= boundary.y2) {
+        j--;
+      }
+
+      var horizonPart, affectedBoundary;
+      var q, k, maxXNew = -Infinity;
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        var xNew;
+        if (affectedBoundary.x2 > boundary.x1) {
+          // In the middle of the previous element, new x shall be at the
+          // boundary start. Extending if further if the affected bondary
+          // placed on top of the current one.
+          xNew = affectedBoundary.index > boundary.index ?
+            affectedBoundary.x1New : boundary.x1;
+        } else if (affectedBoundary.x2New === undefined) {
+          // We have some space in between, new x in middle will be a fair
+          // choice.
+          xNew = (affectedBoundary.x2 + boundary.x1) / 2;
+        } else {
+          // Affected boundary has x2new set, using it as new x.
+          xNew = affectedBoundary.x2New;
+        }
+        if (xNew > maxXNew) {
+          maxXNew = xNew;
+        }
       }
-      if (transform) {
-        CustomStyle.setProp('transform' , textDiv, transform);
+
+      // Set new x1 for current boundary.
+      boundary.x1New = maxXNew;
+
+      // Adjusts new x2 for the affected boundaries.
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        if (affectedBoundary.x2New === undefined) {
+          // Was not set yet, choosing new x if possible.
+          if (affectedBoundary.x2 > boundary.x1) {
+            // Current and affected boundaries intersect. If affected boundary
+            // is placed on top of the current, shrinking the affected.
+            if (affectedBoundary.index > boundary.index) {
+              affectedBoundary.x2New = affectedBoundary.x2;
+            }
+          } else {
+            affectedBoundary.x2New = maxXNew;
+          }
+        } else if (affectedBoundary.x2New > maxXNew) {
+          // Affected boundary is touching new x, pushing it back.
+          affectedBoundary.x2New = Math.max(maxXNew, affectedBoundary.x2);
+        }
       }
-    }
-    capability.resolve();
+
+      // Fixing the horizon.
+      var changedHorizon = [], lastBoundary = null;
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        // Checking which boundary will be visible.
+        var useBoundary = affectedBoundary.x2 > boundary.x2 ?
+          affectedBoundary : boundary;
+        if (lastBoundary === useBoundary) {
+          // Merging with previous.
+          changedHorizon[changedHorizon.length - 1].end = horizonPart.end;
+        } else {
+          changedHorizon.push({
+            start: horizonPart.start,
+            end: horizonPart.end,
+            boundary: useBoundary
+          });
+          lastBoundary = useBoundary;
+        }
+      }
+      if (horizon[i].start < boundary.y1) {
+        changedHorizon[0].start = boundary.y1;
+        changedHorizon.unshift({
+          start: horizon[i].start,
+          end: boundary.y1,
+          boundary: horizon[i].boundary
+        });
+      }
+      if (boundary.y2 < horizon[j].end) {
+        changedHorizon[changedHorizon.length - 1].end = boundary.y2;
+        changedHorizon.push({
+          start: boundary.y2,
+          end: horizon[j].end,
+          boundary: horizon[j].boundary
+        });
+      }
+
+      // Set x2 new of boundary that is no longer visible (see overlapping case
+      // above).
+      // TODO more efficient, e.g. via reference counting.
+      for (q = i; q <= j; q++) {
+        horizonPart = horizon[q];
+        affectedBoundary = horizonPart.boundary;
+        if (affectedBoundary.x2New !== undefined) {
+          continue;
+        }
+        var used = false;
+        for (k = i - 1; !used && k >= 0 &&
+        horizon[k].start >= affectedBoundary.y1; k--) {
+          used = horizon[k].boundary === affectedBoundary;
+        }
+        for (k = j + 1; !used && k < horizon.length &&
+        horizon[k].end <= affectedBoundary.y2; k++) {
+          used = horizon[k].boundary === affectedBoundary;
+        }
+        for (k = 0; !used && k < changedHorizon.length; k++) {
+          used = changedHorizon[k].boundary === affectedBoundary;
+        }
+        if (!used) {
+          affectedBoundary.x2New = maxXNew;
+        }
+      }
+
+      Array.prototype.splice.apply(horizon,
+        [i, j - i + 1].concat(changedHorizon));
+    });
+
+    // Set new x2 for all unset boundaries.
+    horizon.forEach(function (horizonPart) {
+      var affectedBoundary = horizonPart.boundary;
+      if (affectedBoundary.x2New === undefined) {
+        affectedBoundary.x2New = Math.max(width, affectedBoundary.x2);
+      }
+    });
   }
 
   /**
@@ -5313,17 +5609,23 @@ var renderTextLayer = (function renderTextLayerClosure() {
    * @param {HTMLElement} container
    * @param {PageViewport} viewport
    * @param {Array} textDivs
+   * @param {boolean} enhanceTextSelection
    * @private
    */
-  function TextLayerRenderTask(textContent, container, viewport, textDivs) {
+  function TextLayerRenderTask(textContent, container, viewport, textDivs,
+                               enhanceTextSelection) {
     this._textContent = textContent;
     this._container = container;
     this._viewport = viewport;
     textDivs = textDivs || [];
     this._textDivs = textDivs;
+    this._renderingDone = false;
     this._canceled = false;
     this._capability = createPromiseCapability();
     this._renderTimer = null;
+    this._bounds = [];
+    this._enhanceTextSelection = !!enhanceTextSelection;
+    this._expanded = false;
   }
   TextLayerRenderTask.prototype = {
     get promise() {
@@ -5344,8 +5646,11 @@ var renderTextLayer = (function renderTextLayerClosure() {
       var styles = this._textContent.styles;
       var textDivs = this._textDivs;
       var viewport = this._viewport;
+      var enhanceTextSelection = this._enhanceTextSelection;
+
       for (var i = 0, len = textItems.length; i < len; i++) {
-        appendText(textDivs, viewport, textItems[i], styles);
+        appendText(textDivs, viewport, textItems[i], styles, this._bounds,
+                   enhanceTextSelection);
       }
 
       if (!timeout) { // Render right away
@@ -5357,7 +5662,63 @@ var renderTextLayer = (function renderTextLayerClosure() {
           self._renderTimer = null;
         }, timeout);
       }
-    }
+    },
+
+    expandTextDivs: function TextLayer_expandTextDivs(expandDivs) {
+      if (!this._enhanceTextSelection || !this._renderingDone) {
+        return;
+      }
+      if (!this._expanded) {
+        expand(this._bounds, this._viewport);
+        this._expanded = true;
+        this._bounds.length = 0;
+      }
+      if (expandDivs) {
+        for (var i = 0, ii = this._textDivs.length; i < ii; i++) {
+          var div = this._textDivs[i];
+          var transform;
+          var width = div.dataset.originalWidth;
+          if (div.dataset.canvasWidth !== undefined && width > 0) {
+            // Dataset values come of type string.
+            var textScale = div.dataset.canvasWidth / width;
+            transform = 'scaleX(' + textScale + ')';
+          } else {
+            transform = '';
+          }
+          var rotation = div.dataset.angle;
+          if (rotation) {
+            transform = 'rotate(' + rotation + 'deg) ' + transform;
+          }
+          if (div.dataset.paddingLeft) {
+            div.style.paddingLeft =
+              (div.dataset.paddingLeft / textScale) + 'px';
+            transform += ' translateX(' +
+              (-div.dataset.paddingLeft / textScale) + 'px)';
+          }
+          if (div.dataset.paddingTop) {
+            div.style.paddingTop = div.dataset.paddingTop + 'px';
+            transform += ' translateY(' + (-div.dataset.paddingTop) + 'px)';
+          }
+          if (div.dataset.paddingRight) {
+            div.style.paddingRight =
+            div.dataset.paddingRight / textScale + 'px';
+          }
+          if (div.dataset.paddingBottom) {
+            div.style.paddingBottom = div.dataset.paddingBottom + 'px';
+          }
+          if (transform) {
+            CustomStyle.setProp('transform' , div, transform);
+          }
+        }
+      } else {
+        for (i = 0, ii = this._textDivs.length; i < ii; i++) {
+          div = this._textDivs[i];
+          div.style.padding = 0;
+          transform = div.dataset.originalTransform || '';
+          CustomStyle.setProp('transform', div, transform);
+        }
+      }
+    },
   };
 
 
@@ -5371,7 +5732,8 @@ var renderTextLayer = (function renderTextLayerClosure() {
     var task = new TextLayerRenderTask(renderParameters.textContent,
                                        renderParameters.container,
                                        renderParameters.viewport,
-                                       renderParameters.textDivs);
+                                       renderParameters.textDivs,
+                                       renderParameters.enhanceTextSelection);
     task._render(renderParameters.timeout);
     return task;
   }

+ 2 - 2
build/pdf.worker.js

@@ -28,8 +28,8 @@ factory((root.pdfjsDistBuildPdfWorker = {}));
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.410';
-var pdfjsBuild = '61a576c';
+var pdfjsVersion = '1.5.413';
+var pdfjsBuild = '6bb95e3';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "pdfjs-dist",
-  "version": "1.5.410",
+  "version": "1.5.413",
   "main": "build/pdf.js",
   "description": "Generic build of Mozilla's PDF.js library.",
   "keywords": [

+ 41 - 12
web/pdf_viewer.js

@@ -2283,6 +2283,8 @@ var TEXT_LAYER_RENDER_DELAY = 200; // ms
  * @property {PDFRenderingQueue} renderingQueue - The rendering queue object.
  * @property {IPDFTextLayerFactory} textLayerFactory
  * @property {IPDFAnnotationLayerFactory} annotationLayerFactory
+ * @property {boolean} enhanceTextSelection - Turns on the text selection
+ * enhancement. The default is `false`.
  */
 
 /**
@@ -2302,6 +2304,7 @@ var PDFPageView = (function PDFPageViewClosure() {
     var renderingQueue = options.renderingQueue;
     var textLayerFactory = options.textLayerFactory;
     var annotationLayerFactory = options.annotationLayerFactory;
+    var enhanceTextSelection = options.enhanceTextSelection || false;
 
     this.id = id;
     this.renderingId = 'page' + id;
@@ -2311,6 +2314,7 @@ var PDFPageView = (function PDFPageViewClosure() {
     this.viewport = defaultViewport;
     this.pdfPageRotate = defaultViewport.rotation;
     this.hasRestrictedScaling = false;
+    this.enhanceTextSelection = enhanceTextSelection;
 
     this.eventBus = options.eventBus || domEvents.getGlobalEventBus();
     this.renderingQueue = renderingQueue;
@@ -2626,9 +2630,9 @@ var PDFPageView = (function PDFPageViewClosure() {
           div.appendChild(textLayerDiv);
         }
 
-        textLayer = this.textLayerFactory.createTextLayerBuilder(textLayerDiv,
-                                                                 this.id - 1,
-                                                                 this.viewport);
+        textLayer = this.textLayerFactory.
+          createTextLayerBuilder(textLayerDiv, this.id - 1, this.viewport,
+                                 this.enhanceTextSelection);
       }
       this.textLayer = textLayer;
 
@@ -2844,6 +2848,8 @@ exports.PDFPageView = PDFPageView;
  * @property {number} pageIndex - The page index.
  * @property {PageViewport} viewport - The viewport of the text layer.
  * @property {PDFFindController} findController
+ * @property {boolean} enhanceTextSelection - Option to turn on improved
+ * text selection.
  */
 
 /**
@@ -2866,6 +2872,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() {
     this.textDivs = [];
     this.findController = options.findController || null;
     this.textLayerRenderTask = null;
+    this.enhanceTextSelection = options.enhanceTextSelection;
     this._bindMouse();
   }
 
@@ -2873,9 +2880,11 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() {
     _finishRendering: function TextLayerBuilder_finishRendering() {
       this.renderingDone = true;
 
-      var endOfContent = document.createElement('div');
-      endOfContent.className = 'endOfContent';
-      this.textLayerDiv.appendChild(endOfContent);
+      if (!this.enhanceTextSelection) {
+        var endOfContent = document.createElement('div');
+        endOfContent.className = 'endOfContent';
+        this.textLayerDiv.appendChild(endOfContent);
+      }
 
       this.eventBus.dispatch('textlayerrendered', {
         source: this,
@@ -2905,7 +2914,8 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() {
         container: textLayerFrag,
         viewport: this.viewport,
         textDivs: this.textDivs,
-        timeout: timeout
+        timeout: timeout,
+        enhanceTextSelection: this.enhanceTextSelection,
       });
       this.textLayerRenderTask.promise.then(function () {
         this.textLayerDiv.appendChild(textLayerFrag);
@@ -3123,7 +3133,12 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() {
      */
     _bindMouse: function TextLayerBuilder_bindMouse() {
       var div = this.textLayerDiv;
+      var self = this;
       div.addEventListener('mousedown', function (e) {
+        if (self.enhanceTextSelection && self.textLayerRenderTask) {
+          self.textLayerRenderTask.expandTextDivs(true);
+          return;
+        }
         var end = div.querySelector('.endOfContent');
         if (!end) {
           return;
@@ -3143,6 +3158,10 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() {
         end.classList.add('active');
       });
       div.addEventListener('mouseup', function (e) {
+        if (self.enhanceTextSelection && self.textLayerRenderTask) {
+          self.textLayerRenderTask.expandTextDivs(false);
+          return;
+        }
         var end = div.querySelector('.endOfContent');
         if (!end) {
           return;
@@ -3165,13 +3184,16 @@ DefaultTextLayerFactory.prototype = {
    * @param {HTMLDivElement} textLayerDiv
    * @param {number} pageIndex
    * @param {PageViewport} viewport
+   * @param {boolean} enhanceTextSelection
    * @returns {TextLayerBuilder}
    */
-  createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) {
+  createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport,
+                                    enhanceTextSelection) {
     return new TextLayerBuilder({
       textLayerDiv: textLayerDiv,
       pageIndex: pageIndex,
-      viewport: viewport
+      viewport: viewport,
+      enhanceTextSelection: enhanceTextSelection
     });
   }
 };
@@ -3349,6 +3371,8 @@ var DEFAULT_CACHE_SIZE = 10;
  *   queue object.
  * @property {boolean} removePageBorders - (optional) Removes the border shadow
  *   around the pages. The default is false.
+ * @property {boolean} enhanceTextSelection - (optional) Enables the improved
+ *   text selection behaviour. The default is `false`.
  */
 
 /**
@@ -3400,6 +3424,7 @@ var PDFViewer = (function pdfViewer() {
     this.linkService = options.linkService || new SimpleLinkService();
     this.downloadManager = options.downloadManager || null;
     this.removePageBorders = options.removePageBorders || false;
+    this.enhanceTextSelection = options.enhanceTextSelection || false;
 
     this.defaultRenderingQueue = !options.renderingQueue;
     if (this.defaultRenderingQueue) {
@@ -3625,7 +3650,8 @@ var PDFViewer = (function pdfViewer() {
             defaultViewport: viewport.clone(),
             renderingQueue: this.renderingQueue,
             textLayerFactory: textLayerFactory,
-            annotationLayerFactory: this
+            annotationLayerFactory: this,
+            enhanceTextSelection: this.enhanceTextSelection,
           });
           bindOnAfterAndBeforeDraw(pageView);
           this._pages.push(pageView);
@@ -4107,13 +4133,16 @@ var PDFViewer = (function pdfViewer() {
      * @param {PageViewport} viewport
      * @returns {TextLayerBuilder}
      */
-    createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) {
+    createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport,
+                                      enhanceTextSelection) {
       return new TextLayerBuilder({
         textLayerDiv: textLayerDiv,
         eventBus: this.eventBus,
         pageIndex: pageIndex,
         viewport: viewport,
-        findController: this.isInPresentationMode ? null : this.findController
+        findController: this.isInPresentationMode ? null : this.findController,
+        enhanceTextSelection: this.isInPresentationMode ? false :
+                                                          enhanceTextSelection,
       });
     },