diff --git a/src/display/text_layer.js b/src/display/text_layer.js index 72e31d69f..0d8abe50b 100644 --- a/src/display/text_layer.js +++ b/src/display/text_layer.js @@ -56,7 +56,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); @@ -112,6 +113,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) { @@ -126,6 +155,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; } @@ -155,26 +185,292 @@ 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 rotation = textDiv.dataset.angle; - if (rotation) { - transform = 'rotate(' + rotation + 'deg) ' + transform; - } - if (transform) { - CustomStyle.setProp('transform' , textDiv, 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 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; + } + } + + // 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); + } + }); + } + /** * Text layer rendering task. * @@ -182,17 +478,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() { @@ -213,8 +515,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 @@ -226,7 +531,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); + } + } + }, }; @@ -240,7 +601,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/test/driver.js b/test/driver.js index 3fc21e513..153737557 100644 --- a/test/driver.js +++ b/test/driver.js @@ -64,7 +64,8 @@ var rasterizeTextLayer = (function rasterizeTextLayerClosure() { return textLayerStylePromise; } - function rasterizeTextLayer(ctx, viewport, textContent) { + function rasterizeTextLayer(ctx, viewport, textContent, + enhanceTextSelection) { return new Promise(function (resolve) { // Building SVG with size of the viewport. var svg = document.createElementNS(SVG_NS, 'svg:svg'); @@ -90,9 +91,11 @@ var rasterizeTextLayer = (function rasterizeTextLayerClosure() { var task = PDFJS.renderTextLayer({ textContent: textContent, container: div, - viewport: viewport + viewport: viewport, + enhanceTextSelection: enhanceTextSelection, }); Promise.all([stylePromise, task.promise]).then(function (results) { + task.expandTextDivs(true); style.textContent = results[0]; svg.appendChild(foreignObject); @@ -468,12 +471,13 @@ var Driver = (function DriverClosure() { var textLayerContext = textLayerCanvas.getContext('2d'); textLayerContext.clearRect(0, 0, textLayerCanvas.width, textLayerCanvas.height); + var enhanceText = !!task.enhance; // The text builder will draw its content on the test canvas initPromise = page.getTextContent({ normalizeWhitespace: true, }).then(function(textContent) { return rasterizeTextLayer(textLayerContext, viewport, - textContent); + textContent, enhanceText); }); } else { textLayerCanvas = null; diff --git a/test/test_manifest.json b/test/test_manifest.json index dc0498774..b68bdb561 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -23,6 +23,13 @@ "rounds": 1, "type": "text" }, + { "id": "tracemonkey-text-enhance", + "file": "pdfs/tracemonkey.pdf", + "md5": "9a192d8b1a7dc652a19835f6f08098bd", + "rounds": 1, + "enhance": true, + "type": "text" + }, { "id": "issue3925", "file": "pdfs/issue3925.pdf", "md5": "c5c895deecf7a7565393587e0d61be2b", @@ -331,12 +338,28 @@ "lastPage": 4, "type": "text" }, + { "id": "taro-text-enhance", + "file": "pdfs/TaroUTR50SortedList112.pdf", + "md5": "ce63eab622ff473a43f8a8de85ef8a46", + "link":true, + "rounds": 1, + "lastPage": 4, + "enhance": true, + "type": "text" + }, { "id": "rotated-text", "file": "pdfs/rotated.pdf", "md5": "aed187f53e969ccdcbab0bb4c59f9e46", "rounds": 1, "type": "text" }, + { "id": "rotated-text-enhance", + "file": "pdfs/rotated.pdf", + "md5": "aed187f53e969ccdcbab0bb4c59f9e46", + "rounds": 1, + "enhance": true, + "type": "text" + }, { "id": "issue3115", "file": "pdfs/issue3115r.pdf", diff --git a/web/app.js b/web/app.js index d309cb80b..a192bfbc7 100644 --- a/web/app.js +++ b/web/app.js @@ -101,6 +101,7 @@ var SCALE_SELECT_CONTAINER_PADDING = 8; var SCALE_SELECT_PADDING = 22; var PAGE_NUMBER_LOADING_INDICATOR = 'visiblePageIsLoading'; var DISABLE_AUTO_FETCH_LOADING_BAR_TIMEOUT = 5000; +var ENHANCE_TEXT_SELECTION = false; function configure(PDFJS) { PDFJS.imageResourcesPath = './images/'; @@ -209,7 +210,8 @@ var PDFViewerApplication = { eventBus: eventBus, renderingQueue: pdfRenderingQueue, linkService: pdfLinkService, - downloadManager: downloadManager + downloadManager: downloadManager, + enhanceTextSelection: ENHANCE_TEXT_SELECTION, }); pdfRenderingQueue.setViewer(this.pdfViewer); pdfLinkService.setViewer(this.pdfViewer); diff --git a/web/default_preferences.json b/web/default_preferences.json index af97bffd1..ebc1b4282 100644 --- a/web/default_preferences.json +++ b/web/default_preferences.json @@ -12,4 +12,4 @@ "disableTextLayer": false, "useOnlyCssZoom": false, "externalLinkTarget": 0 -} \ No newline at end of file +} diff --git a/web/interfaces.js b/web/interfaces.js index 94d958cde..933eddc3c 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -98,9 +98,11 @@ IPDFTextLayerFactory.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) {} }; /** diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 467b8c029..6ed32e0fc 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -50,6 +50,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`. */ /** @@ -69,6 +71,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; @@ -78,6 +81,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; @@ -395,9 +399,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; diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 8f3df5189..771f68805 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -76,6 +76,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`. */ /** @@ -127,6 +129,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) { @@ -352,7 +355,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); @@ -836,13 +840,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, }); }, diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 14c416a0b..1f91a9033 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -35,6 +35,8 @@ * @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. */ /** @@ -57,6 +59,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { this.textDivs = []; this.findController = options.findController || null; this.textLayerRenderTask = null; + this.enhanceTextSelection = options.enhanceTextSelection; this._bindMouse(); } @@ -64,9 +67,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, @@ -96,7 +101,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); @@ -314,7 +320,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; @@ -338,6 +349,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; @@ -362,13 +377,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 }); } };