diff --git a/src/canvas.js b/src/canvas.js index 70dd65e3d..d771fa15e 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -60,7 +60,7 @@ var CanvasGraphics = (function canvasGraphics() { // if we execute longer then `kExecutionTime`. var kExecutionTimeCheck = 500; - function constructor(canvasCtx, objs) { + function constructor(canvasCtx, objs, textLayer, textScale) { this.ctx = canvasCtx; this.current = new CanvasExtraState(); this.stateStack = []; @@ -69,6 +69,8 @@ var CanvasGraphics = (function canvasGraphics() { this.xobjs = null; this.ScratchCanvas = ScratchCanvas; this.objs = objs; + this.textLayer = textLayer; + this.textScale = textScale; } var LINE_CAP_STYLES = ['butt', 'round', 'square']; @@ -95,6 +97,7 @@ var CanvasGraphics = (function canvasGraphics() { break; } this.ctx.scale(cw / mediaBox.width, ch / mediaBox.height); + this.textDivs = []; }, executeIRQueue: function canvasGraphicsExecuteIRQueue(codeIR, @@ -150,6 +153,17 @@ var CanvasGraphics = (function canvasGraphics() { endDrawing: function canvasGraphicsEndDrawing() { this.ctx.restore(); + + // Text selection-specific + var textLayer = this.textLayer; + var textDivs = this.textDivs; + for (var i = 0, length = textDivs.length; i < length; ++i) { + if (textDivs[i].dataset.textLength>1) { // avoid div by zero + textLayer.appendChild(textDivs[i]); + // Adjust div width to match canvas text width + textDivs[i].style.letterSpacing = ((textDivs[i].dataset.canvasWidth - textDivs[i].offsetWidth)/(textDivs[i].dataset.textLength-1)) + 'px'; + } + } }, // Graphics state @@ -414,6 +428,12 @@ var CanvasGraphics = (function canvasGraphics() { this.moveText(0, this.current.leading); }, showText: function canvasGraphicsShowText(text) { + function unicodeToChar(unicode) { + return (unicode >= 0x10000) ? + String.fromCharCode(0xD800 | ((unicode - 0x10000) >> 10), + 0xDC00 | (unicode & 0x3FF)) : String.fromCharCode(unicode); + }; + var ctx = this.ctx; var current = this.current; var font = current.font; @@ -423,6 +443,8 @@ var CanvasGraphics = (function canvasGraphics() { var wordSpacing = current.wordSpacing; var textHScale = current.textHScale; var glyphsLength = glyphs.length; + var text = { chars:'', width:0 }; + if (font.coded) { ctx.save(); ctx.transform.apply(ctx, current.textMatrix); @@ -446,11 +468,12 @@ var CanvasGraphics = (function canvasGraphics() { this.restore(); var transformed = Util.applyTransform([glyph.width, 0], fontMatrix); - var width = transformed[0] * fontSize + charSpacing; - - ctx.translate(width, 0); - current.x += width; + var charWidth = transformed[0] * fontSize + charSpacing; + ctx.translate(charWidth, 0); + current.x += charWidth; + text.chars += unicodeToChar(glyph.unicode); + text.width += charWidth; } ctx.restore(); } else { @@ -459,7 +482,6 @@ var CanvasGraphics = (function canvasGraphics() { ctx.scale(1, -1); ctx.translate(current.x, -1 * current.y); ctx.transform.apply(ctx, font.fontMatrix || IDENTITY_MATRIX); - ctx.scale(1 / textHScale, 1); var width = 0; @@ -471,36 +493,100 @@ var CanvasGraphics = (function canvasGraphics() { continue; } - var unicode = glyph.unicode; - var char = (unicode >= 0x10000) ? - String.fromCharCode(0xD800 | ((unicode - 0x10000) >> 10), - 0xDC00 | (unicode & 0x3FF)) : String.fromCharCode(unicode); - + var char = unicodeToChar(glyph.unicode); + var charWidth = glyph.width * fontSize * 0.001 + charSpacing; ctx.fillText(char, width, 0); - width += glyph.width * fontSize * 0.001 + charSpacing; + width += charWidth; + + text.chars += char; + text.width += charWidth; } current.x += width; - ctx.restore(); } + return text; }, - showSpacedText: function canvasGraphicsShowSpacedText(arr) { var ctx = this.ctx; var current = this.current; var fontSize = current.fontSize; var textHScale = current.textHScale; var arrLength = arr.length; + var textLayer = this.textLayer; + var font = current.font; + var text = {str:'', length:0, canvasWidth:0, spaceWidth:0, geom:{}}; + + // Text selection-specific + text.spaceWidth = this.current.font.charsToGlyphs(' ')[0].width; + if (!text.spaceWidth>0) { + // Hack (space is sometimes not encoded) + text.spaceWidth = this.current.font.charsToGlyphs('i')[0].width; + } + + // Compute text.geom + // TODO: refactor the series of transformations below, and share it with showText() + ctx.save(); + ctx.transform.apply(ctx, current.textMatrix); + ctx.scale(1, -1); + ctx.translate(current.x, -1 * current.y); + ctx.transform.apply(ctx, font.fontMatrix || IDENTITY_MATRIX); + ctx.scale(1 / textHScale, 1); + var inv = ctx.mozCurrentTransform; + if (inv) { + var bl = Util.applyTransform([0, 0], inv); + var tr = Util.applyTransform([1, 1], inv); + text.geom.x = bl[0]; + text.geom.y = bl[1]; + text.geom.xFactor = tr[0] - bl[0]; + text.geom.yFactor = tr[1] - bl[1]; + } + ctx.restore(); + for (var i = 0; i < arrLength; ++i) { var e = arr[i]; if (isNum(e)) { - current.x -= e * 0.001 * fontSize * textHScale; + var spacingLength = -e * 0.001 * fontSize * textHScale; + current.x += spacingLength; + + // Text selection-specific + // Emulate arbitrary spacing via HTML spaces + text.canvasWidth += spacingLength; + if (e<0 && text.spaceWidth>0) { // avoid div by zero + var numFakeSpaces = Math.round(-e / text.spaceWidth); + for (var j = 0; j < numFakeSpaces; ++j) + text.str += ' '; + text.length += numFakeSpaces>0 ? 1 : 0; + } } else if (isString(e)) { - this.showText(e); + var shownText = this.showText(e); + + // Text selection-specific + if (shownText.chars === ' ') { + text.str += ' '; + } else { + text.str += shownText.chars; + } + text.canvasWidth += shownText.width; + text.length += e.length; } else { malformed('TJ array element ' + e + ' is not string or num'); } } + + if (textLayer) { + var div = document.createElement('div'); + var fontHeight = text.geom.yFactor * fontSize; + div.style.fontSize = fontHeight + 'px'; + // TODO: family should be '= font.loadedName', but some fonts don't + // have spacing info (cf. fonts.js > Font > fields > htmx) + div.style.fontFamily = 'serif'; + div.style.left = text.geom.x + 'px'; + div.style.top = (text.geom.y - fontHeight) + 'px'; + div.innerHTML = text.str; + div.dataset.canvasWidth = text.canvasWidth * text.geom.xFactor; + div.dataset.textLength = text.length; + this.textDivs.push(div); + } }, nextLineShowText: function canvasGraphicsNextLineShowText(text) { this.nextLine(); diff --git a/src/core.js b/src/core.js index 4b411cff5..7e7bb6ea8 100644 --- a/src/core.js +++ b/src/core.js @@ -157,7 +157,7 @@ var Page = (function pagePage() { IRQueue, fonts) { var self = this; this.IRQueue = IRQueue; - var gfx = new CanvasGraphics(this.ctx, this.objs); + var gfx = new CanvasGraphics(this.ctx, this.objs, this.textLayer, this.textScale); var startTime = Date.now(); var displayContinuation = function pageDisplayContinuation() { @@ -243,6 +243,7 @@ var Page = (function pagePage() { startIdx = gfx.executeIRQueue(IRQueue, startIdx, next); if (startIdx == length) { self.stats.render = Date.now(); + gfx.endDrawing(); if (callback) callback(); } } @@ -305,9 +306,11 @@ var Page = (function pagePage() { } return links; }, - startRendering: function(ctx, callback) { + startRendering: function(ctx, callback, textLayer, textScale) { this.ctx = ctx; this.callback = callback; + this.textLayer = textLayer; + this.textScale = textScale; this.startRenderingTime = Date.now(); this.pdf.startRendering(this); diff --git a/web/viewer.css b/web/viewer.css index 52852d591..19895ac27 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -246,6 +246,12 @@ canvas { line-height:1.3; } +::selection { background:rgba(0,0,255,0.3); } +::-moz-selection { background:rgba(0,0,255,0.3); } +/* TODO: file FF bug to support ::-moz-selection:window-inactive + so we can override the opaque grey background when the window is inactive; + see also http://css-tricks.com/9288-window-inactive-styling */ + #viewer { margin: 44px 0px 0px; padding: 8px 0px; diff --git a/web/viewer.js b/web/viewer.js index 63215a6d8..523b7dc56 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -475,9 +475,9 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight, canvas.mozOpaque = true; div.appendChild(canvas); - var textDiv = document.createElement('div'); - textDiv.className = 'textLayer'; - div.appendChild(textDiv); + var textLayer = document.createElement('div'); + textLayer.className = 'textLayer'; + div.appendChild(textLayer); var scale = this.scale; canvas.width = pageWidth * scale; @@ -491,7 +491,7 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight, ctx.translate(-this.x * scale, -this.y * scale); stats.begin = Date.now(); - this.content.startRendering(ctx, this.updateStats, textDiv, scale); + this.content.startRendering(ctx, this.updateStats, textLayer, scale); setupLinks(this.content, this.scale); div.setAttribute('data-loaded', true);