diff --git a/LICENSE b/LICENSE index f8a848205..db52dec8e 100644 --- a/LICENSE +++ b/LICENSE @@ -9,6 +9,7 @@ Yury Delendik Kalervo Kujala Adil Allawi <@ironymark> + Jakob Miland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/README.md b/README.md index 7e5d2eeb3..da70a4f57 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pdf.js - + ## Overview diff --git a/examples/acroforms/forms.js b/examples/acroforms/forms.js new file mode 100644 index 000000000..6ec92766d --- /dev/null +++ b/examples/acroforms/forms.js @@ -0,0 +1,141 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +// +// Basic AcroForms input controls rendering +// + +'use strict'; + +var formFields = {}; + +function setupForm(div, content, scale) { + function bindInputItem(input, item) { + if (input.name in formFields) { + var value = formFields[input.name]; + if (input.type == 'checkbox') + input.checked = value; + else if (!input.type || input.type == 'text') + input.value = value; + } + input.onchange = function pageViewSetupInputOnBlur() { + if (input.type == 'checkbox') + formFields[input.name] = input.checked; + else if (!input.type || input.type == 'text') + formFields[input.name] = input.value; + }; + } + function createElementWithStyle(tagName, item) { + var element = document.createElement(tagName); + element.style.left = (item.x * scale) + 'px'; + element.style.top = (item.y * scale) + 'px'; + element.style.width = Math.ceil(item.width * scale) + 'px'; + element.style.height = Math.ceil(item.height * scale) + 'px'; + return element; + } + function assignFontStyle(element, item) { + var fontStyles = ''; + if ('fontSize' in item) + fontStyles += 'font-size: ' + Math.round(item.fontSize * scale) + 'px;'; + switch (item.textAlignment) { + case 0: + fontStyles += 'text-align: left;'; + break; + case 1: + fontStyles += 'text-align: center;'; + break; + case 2: + fontStyles += 'text-align: right;'; + break; + } + element.setAttribute('style', element.getAttribute('style') + fontStyles); + } + + var items = content.getAnnotations(); + for (var i = 0; i < items.length; i++) { + var item = items[i]; + switch (item.type) { + case 'Widget': + if (item.fieldType != 'Tx' && item.fieldType != 'Btn' && + item.fieldType != 'Ch') + break; + var inputDiv = createElementWithStyle('div', item); + inputDiv.className = 'inputHint'; + div.appendChild(inputDiv); + var input; + if (item.fieldType == 'Tx') { + input = createElementWithStyle('input', item); + } + if (item.fieldType == 'Btn') { + input = createElementWithStyle('input', item); + if (item.flags & 32768) { + input.type = 'radio'; + // radio button is not supported + } else if (item.flags & 65536) { + input.type = 'button'; + // pushbutton is not supported + } else { + input.type = 'checkbox'; + } + } + if (item.fieldType == 'Ch') { + input = createElementWithStyle('select', item); + // select box is not supported + } + input.className = 'inputControl'; + input.name = item.fullName; + input.title = item.alternativeText; + assignFontStyle(input, item); + bindInputItem(input, item); + div.appendChild(input); + break; + } + } +} + +function renderPage(div, pdf, pageNumber, callback) { + var page = pdf.getPage(pageNumber); + var scale = 1.5; + + var pageDisplayWidth = page.width * scale; + var pageDisplayHeight = page.height * scale; + + var pageDivHolder = document.createElement('div'); + pageDivHolder.className = 'pdfpage'; + pageDivHolder.style.width = pageDisplayWidth + 'px'; + pageDivHolder.style.height = pageDisplayHeight + 'px'; + div.appendChild(pageDivHolder); + + // Prepare canvas using PDF page dimensions + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + canvas.width = pageDisplayWidth; + canvas.height = pageDisplayHeight; + pageDivHolder.appendChild(canvas); + + + // Render PDF page into canvas context + page.startRendering(context, callback); + + // Prepare and populate form elements layer + var formDiv = document.createElement('div'); + pageDivHolder.appendChild(formDiv); + + setupForm(formDiv, page, scale); +} + +PDFJS.getPdf(pdfWithFormsPath, function getPdfForm(data) { + // Instantiate PDFDoc with PDF data + var pdf = new PDFJS.PDFDoc(data); + + // Rendering all pages starting from first + var viewer = document.getElementById('viewer'); + var pageNumber = 1; + renderPage(viewer, pdf, pageNumber++, function pageRenderingComplete() { + if (pageNumber > pdf.numPages) + return; // All pages rendered + // Continue rendering of the next page + renderPage(viewer, pdf, pageNumber++, pageRenderingComplete); + }); +}); + diff --git a/examples/acroforms/index.html b/examples/acroforms/index.html new file mode 100644 index 000000000..5fad4648a --- /dev/null +++ b/examples/acroforms/index.html @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/src/canvas.js b/src/canvas.js index cd49c88b1..00858c937 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -672,6 +672,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { ctx.translate(current.x, current.y); ctx.scale(textHScale, 1); + ctx.lineWidth /= current.textMatrix[0]; if (textSelection) { this.save(); @@ -708,6 +709,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { } else { ctx.save(); this.applyTextTransforms(); + ctx.lineWidth /= current.textMatrix[0] * fontMatrix[0]; + if (textSelection) text.geom = this.getTextGeometry(); diff --git a/src/colorspace.js b/src/colorspace.js index 8088ab7df..827fd2e19 100644 --- a/src/colorspace.js +++ b/src/colorspace.js @@ -225,7 +225,7 @@ var AlternateCS = (function AlternateCSClosure() { return base.getRgbBuffer(baseBuf, 8); }, isDefaultDecode: function altcs_isDefaultDecode(decodeMap) { - ColorSpace.isDefaultDecode(decodeMap, this.numComps); + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); } }; @@ -327,7 +327,7 @@ var DeviceGrayCS = (function DeviceGrayCSClosure() { return rgbBuf; }, isDefaultDecode: function graycs_isDefaultDecode(decodeMap) { - ColorSpace.isDefaultDecode(decodeMap, this.numComps); + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); } }; return DeviceGrayCS; @@ -354,7 +354,7 @@ var DeviceRgbCS = (function DeviceRgbCSClosure() { return rgbBuf; }, isDefaultDecode: function rgbcs_isDefaultDecode(decodeMap) { - ColorSpace.isDefaultDecode(decodeMap, this.numComps); + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); } }; return DeviceRgbCS; @@ -441,7 +441,7 @@ var DeviceCmykCS = (function DeviceCmykCSClosure() { return rgbBuf; }, isDefaultDecode: function cmykcs_isDefaultDecode(decodeMap) { - ColorSpace.isDefaultDecode(decodeMap, this.numComps); + return ColorSpace.isDefaultDecode(decodeMap, this.numComps); } }; diff --git a/src/core.js b/src/core.js index a6effd2bd..26e376327 100644 --- a/src/core.js +++ b/src/core.js @@ -274,46 +274,125 @@ var Page = (function PageClosure() { } }, getLinks: function pageGetLinks() { + var links = []; + var annotations = pageGetAnnotations(); + var i, n = annotations.length; + for (i = 0; i < n; ++i) { + if (annotations[i].type != 'Link') + continue; + links.push(annotations[i]); + } + return links; + }, + getAnnotations: function pageGetAnnotations() { var xref = this.xref; + function getInheritableProperty(annotation, name) { + var item = annotation; + while (item && !item.has(name)) { + item = xref.fetchIfRef(item.get('Parent')); + } + if (!item) + return null; + return item.get(name); + } + var annotations = xref.fetchIfRef(this.annotations) || []; var i, n = annotations.length; - var links = []; + var items = []; for (i = 0; i < n; ++i) { - var annotation = xref.fetch(annotations[i]); + var annotationRef = annotations[i]; + var annotation = xref.fetch(annotationRef); if (!isDict(annotation)) continue; var subtype = annotation.get('Subtype'); - if (!isName(subtype) || subtype.name != 'Link') + if (!isName(subtype)) continue; var rect = annotation.get('Rect'); var topLeftCorner = this.rotatePoint(rect[0], rect[1]); var bottomRightCorner = this.rotatePoint(rect[2], rect[3]); - var link = {}; - link.x = Math.min(topLeftCorner.x, bottomRightCorner.x); - link.y = Math.min(topLeftCorner.y, bottomRightCorner.y); - link.width = Math.abs(topLeftCorner.x - bottomRightCorner.x); - link.height = Math.abs(topLeftCorner.y - bottomRightCorner.y); - var a = this.xref.fetchIfRef(annotation.get('A')); - if (a) { - switch (a.get('S').name) { - case 'URI': - link.url = a.get('URI'); + var item = {}; + item.type = subtype.name; + item.x = Math.min(topLeftCorner.x, bottomRightCorner.x); + item.y = Math.min(topLeftCorner.y, bottomRightCorner.y); + item.width = Math.abs(topLeftCorner.x - bottomRightCorner.x); + item.height = Math.abs(topLeftCorner.y - bottomRightCorner.y); + switch (subtype.name) { + case 'Link': + var a = this.xref.fetchIfRef(annotation.get('A')); + if (a) { + switch (a.get('S').name) { + case 'URI': + item.url = a.get('URI'); + break; + case 'GoTo': + item.dest = a.get('D'); + break; + default: + TODO('other link types'); + } + } else if (annotation.has('Dest')) { + // simple destination link + var dest = annotation.get('Dest'); + item.dest = isName(dest) ? dest.name : dest; + } + break; + case 'Widget': + var fieldType = getInheritableProperty(annotation, 'FT'); + if (!isName(fieldType)) break; - case 'GoTo': - link.dest = a.get('D'); - break; - default: - TODO('other link types'); - } - } else if (annotation.has('Dest')) { - // simple destination link - var dest = annotation.get('Dest'); - link.dest = isName(dest) ? dest.name : dest; + item.fieldType = fieldType.name; + // Building the full field name by collecting the field and + // its ancestors 'T' properties and joining them using '.'. + var fieldName = []; + var namedItem = annotation, ref = annotationRef; + while (namedItem) { + var parentRef = namedItem.get('Parent'); + var parent = xref.fetchIfRef(parentRef); + var name = namedItem.get('T'); + if (name) + fieldName.unshift(stringToPDFString(name)); + else { + // The field name is absent, that means more than one field + // with the same name may exist. Replacing the empty name + // with the '`' plus index in the parent's 'Kids' array. + // This is not in the PDF spec but necessary to id the + // the input controls. + var kids = xref.fetchIfRef(parent.get('Kids')); + var j, jj; + for (j = 0, jj = kids.length; j < jj; j++) { + if (kids[j].num == ref.num && kids[j].gen == ref.gen) + break; + } + fieldName.unshift('`' + j); + } + namedItem = parent; + ref = parentRef; + } + item.fullName = fieldName.join('.'); + var alternativeText = stringToPDFString(annotation.get('TU') || ''); + item.alternativeText = alternativeText; + var da = getInheritableProperty(annotation, 'DA') || ''; + var m = /([\d\.]+)\sTf/.exec(da); + if (m) + item.fontSize = parseFloat(m[1]); + item.textAlignment = getInheritableProperty(annotation, 'Q'); + item.flags = getInheritableProperty(annotation, 'Ff') || 0; + break; + case 'Text': + var content = annotation.get('Contents'); + var title = annotation.get('T'); + item.content = stringToPDFString(content || ''); + item.title = stringToPDFString(title || ''); + item.name = annotation.get('Name').name; + break; + default: + TODO('unimplemented annotation type: ' + subtype.name); + break; } - links.push(link); + items.push(item); } - return links; + return items; }, startRendering: function pageStartRendering(ctx, callback, textLayer) { this.ctx = ctx; @@ -352,6 +431,7 @@ var PDFDocModel = (function PDFDocModelClosure() { assertWellFormed(stream.length > 0, 'stream must have data'); this.stream = stream; this.setup(); + this.acroForm = this.xref.fetchIfRef(this.catalog.catDict.get('AcroForm')); } function find(stream, needle, limit, backwards) { @@ -499,36 +579,35 @@ var PDFDoc = (function PDFDocClosure() { throw 'No PDFJS.workerSrc specified'; } - var worker; try { - worker = new Worker(workerSrc); - } catch (e) { // Some versions of FF can't create a worker on localhost, see: // https://bugzilla.mozilla.org/show_bug.cgi?id=683280 - globalScope.PDFJS.disableWorker = true; - this.setupFakeWorker(); + var worker = new Worker(workerSrc); + + var messageHandler = new MessageHandler('main', worker); + // Tell the worker the file it was created from. + messageHandler.send('workerSrc', workerSrc); + messageHandler.on('test', function pdfDocTest(supportTypedArray) { + if (supportTypedArray) { + this.worker = worker; + this.setupMessageHandler(messageHandler); + } else { + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); + } + }.bind(this)); + + var testObj = new Uint8Array(1); + // Some versions of Opera throw a DATA_CLONE_ERR on + // serializing the typed array. + messageHandler.send('test', testObj); return; - } - - var messageHandler = new MessageHandler('main', worker); - - // Tell the worker the file it was created from. - messageHandler.send('workerSrc', workerSrc); - - messageHandler.on('test', function pdfDocTest(supportTypedArray) { - if (supportTypedArray) { - this.worker = worker; - this.setupMessageHandler(messageHandler); - } else { - this.setupFakeWorker(); - } - }.bind(this)); - - var testObj = new Uint8Array(1); - messageHandler.send('test', testObj); - } else { - this.setupFakeWorker(); + } catch (e) {} } + // Either workers are disabled, not supported or have thrown an exception. + // Thus, we fallback to a faked worker. + globalScope.PDFJS.disableWorker = true; + this.setupFakeWorker(); } PDFDoc.prototype = { diff --git a/src/evaluator.js b/src/evaluator.js index 5c20c8660..2905565da 100644 --- a/src/evaluator.js +++ b/src/evaluator.js @@ -221,13 +221,15 @@ var PartialEvaluator = (function PartialEvaluatorClosure() { fn = 'paintImageXObject'; PDFImage.buildImage(function(imageObj) { + var drawWidth = imageObj.drawWidth; + var drawHeight = imageObj.drawHeight; var imgData = { - width: w, - height: h, - data: new Uint8Array(w * h * 4) + width: drawWidth, + height: drawHeight, + data: new Uint8Array(drawWidth * drawHeight * 4) }; var pixels = imgData.data; - imageObj.fillRgbaBuffer(pixels); + imageObj.fillRgbaBuffer(pixels, drawWidth, drawHeight); handler.send('obj', [objId, 'Image', imgData]); }, handler, xref, resources, image, inline); } diff --git a/src/image.js b/src/image.js index 22a9f9eb4..29bad4d8a 100644 --- a/src/image.js +++ b/src/image.js @@ -127,7 +127,56 @@ var PDFImage = (function PDFImageClosure() { smaskPromise.resolve(null); }; + /** + * Resize an image using the nearest neighbor algorithm. Currently only + * supports one and three component images. + * @param {TypedArray} pixels The original image with one component. + * @param {Number} bpc Number of bits per component. + * @param {Number} components Number of color components, 1 or 3 is supported. + * @param {Number} w1 Original width. + * @param {Number} h1 Original height. + * @param {Number} w2 New width. + * @param {Number} h2 New height. + * @return {TypedArray} Resized image data. + */ + PDFImage.resize = function resize(pixels, bpc, components, w1, h1, w2, h2) { + var length = w2 * h2 * components; + var temp = bpc <= 8 ? new Uint8Array(length) : + bpc <= 16 ? new Uint16Array(length) : new Uint32Array(length); + var xRatio = w1 / w2; + var yRatio = h1 / h2; + var px, py, newIndex, oldIndex; + for (var i = 0; i < h2; i++) { + for (var j = 0; j < w2; j++) { + px = Math.floor(j * xRatio); + py = Math.floor(i * yRatio); + newIndex = (i * w2) + j; + oldIndex = ((py * w1) + px); + if (components === 1) { + temp[newIndex] = pixels[oldIndex]; + } else if (components === 3) { + newIndex *= 3; + oldIndex *= 3; + temp[newIndex] = pixels[oldIndex]; + temp[newIndex + 1] = pixels[oldIndex + 1]; + temp[newIndex + 2] = pixels[oldIndex + 2]; + } + } + } + return temp; + }; + PDFImage.prototype = { + get drawWidth() { + if (!this.smask) + return this.width; + return Math.max(this.width, this.smask.width); + }, + get drawHeight() { + if (!this.smask) + return this.height; + return Math.max(this.height, this.smask.height); + }, getComponents: function getComponents(buffer) { var bpc = this.bpc; var needsDecode = this.needsDecode; @@ -216,22 +265,21 @@ var PDFImage = (function PDFImageClosure() { } return output; }, - getOpacity: function getOpacity() { + getOpacity: function getOpacity(width, height) { var smask = this.smask; - var width = this.width; - var height = this.height; - var buf = new Uint8Array(width * height); + var originalWidth = this.width; + var originalHeight = this.height; + var buf; if (smask) { var sw = smask.width; var sh = smask.height; - if (sw != this.width || sh != this.height) - error('smask dimensions do not match image dimensions: ' + sw + - ' != ' + this.width + ', ' + sh + ' != ' + this.height); - + buf = new Uint8Array(sw * sh); smask.fillGrayBuffer(buf); - return buf; + if (sw != width || sh != height) + buf = PDFImage.resize(buf, smask.bps, 1, sw, sh, width, height); } else { + buf = new Uint8Array(width * height); for (var i = 0, ii = width * height; i < ii; ++i) buf[i] = 255; } @@ -260,20 +308,23 @@ var PDFImage = (function PDFImageClosure() { } } }, - fillRgbaBuffer: function fillRgbaBuffer(buffer) { + fillRgbaBuffer: function fillRgbaBuffer(buffer, width, height) { var numComps = this.numComps; - var width = this.width; - var height = this.height; + var originalWidth = this.width; + var originalHeight = this.height; var bpc = this.bpc; // rows start at byte boundary; - var rowBytes = (width * numComps * bpc + 7) >> 3; - var imgArray = this.getImageBytes(height * rowBytes); + var rowBytes = (originalWidth * numComps * bpc + 7) >> 3; + var imgArray = this.getImageBytes(originalHeight * rowBytes); var comps = this.colorSpace.getRgbBuffer( this.getComponents(imgArray), bpc); + if (originalWidth != width || originalHeight != height) + comps = PDFImage.resize(comps, this.bpc, 3, originalWidth, + originalHeight, width, height); var compsPos = 0; - var opacity = this.getOpacity(); + var opacity = this.getOpacity(width, height); var opacityPos = 0; var length = width * height * 4; @@ -299,9 +350,10 @@ var PDFImage = (function PDFImageClosure() { var comps = this.getComponents(imgArray); var length = width * height; - + // we aren't using a colorspace so we need to scale the value + var scale = 255 / ((1 << bpc) - 1); for (var i = 0; i < length; ++i) - buffer[i] = comps[i]; + buffer[i] = (scale * comps[i]) | 0; }, getImageBytes: function getImageBytes(length) { this.image.reset(); diff --git a/src/obj.js b/src/obj.js index 2f7488a76..453014a91 100644 --- a/src/obj.js +++ b/src/obj.js @@ -22,6 +22,17 @@ var Cmd = (function CmdClosure() { Cmd.prototype = { }; + + var cmdCache = {}; + + Cmd.get = function cmdGet(cmd) { + var cmdValue = cmdCache[cmd]; + if (cmdValue) + return cmdValue; + + return cmdCache[cmd] = new Cmd(cmd); + }; + return Cmd; })(); diff --git a/src/parser.js b/src/parser.js index 695438379..e50b12b9b 100644 --- a/src/parser.js +++ b/src/parser.js @@ -157,7 +157,7 @@ var Parser = (function ParserClosure() { imageStream = this.filter(imageStream, dict, length); imageStream.parameters = dict; - this.buf2 = new Cmd('EI'); + this.buf2 = Cmd.get('EI'); this.shift(); return imageStream; @@ -496,14 +496,14 @@ var Lexer = (function LexerClosure() { // array punctuation case '[': case ']': - return new Cmd(ch); + return Cmd.get(ch); // hex string or dict punctuation case '<': ch = stream.lookChar(); if (ch == '<') { // dict punctuation stream.skip(); - return new Cmd('<<'); + return Cmd.get('<<'); } return this.getHexString(ch); // dict punctuation @@ -511,11 +511,11 @@ var Lexer = (function LexerClosure() { ch = stream.lookChar(); if (ch == '>') { stream.skip(); - return new Cmd('>>'); + return Cmd.get('>>'); } case '{': case '}': - return new Cmd(ch); + return Cmd.get(ch); // fall through case ')': error('Illegal character: ' + ch); @@ -538,7 +538,7 @@ var Lexer = (function LexerClosure() { return false; if (str == 'null') return null; - return new Cmd(str); + return Cmd.get(str); }, skipToNextLine: function lexerSkipToNextLine() { var stream = this.stream; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 3d636796d..956980782 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -20,5 +20,5 @@ !scan-bad.pdf !freeculture.pdf !issue918.pdf +!smaskdim.pdf !type4psfunc.pdf - diff --git a/test/pdfs/smaskdim.pdf b/test/pdfs/smaskdim.pdf new file mode 100755 index 000000000..1ca92c3ac Binary files /dev/null and b/test/pdfs/smaskdim.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 7469db678..5a1efd75d 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -17,12 +17,13 @@ "rounds": 1, "type": "load" }, - { "id": "intelisa-load", + { "id": "intelisa-eq", "file": "pdfs/intelisa.pdf", "md5": "f5712097d29287a97f1278839814f682", "link": true, + "pageLimit": 100, "rounds": 1, - "type": "load" + "type": "eq" }, { "id": "pdfspec-load", "file": "pdfs/pdf.pdf", @@ -361,5 +362,11 @@ "rounds": 1, "link": true, "type": "eq" + }, + { "id": "smaskdim", + "file": "pdfs/smaskdim.pdf", + "md5": "de80aeca7cbf79940189fd34d59671ee", + "rounds": 1, + "type": "eq" } ] diff --git a/web/compatibility.js b/web/compatibility.js index 7d1d72553..e4e2f2440 100644 --- a/web/compatibility.js +++ b/web/compatibility.js @@ -205,3 +205,15 @@ }); })(); +// HTMLElement dataset property +(function checkDatasetProperty() { + var div = document.createElement('div'); + if ('dataset' in div) + return; // dataset property exists + Object.defineProperty(HTMLElement.prototype, 'dataset', { + get: function htmlElementDatasetGetter() { + // adding dataset field to the actual object + return (this.dataset = {}); + } + }); +})(); diff --git a/web/images/check.svg b/web/images/check.svg new file mode 100644 index 000000000..e0e1590a9 --- /dev/null +++ b/web/images/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/images/comment.svg b/web/images/comment.svg new file mode 100644 index 000000000..84feef1c8 --- /dev/null +++ b/web/images/comment.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/viewer.css b/web/viewer.css index a1ef92810..0b64d9d86 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -247,6 +247,35 @@ canvas { line-height:1.3; } +.annotComment > div { + position: absolute; +} + +.annotComment > img { + position: absolute; +} + +.annotComment > img:hover { + cursor: pointer; + opacity: 0.7; +} + +.annotComment > div { + padding: 0.2em; + max-width: 20em; + background-color: #F1E47B; + box-shadow: 0px 2px 10px #333; + -moz-box-shadow: 0px 2px 10px #333; + -webkit-box-shadow: 0px 2px 10px #333; +} + +.annotComment > div > h1 { + font-weight: normal; + font-size: 1.2em; + border-bottom: 1px solid #000000; + margin: 0px; +} + /* 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 */ diff --git a/web/viewer.js b/web/viewer.js index daf0174ab..93b4e70c2 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -11,7 +11,7 @@ var kCssUnits = 96.0 / 72.0; var kScrollbarPadding = 40; var kMinScale = 0.25; var kMaxScale = 4.0; - +var kImageDirectory = './images/'; var Cache = function cacheCache(size) { var data = []; @@ -458,7 +458,7 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight, delete this.canvas; }; - function setupLinks(content, scale) { + function setupAnnotations(content, scale) { function bindLink(link, dest) { link.href = PDFView.getDestinationHash(dest); link.onclick = function pageViewSetupLinksOnclick() { @@ -467,18 +467,67 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight, return false; }; } + function createElementWithStyle(tagName, item) { + var element = document.createElement(tagName); + element.style.left = (Math.floor(item.x - view.x) * scale) + 'px'; + element.style.top = (Math.floor(item.y - view.y) * scale) + 'px'; + element.style.width = Math.ceil(item.width * scale) + 'px'; + element.style.height = Math.ceil(item.height * scale) + 'px'; + return element; + } + function createCommentAnnotation(type, item) { + var container = document.createElement('section'); + container.className = 'annotComment'; - var links = content.getLinks(); - for (var i = 0; i < links.length; i++) { - var link = document.createElement('a'); - link.style.left = (Math.floor(links[i].x - view.x) * scale) + 'px'; - link.style.top = (Math.floor(links[i].y - view.y) * scale) + 'px'; - link.style.width = Math.ceil(links[i].width * scale) + 'px'; - link.style.height = Math.ceil(links[i].height * scale) + 'px'; - link.href = links[i].url || ''; - if (!links[i].url) - bindLink(link, ('dest' in links[i]) ? links[i].dest : null); - div.appendChild(link); + var image = createElementWithStyle('img', item); + image.src = kImageDirectory + type.toLowerCase() + '.svg'; + var content = document.createElement('div'); + content.setAttribute('hidden', true); + var title = document.createElement('h1'); + var text = document.createElement('p'); + var offsetPos = Math.floor(item.x - view.x + item.width); + content.style.left = (offsetPos * scale) + 'px'; + content.style.top = (Math.floor(item.y - view.y) * scale) + 'px'; + title.textContent = item.title; + + if (!item.content) { + content.setAttribute('hidden', true); + } else { + text.innerHTML = item.content.replace('\n', '
'); + image.addEventListener('mouseover', function annotationImageOver() { + this.nextSibling.removeAttribute('hidden'); + }, false); + + image.addEventListener('mouseout', function annotationImageOut() { + this.nextSibling.setAttribute('hidden', true); + }, false); + } + + content.appendChild(title); + content.appendChild(text); + container.appendChild(image); + container.appendChild(content); + + return container; + } + + var items = content.getAnnotations(); + for (var i = 0; i < items.length; i++) { + var item = items[i]; + switch (item.type) { + case 'Link': + var link = createElementWithStyle('a', item); + link.href = item.url || ''; + if (!item.url) + bindLink(link, ('dest' in item) ? item.dest : null); + div.appendChild(link); + break; + case 'Text': + var comment = createCommentAnnotation(item.name, item); + if (comment) + div.appendChild(comment); + break; + } } } @@ -598,7 +647,7 @@ var PageView = function pageView(container, content, id, pageWidth, pageHeight, }).bind(this), textLayer ); - setupLinks(this.content, this.scale); + setupAnnotations(this.content, this.scale); div.setAttribute('data-loaded', true); return true; @@ -888,25 +937,49 @@ window.addEventListener('pagechange', function pagechange(evt) { }, true); window.addEventListener('keydown', function keydown(evt) { + if (evt.ctrlKey || evt.altKey || evt.shiftKey || evt.metaKey) + return; var curElement = document.activeElement; + if (curElement && curElement.tagName == 'INPUT') + return; var controlsElement = document.getElementById('controls'); while (curElement) { if (curElement === controlsElement) return; // ignoring if the 'controls' element is focused curElement = curElement.parentNode; } + var handled = false; switch (evt.keyCode) { case 61: // FF/Mac '=' case 107: // FF '+' and '=' case 187: // Chrome '+' PDFView.zoomIn(); + handled = true; break; case 109: // FF '-' case 189: // Chrome '-' PDFView.zoomOut(); + handled = true; break; case 48: // '0' PDFView.setScale(kDefaultScale, true); + handled = true; + break; + case 37: // left arrow + case 75: // 'k' + case 80: // 'p' + PDFView.page--; + handled = true; + break; + case 39: // right arrow + case 74: // 'j' + case 78: // 'n' + PDFView.page++; + handled = true; break; } + + if (handled) { + evt.preventDefault(); + } });