From 7a01cdddad11e84d1bfc90619c7bba56a8338ca6 Mon Sep 17 00:00:00 2001 From: Pdf Bot Date: Thu, 1 Sep 2016 16:46:35 +0100 Subject: [PATCH] PDF.js version 1.5.413 - See mozilla/pdf.js@6bb95e3129a58b5c0e2cb1440914f79eb984e498 --- bower.json | 2 +- build/pdf.combined.js | 404 +++++++++++++++++++++++++++++++++++++++--- build/pdf.js | 404 +++++++++++++++++++++++++++++++++++++++--- build/pdf.worker.js | 4 +- package.json | 2 +- web/pdf_viewer.js | 53 ++++-- 6 files changed, 811 insertions(+), 58 deletions(-) diff --git a/bower.json b/bower.json index fed6e4386..34d414ced 100644 --- a/bower.json +++ b/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" diff --git a/build/pdf.combined.js b/build/pdf.combined.js index 17950342b..ee2bb5cf2 100644 --- a/build/pdf.combined.js +++ b/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; } diff --git a/build/pdf.js b/build/pdf.js index 166352db8..54855192a 100644 --- a/build/pdf.js +++ b/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; } diff --git a/build/pdf.worker.js b/build/pdf.worker.js index 1a7f17791..55175b39c 100644 --- a/build/pdf.worker.js +++ b/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 ? diff --git a/package.json b/package.json index 25a8323dc..eca4841e5 100644 --- a/package.json +++ b/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": [ diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 5388b3b34..9869e8b7c 100644 --- a/web/pdf_viewer.js +++ b/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, }); },