commit
b958fd41d8
179
src/canvas.js
179
src/canvas.js
@ -174,7 +174,7 @@ var CanvasGraphics = (function canvasGraphics() {
|
|||||||
// before it stops and shedules a continue of execution.
|
// before it stops and shedules a continue of execution.
|
||||||
var kExecutionTime = 50;
|
var kExecutionTime = 50;
|
||||||
|
|
||||||
function constructor(canvasCtx, objs) {
|
function constructor(canvasCtx, objs, textLayer) {
|
||||||
this.ctx = canvasCtx;
|
this.ctx = canvasCtx;
|
||||||
this.current = new CanvasExtraState();
|
this.current = new CanvasExtraState();
|
||||||
this.stateStack = [];
|
this.stateStack = [];
|
||||||
@ -183,7 +183,7 @@ var CanvasGraphics = (function canvasGraphics() {
|
|||||||
this.xobjs = null;
|
this.xobjs = null;
|
||||||
this.ScratchCanvas = ScratchCanvas;
|
this.ScratchCanvas = ScratchCanvas;
|
||||||
this.objs = objs;
|
this.objs = objs;
|
||||||
|
this.textLayer = textLayer;
|
||||||
if (canvasCtx) {
|
if (canvasCtx) {
|
||||||
addContextCurrentTransform(canvasCtx);
|
addContextCurrentTransform(canvasCtx);
|
||||||
}
|
}
|
||||||
@ -212,7 +212,13 @@ var CanvasGraphics = (function canvasGraphics() {
|
|||||||
this.ctx.transform(0, -1, -1, 0, cw, ch);
|
this.ctx.transform(0, -1, -1, 0, cw, ch);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// Scale so that canvas units are the same as PDF user space units
|
||||||
this.ctx.scale(cw / mediaBox.width, ch / mediaBox.height);
|
this.ctx.scale(cw / mediaBox.width, ch / mediaBox.height);
|
||||||
|
this.textDivs = [];
|
||||||
|
this.textLayerQueue = [];
|
||||||
|
// Prevent textLayerQueue from being rendered while rendering a new page
|
||||||
|
if (this.textLayerTimer)
|
||||||
|
clearTimeout(this.textLayerTimer);
|
||||||
},
|
},
|
||||||
|
|
||||||
executeIRQueue: function canvasGraphicsExecuteIRQueue(codeIR,
|
executeIRQueue: function canvasGraphicsExecuteIRQueue(codeIR,
|
||||||
@ -270,6 +276,37 @@ var CanvasGraphics = (function canvasGraphics() {
|
|||||||
|
|
||||||
endDrawing: function canvasGraphicsEndDrawing() {
|
endDrawing: function canvasGraphicsEndDrawing() {
|
||||||
this.ctx.restore();
|
this.ctx.restore();
|
||||||
|
|
||||||
|
var textLayer = this.textLayer;
|
||||||
|
if (!textLayer)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var renderTextLayer = function canvasRenderTextLayer() {
|
||||||
|
var textDivs = self.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 (via letterSpacing) to match canvas text
|
||||||
|
// Due to the .offsetWidth calls, this is slow
|
||||||
|
textDivs[i].style.letterSpacing =
|
||||||
|
((textDivs[i].dataset.canvasWidth - textDivs[i].offsetWidth) /
|
||||||
|
(textDivs[i].dataset.textLength - 1)) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var textLayerQueue = this.textLayerQueue;
|
||||||
|
textLayerQueue.push(renderTextLayer);
|
||||||
|
|
||||||
|
// Lazy textLayer rendering (to prevent UI hangs)
|
||||||
|
// Only render queue if activity has stopped, where "no activity" ==
|
||||||
|
// "no beginDrawing() calls in the last N ms"
|
||||||
|
this.textLayerTimer = setTimeout(function renderTextLayerQueue() {
|
||||||
|
// Render most recent (==most relevant) layers first
|
||||||
|
for (var i = textLayerQueue.length - 1; i >= 0; i--) {
|
||||||
|
textLayerQueue.pop().call();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Graphics state
|
// Graphics state
|
||||||
@ -528,11 +565,68 @@ var CanvasGraphics = (function canvasGraphics() {
|
|||||||
nextLine: function canvasGraphicsNextLine() {
|
nextLine: function canvasGraphicsNextLine() {
|
||||||
this.moveText(0, this.current.leading);
|
this.moveText(0, this.current.leading);
|
||||||
},
|
},
|
||||||
showText: function canvasGraphicsShowText(text) {
|
applyTextTransforms: function canvasApplyTransforms() {
|
||||||
|
var ctx = this.ctx;
|
||||||
|
var current = this.current;
|
||||||
|
var textHScale = current.textHScale;
|
||||||
|
var fontMatrix = current.font.fontMatrix || IDENTITY_MATRIX;
|
||||||
|
|
||||||
|
ctx.transform.apply(ctx, current.textMatrix);
|
||||||
|
ctx.scale(1, -1);
|
||||||
|
ctx.translate(current.x, -1 * current.y);
|
||||||
|
ctx.transform.apply(ctx, fontMatrix);
|
||||||
|
ctx.scale(1 / textHScale, 1);
|
||||||
|
},
|
||||||
|
getTextGeometry: function canvasGetTextGeometry() {
|
||||||
|
var geometry = {};
|
||||||
|
var ctx = this.ctx;
|
||||||
|
var font = this.current.font;
|
||||||
|
var ctxMatrix = ctx.mozCurrentTransform;
|
||||||
|
if (ctxMatrix) {
|
||||||
|
var bl = Util.applyTransform([0, 0], ctxMatrix);
|
||||||
|
var tr = Util.applyTransform([1, 1], ctxMatrix);
|
||||||
|
geometry.x = bl[0];
|
||||||
|
geometry.y = bl[1];
|
||||||
|
geometry.hScale = tr[0] - bl[0];
|
||||||
|
geometry.vScale = tr[1] - bl[1];
|
||||||
|
}
|
||||||
|
var spaceGlyph = font.charsToGlyphs(' ');
|
||||||
|
|
||||||
|
// Hack (sometimes space is not encoded)
|
||||||
|
if (spaceGlyph.length === 0 || spaceGlyph[0].width === 0)
|
||||||
|
spaceGlyph = font.charsToGlyphs('i');
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
if (spaceGlyph.length === 0 || spaceGlyph[0].width === 0)
|
||||||
|
spaceGlyph = [{width: 0}];
|
||||||
|
|
||||||
|
geometry.spaceWidth = spaceGlyph[0].width;
|
||||||
|
return geometry;
|
||||||
|
},
|
||||||
|
|
||||||
|
pushTextDivs: function canvasGraphicsPushTextDivs(text) {
|
||||||
|
var div = document.createElement('div');
|
||||||
|
var fontSize = this.current.fontSize;
|
||||||
|
|
||||||
|
// vScale and hScale already contain the scaling to pixel units
|
||||||
|
// as mozCurrentTransform reflects ctx.scale() changes
|
||||||
|
// (see beginDrawing())
|
||||||
|
var fontHeight = fontSize * text.geom.vScale;
|
||||||
|
div.dataset.canvasWidth = text.canvasWidth * text.geom.hScale;
|
||||||
|
|
||||||
|
div.style.fontSize = fontHeight + 'px';
|
||||||
|
div.style.fontFamily = this.current.font.loadedName || 'sans-serif';
|
||||||
|
div.style.left = text.geom.x + 'px';
|
||||||
|
div.style.top = (text.geom.y - fontHeight) + 'px';
|
||||||
|
div.innerHTML = text.str;
|
||||||
|
div.dataset.textLength = text.length;
|
||||||
|
this.textDivs.push(div);
|
||||||
|
},
|
||||||
|
showText: function canvasGraphicsShowText(str, skipTextSelection) {
|
||||||
var ctx = this.ctx;
|
var ctx = this.ctx;
|
||||||
var current = this.current;
|
var current = this.current;
|
||||||
var font = current.font;
|
var font = current.font;
|
||||||
var glyphs = font.charsToGlyphs(text);
|
var glyphs = font.charsToGlyphs(str);
|
||||||
var fontSize = current.fontSize;
|
var fontSize = current.fontSize;
|
||||||
var charSpacing = current.charSpacing;
|
var charSpacing = current.charSpacing;
|
||||||
var wordSpacing = current.wordSpacing;
|
var wordSpacing = current.wordSpacing;
|
||||||
@ -540,6 +634,18 @@ var CanvasGraphics = (function canvasGraphics() {
|
|||||||
var fontMatrix = font.fontMatrix || IDENTITY_MATRIX;
|
var fontMatrix = font.fontMatrix || IDENTITY_MATRIX;
|
||||||
var textHScale2 = textHScale * fontMatrix[0];
|
var textHScale2 = textHScale * fontMatrix[0];
|
||||||
var glyphsLength = glyphs.length;
|
var glyphsLength = glyphs.length;
|
||||||
|
var textLayer = this.textLayer;
|
||||||
|
var text = {str: '', length: 0, canvasWidth: 0, geom: {}};
|
||||||
|
var textSelection = textLayer && !skipTextSelection ? true : false;
|
||||||
|
|
||||||
|
if (textSelection) {
|
||||||
|
ctx.save();
|
||||||
|
this.applyTextTransforms();
|
||||||
|
text.geom = this.getTextGeometry();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type3 fonts - each glyph is a "mini-PDF"
|
||||||
if (font.coded) {
|
if (font.coded) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.transform.apply(ctx, current.textMatrix);
|
ctx.transform.apply(ctx, current.textMatrix);
|
||||||
@ -567,16 +673,14 @@ var CanvasGraphics = (function canvasGraphics() {
|
|||||||
ctx.translate(width, 0);
|
ctx.translate(width, 0);
|
||||||
current.x += width * textHScale2;
|
current.x += width * textHScale2;
|
||||||
|
|
||||||
|
text.str += glyph.unicode;
|
||||||
|
text.length++;
|
||||||
|
text.canvasWidth += width;
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
} else {
|
} else {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.transform.apply(ctx, current.textMatrix);
|
this.applyTextTransforms();
|
||||||
ctx.scale(1, -1);
|
|
||||||
ctx.translate(current.x, -1 * current.y);
|
|
||||||
ctx.transform.apply(ctx, fontMatrix);
|
|
||||||
|
|
||||||
ctx.scale(1 / textHScale, 1);
|
|
||||||
|
|
||||||
var width = 0;
|
var width = 0;
|
||||||
for (var i = 0; i < glyphsLength; ++i) {
|
for (var i = 0; i < glyphsLength; ++i) {
|
||||||
@ -588,17 +692,23 @@ var CanvasGraphics = (function canvasGraphics() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var char = glyph.fontChar;
|
var char = glyph.fontChar;
|
||||||
|
var charWidth = glyph.width * fontSize * 0.001 + charSpacing;
|
||||||
ctx.fillText(char, width, 0);
|
ctx.fillText(char, width, 0);
|
||||||
width += glyph.width * fontSize * 0.001 + charSpacing;
|
width += charWidth;
|
||||||
|
|
||||||
// TODO actual characters can be extracted from the glyph.unicode
|
text.str += glyph.unicode === ' ' ? ' ' : glyph.unicode;
|
||||||
|
text.length++;
|
||||||
|
text.canvasWidth += charWidth;
|
||||||
}
|
}
|
||||||
current.x += width * textHScale2;
|
current.x += width * textHScale2;
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
|
if (textSelection)
|
||||||
|
this.pushTextDivs(text);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
},
|
||||||
showSpacedText: function canvasGraphicsShowSpacedText(arr) {
|
showSpacedText: function canvasGraphicsShowSpacedText(arr) {
|
||||||
var ctx = this.ctx;
|
var ctx = this.ctx;
|
||||||
var current = this.current;
|
var current = this.current;
|
||||||
@ -606,16 +716,53 @@ var CanvasGraphics = (function canvasGraphics() {
|
|||||||
var textHScale2 = current.textHScale *
|
var textHScale2 = current.textHScale *
|
||||||
(current.font.fontMatrix || IDENTITY_MATRIX)[0];
|
(current.font.fontMatrix || IDENTITY_MATRIX)[0];
|
||||||
var arrLength = arr.length;
|
var arrLength = arr.length;
|
||||||
|
var textLayer = this.textLayer;
|
||||||
|
var font = current.font;
|
||||||
|
var text = {str: '', length: 0, canvasWidth: 0, geom: {}};
|
||||||
|
var textSelection = textLayer ? true : false;
|
||||||
|
|
||||||
|
if (textSelection) {
|
||||||
|
ctx.save();
|
||||||
|
this.applyTextTransforms();
|
||||||
|
text.geom = this.getTextGeometry();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < arrLength; ++i) {
|
for (var i = 0; i < arrLength; ++i) {
|
||||||
var e = arr[i];
|
var e = arr[i];
|
||||||
if (isNum(e)) {
|
if (isNum(e)) {
|
||||||
current.x -= e * 0.001 * fontSize * textHScale2;
|
var spacingLength = -e * 0.001 * fontSize * textHScale2;
|
||||||
|
current.x += spacingLength;
|
||||||
|
|
||||||
|
if (textSelection) {
|
||||||
|
// Emulate precise spacing via HTML spaces
|
||||||
|
text.canvasWidth += spacingLength;
|
||||||
|
if (e < 0 && text.geom.spaceWidth > 0) { // avoid div by zero
|
||||||
|
var numFakeSpaces = Math.round(-e / text.geom.spaceWidth);
|
||||||
|
for (var j = 0; j < numFakeSpaces; ++j)
|
||||||
|
text.str += ' ';
|
||||||
|
text.length += numFakeSpaces > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (isString(e)) {
|
} else if (isString(e)) {
|
||||||
this.showText(e);
|
var shownText = this.showText(e, true);
|
||||||
|
|
||||||
|
if (textSelection) {
|
||||||
|
if (shownText.str === ' ') {
|
||||||
|
text.str += ' ';
|
||||||
|
} else {
|
||||||
|
text.str += shownText.str;
|
||||||
|
}
|
||||||
|
text.canvasWidth += shownText.canvasWidth;
|
||||||
|
text.length += e.length;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
malformed('TJ array element ' + e + ' is not string or num');
|
malformed('TJ array element ' + e + ' is not string or num');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (textSelection)
|
||||||
|
this.pushTextDivs(text);
|
||||||
},
|
},
|
||||||
nextLineShowText: function canvasGraphicsNextLineShowText(text) {
|
nextLineShowText: function canvasGraphicsNextLineShowText(text) {
|
||||||
this.nextLine();
|
this.nextLine();
|
||||||
|
@ -156,7 +156,7 @@ var Page = (function pagePage() {
|
|||||||
IRQueue, fonts) {
|
IRQueue, fonts) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.IRQueue = IRQueue;
|
this.IRQueue = IRQueue;
|
||||||
var gfx = new CanvasGraphics(this.ctx, this.objs);
|
var gfx = new CanvasGraphics(this.ctx, this.objs, this.textLayer);
|
||||||
|
|
||||||
var displayContinuation = function pageDisplayContinuation() {
|
var displayContinuation = function pageDisplayContinuation() {
|
||||||
// Always defer call to display() to work around bug in
|
// Always defer call to display() to work around bug in
|
||||||
@ -241,6 +241,7 @@ var Page = (function pagePage() {
|
|||||||
startIdx = gfx.executeIRQueue(IRQueue, startIdx, next);
|
startIdx = gfx.executeIRQueue(IRQueue, startIdx, next);
|
||||||
if (startIdx == length) {
|
if (startIdx == length) {
|
||||||
self.stats.render = Date.now();
|
self.stats.render = Date.now();
|
||||||
|
gfx.endDrawing();
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -303,9 +304,10 @@ var Page = (function pagePage() {
|
|||||||
}
|
}
|
||||||
return links;
|
return links;
|
||||||
},
|
},
|
||||||
startRendering: function pageStartRendering(ctx, callback) {
|
startRendering: function pageStartRendering(ctx, callback, textLayer) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
this.textLayer = textLayer;
|
||||||
|
|
||||||
this.startRenderingTime = Date.now();
|
this.startRenderingTime = Date.now();
|
||||||
this.pdf.startRendering(this);
|
this.pdf.startRendering(this);
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// The Metrics object contains glyph widths (in glyph space units).
|
||||||
|
// As per PDF spec, for most fonts (Type 3 being an exception) a glyph
|
||||||
|
// space unit corresponds to 1/1000th of text space unit.
|
||||||
var Metrics = {
|
var Metrics = {
|
||||||
'Courier': 600,
|
'Courier': 600,
|
||||||
'Courier-Bold': 600,
|
'Courier-Bold': 600,
|
||||||
|
@ -232,6 +232,27 @@ canvas {
|
|||||||
-webkit-box-shadow: 0px 2px 10px #ff0;
|
-webkit-box-shadow: 0px 2px 10px #ff0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textLayer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer > div {
|
||||||
|
color: transparent;
|
||||||
|
position: absolute;
|
||||||
|
line-height:1.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 https://bugzilla.mozilla.org/show_bug.cgi?id=706209 */
|
||||||
|
::selection { background:rgba(0,0,255,0.3); }
|
||||||
|
::-moz-selection { background:rgba(0,0,255,0.3); }
|
||||||
|
|
||||||
#viewer {
|
#viewer {
|
||||||
margin: 44px 0px 0px;
|
margin: 44px 0px 0px;
|
||||||
padding: 8px 0px;
|
padding: 8px 0px;
|
||||||
|
@ -489,6 +489,10 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight,
|
|||||||
div.appendChild(canvas);
|
div.appendChild(canvas);
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
|
|
||||||
|
var textLayer = document.createElement('div');
|
||||||
|
textLayer.className = 'textLayer';
|
||||||
|
div.appendChild(textLayer);
|
||||||
|
|
||||||
var scale = this.scale;
|
var scale = this.scale;
|
||||||
canvas.width = pageWidth * scale;
|
canvas.width = pageWidth * scale;
|
||||||
canvas.height = pageHeight * scale;
|
canvas.height = pageHeight * scale;
|
||||||
@ -505,7 +509,7 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight,
|
|||||||
this.updateStats();
|
this.updateStats();
|
||||||
if (this.onAfterDraw)
|
if (this.onAfterDraw)
|
||||||
this.onAfterDraw();
|
this.onAfterDraw();
|
||||||
}).bind(this));
|
}).bind(this), textLayer);
|
||||||
|
|
||||||
setupLinks(this.content, this.scale);
|
setupLinks(this.content, this.scale);
|
||||||
div.setAttribute('data-loaded', true);
|
div.setAttribute('data-loaded', true);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user