From 26217a5ad9e543df7c425f127e960a704e860edb Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Thu, 19 Nov 2015 10:20:11 -0600 Subject: [PATCH] Expanding divs to improve selection. --- web/text_layer_builder.css | 2 + web/text_layer_builder.js | 328 ++++++++++++++++++++++++++++++++++++- 2 files changed, 326 insertions(+), 4 deletions(-) diff --git a/web/text_layer_builder.css b/web/text_layer_builder.css index dd62aa6aec565..f7a358232e5e8 100644 --- a/web/text_layer_builder.css +++ b/web/text_layer_builder.css @@ -33,6 +33,8 @@ -o-transform-origin: 0% 0%; -ms-transform-origin: 0% 0%; transform-origin: 0% 0%; + + background-color: rgba(180, 0, 170, 0.3); } .textLayer .highlight { diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 0691c8c14a7af..c91d402b1dcdc 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -50,7 +50,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { this.viewport = options.viewport; this.textDivs = []; this.findController = options.findController || null; - this._bindMouse(); + // this._bindMouse(); } TextLayerBuilder.prototype = { @@ -69,6 +69,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { }, renderLayer: function TextLayerBuilder_renderLayer() { + console.time('renderLayer'); var textLayerFrag = document.createDocumentFragment(); var textDivs = this.textDivs; var textDivsLength = textDivs.length; @@ -82,6 +83,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { // unusable even after the divs are rendered. if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) { this._finishRendering(); + console.timeEnd('renderLayer'); return; } @@ -107,9 +109,10 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { if (width > 0) { textLayerFrag.appendChild(textDiv); var transform; + var textScale = 1; if (textDiv.dataset.canvasWidth !== undefined) { // Dataset values come of type string. - var textScale = textDiv.dataset.canvasWidth / width; + textScale = textDiv.dataset.canvasWidth / width; transform = 'scaleX(' + textScale + ')'; } else { transform = ''; @@ -118,6 +121,23 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { if (rotation) { transform = 'rotate(' + rotation + 'deg) ' + transform; } + if (textDiv.dataset.paddingLeft) { + textDiv.style.paddingLeft = + (textDiv.dataset.paddingLeft / textScale) + 'px'; + transform += ' translateX(' + + (-textDiv.dataset.paddingLeft / textScale) + 'px)'; + } + if (textDiv.dataset.paddingTop) { + textDiv.style.paddingTop = textDiv.dataset.paddingTop + 'px'; + transform += ' translateY(' + (-textDiv.dataset.paddingTop) + 'px)'; + } + if (textDiv.dataset.paddingRight) { + textDiv.style.paddingRight = + textDiv.dataset.paddingRight / textScale + 'px'; + } + if (textDiv.dataset.paddingBottom) { + textDiv.style.paddingBottom = textDiv.dataset.paddingBottom + 'px'; + } if (transform) { CustomStyle.setProp('transform' , textDiv, transform); } @@ -127,6 +147,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { this.textLayerDiv.appendChild(textLayerFrag); this._finishRendering(); this.updateMatches(); + console.timeEnd('renderLayer'); }, /** @@ -155,7 +176,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { } }, - appendText: function TextLayerBuilder_appendText(geom, styles) { + appendText: function TextLayerBuilder_appendText(geom, styles, bounds) { var style = styles[geom.fontName]; var textDiv = document.createElement('div'); this.textDivs.push(textDiv); @@ -211,15 +232,108 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { textDiv.dataset.canvasWidth = geom.width * this.viewport.scale; } } + + var angleCos = 1, angleSin = 0; + if (angle !== 0) { + angleCos = Math.cos(angle); + angleSin = Math.sin(angle); + } + var divWidth = (style.vertical ? geom.height : geom.width) * + this.viewport.scale; + var divHeight = fontHeight; + + var m, b; + if (angle !== 0) { + m = [angleCos, angleSin, -angleSin, angleCos, left, top]; + b = + PDFJS.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 + }); + }, + + expand: function (bounds) { + var expanded = expandBounds(this.viewport.width, this.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 = PDFJS.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; + } }, setTextContent: function TextLayerBuilder_setTextContent(textContent) { this.textContent = textContent; + var bounds = []; + console.time('setTextContent'); var textItems = textContent.items; for (var i = 0, len = textItems.length; i < len; i++) { - this.appendText(textItems[i], textContent.styles); + this.appendText(textItems[i], textContent.styles, bounds); } + this.expand(bounds); + console.timeEnd('setTextContent'); this.divContentDone = true; }, @@ -443,6 +557,212 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { }); }, }; + + 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); + } + }); + } + + 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 = []; + expanded.length = 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; + } + return TextLayerBuilder; })();