diff --git a/extensions/b2g/viewer.js b/extensions/b2g/viewer.js index 94bb77caa..e93d503b5 100644 --- a/extensions/b2g/viewer.js +++ b/extensions/b2g/viewer.js @@ -88,7 +88,7 @@ var PDFViewerApplication = { setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) { this.url = url; - var title = PDFJS.getFileName(url) || url; + var title = PDFJS.getFilenameFromUrl(url) || url; try { title = decodeURIComponent(title); } catch (e) { diff --git a/extensions/firefox/content/PdfStreamConverter.jsm b/extensions/firefox/content/PdfStreamConverter.jsm index 4e3d5d299..a124b31fe 100644 --- a/extensions/firefox/content/PdfStreamConverter.jsm +++ b/extensions/firefox/content/PdfStreamConverter.jsm @@ -343,7 +343,7 @@ ChromeActions.prototype = { try { // contentDisposition/contentDispositionFilename is readonly before FF18 channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT; - if (self.contentDispositionFilename) { + if (self.contentDispositionFilename && !data.isAttachment) { channel.contentDispositionFilename = self.contentDispositionFilename; } else { channel.contentDispositionFilename = filename; diff --git a/src/core/annotation.js b/src/core/annotation.js index 1cc12b883..3ba5e32ea 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -50,6 +50,7 @@ var isName = corePrimitives.isName; var Stream = coreStream.Stream; var ColorSpace = coreColorSpace.ColorSpace; var ObjectLoader = coreObj.ObjectLoader; +var FileSpec = coreObj.FileSpec; var OperatorList = coreEvaluator.OperatorList; /** @@ -75,6 +76,7 @@ AnnotationFactory.prototype = /** @lends AnnotationFactory.prototype */ { // Return the right annotation object based on the subtype and field type. var parameters = { + xref: xref, dict: dict, ref: ref }; @@ -108,6 +110,9 @@ AnnotationFactory.prototype = /** @lends AnnotationFactory.prototype */ { case 'StrikeOut': return new StrikeOutAnnotation(parameters); + case 'FileAttachment': + return new FileAttachmentAnnotation(parameters); + default: warn('Unimplemented annotation type "' + subtype + '", ' + 'falling back to base annotation'); @@ -852,6 +857,31 @@ var StrikeOutAnnotation = (function StrikeOutAnnotationClosure() { return StrikeOutAnnotation; })(); +var FileAttachmentAnnotation = (function FileAttachmentAnnotationClosure() { + function FileAttachmentAnnotation(parameters) { + Annotation.call(this, parameters); + + var dict = parameters.dict; + var file = new FileSpec(dict.get('FS'), parameters.xref); + + this.data.annotationType = AnnotationType.FILEATTACHMENT; + this.data.file = file.serializable; + + if (!dict.has('C')) { + // Fall back to the default background color. + this.data.color = null; + } + + this.data.hasPopup = dict.has('Popup'); + this.data.title = stringToPDFString(dict.get('T') || ''); + this.data.contents = stringToPDFString(dict.get('Contents') || ''); + } + + Util.inherit(FileAttachmentAnnotation, Annotation, {}); + + return FileAttachmentAnnotation; +})(); + exports.Annotation = Annotation; exports.AnnotationBorderStyle = AnnotationBorderStyle; exports.AnnotationFactory = AnnotationFactory; diff --git a/src/core/obj.js b/src/core/obj.js index 5f41e37ec..830ed6c6c 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -1601,4 +1601,5 @@ var ObjectLoader = (function() { exports.Catalog = Catalog; exports.ObjectLoader = ObjectLoader; exports.XRef = XRef; +exports.FileSpec = FileSpec; })); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 2869e5279..303487b81 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -32,6 +32,7 @@ var AnnotationBorderStyleType = sharedUtil.AnnotationBorderStyleType; var AnnotationType = sharedUtil.AnnotationType; var Util = sharedUtil.Util; var addLinkAttributes = sharedUtil.addLinkAttributes; +var getFilenameFromUrl = sharedUtil.getFilenameFromUrl; var warn = sharedUtil.warn; var CustomStyle = displayDOMUtils.CustomStyle; @@ -42,6 +43,7 @@ var CustomStyle = displayDOMUtils.CustomStyle; * @property {PDFPage} page * @property {PageViewport} viewport * @property {IPDFLinkService} linkService + * @property {DownloadManager} downloadManager */ /** @@ -83,6 +85,9 @@ AnnotationElementFactory.prototype = case AnnotationType.STRIKEOUT: return new StrikeOutAnnotationElement(parameters); + case AnnotationType.FILEATTACHMENT: + return new FileAttachmentAnnotationElement(parameters); + default: return new AnnotationElement(parameters); } @@ -101,6 +106,7 @@ var AnnotationElement = (function AnnotationElementClosure() { this.page = parameters.page; this.viewport = parameters.viewport; this.linkService = parameters.linkService; + this.downloadManager = parameters.downloadManager; if (isRenderable) { this.container = this._createContainer(); @@ -721,6 +727,76 @@ var StrikeOutAnnotationElement = ( return StrikeOutAnnotationElement; })(); +/** + * @class + * @alias FileAttachmentAnnotationElement + */ +var FileAttachmentAnnotationElement = ( + function FileAttachmentAnnotationElementClosure() { + function FileAttachmentAnnotationElement(parameters) { + AnnotationElement.call(this, parameters, true); + + this.filename = getFilenameFromUrl(parameters.data.file.filename); + this.content = parameters.data.file.content; + } + + Util.inherit(FileAttachmentAnnotationElement, AnnotationElement, { + /** + * Render the file attachment annotation's HTML element in the empty + * container. + * + * @public + * @memberof FileAttachmentAnnotationElement + * @returns {HTMLSectionElement} + */ + render: function FileAttachmentAnnotationElement_render() { + this.container.className = 'fileAttachmentAnnotation'; + + var trigger = document.createElement('div'); + trigger.style.height = this.container.style.height; + trigger.style.width = this.container.style.width; + trigger.addEventListener('dblclick', this._download.bind(this)); + + if (!this.data.hasPopup && (this.data.title || this.data.contents)) { + var popupElement = new PopupElement({ + container: this.container, + trigger: trigger, + color: this.data.color, + title: this.data.title, + contents: this.data.contents, + hideWrapper: true + }); + var popup = popupElement.render(); + + // Position the popup next to the FileAttachment annotation's + // container. + popup.style.left = this.container.style.width; + + this.container.appendChild(popup); + } + + this.container.appendChild(trigger); + return this.container; + }, + + /** + * Download the file attachment associated with this annotation. + * + * @private + * @memberof FileAttachmentAnnotationElement + */ + _download: function FileAttachmentAnnotationElement_download() { + if (!this.downloadManager) { + warn('Download cannot be started due to unavailable download manager'); + return; + } + this.downloadManager.downloadData(this.content, this.filename, ''); + } + }); + + return FileAttachmentAnnotationElement; +})(); + /** * @typedef {Object} AnnotationLayerParameters * @property {PageViewport} viewport @@ -757,7 +833,8 @@ var AnnotationLayer = (function AnnotationLayerClosure() { layer: parameters.div, page: parameters.page, viewport: parameters.viewport, - linkService: parameters.linkService + linkService: parameters.linkService, + downloadManager: parameters.downloadManager }; var element = annotationElementFactory.create(properties); if (element.isRenderable) { diff --git a/src/shared/util.js b/src/shared/util.js index 3a6b4f2c6..2c5f96ea3 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -284,6 +284,17 @@ var UNSUPPORTED_FEATURES = PDFJS.UNSUPPORTED_FEATURES = { font: 'font' }; +// Gets the file name from a given URL. +function getFilenameFromUrl(url) { + var anchor = url.indexOf('#'); + var query = url.indexOf('?'); + var end = Math.min( + anchor > 0 ? anchor : url.length, + query > 0 ? query : url.length); + return url.substring(url.lastIndexOf('/', end) + 1, end); +} +PDFJS.getFilenameFromUrl = getFilenameFromUrl; + // Combines two URLs. The baseUrl shall be absolute URL. If the url is an // absolute URL, it will be returned as is. function combineUrl(baseUrl, url) { @@ -2367,6 +2378,7 @@ exports.combineUrl = combineUrl; exports.createPromiseCapability = createPromiseCapability; exports.deprecated = deprecated; exports.error = error; +exports.getFilenameFromUrl = getFilenameFromUrl; exports.getLookupTableFactory = getLookupTableFactory; exports.info = info; exports.isArray = isArray; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 911781e82..ec2d0568e 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -217,3 +217,4 @@ !annotation-strikeout.pdf !annotation-squiggly.pdf !annotation-highlight.pdf +!annotation-fileattachment.pdf diff --git a/test/pdfs/annotation-fileattachment.pdf b/test/pdfs/annotation-fileattachment.pdf new file mode 100644 index 000000000..c3b60cd7e Binary files /dev/null and b/test/pdfs/annotation-fileattachment.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 1bbc7f005..a6d4c5d09 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -2773,6 +2773,13 @@ "type": "eq", "annotations": true }, + { "id": "annotation-fileattachment", + "file": "pdfs/annotation-fileattachment.pdf", + "md5": "d20ecee4b53c81b2dd44c8715a1b4a83", + "rounds": 1, + "type": "eq", + "annotations": true + }, { "id": "issue6108", "file": "pdfs/issue6108.pdf", "md5": "8961cb55149495989a80bf0487e0f076", diff --git a/test/unit/annotation_layer_spec.js b/test/unit/annotation_layer_spec.js index d97861aba..df44e62a6 100644 --- a/test/unit/annotation_layer_spec.js +++ b/test/unit/annotation_layer_spec.js @@ -1,9 +1,25 @@ /* globals expect, it, describe, Dict, Name, Annotation, AnnotationBorderStyle, - AnnotationBorderStyleType, AnnotationFlag */ + AnnotationBorderStyleType, AnnotationFlag, PDFJS, combineUrl, + waitsFor, beforeEach, afterEach, stringToBytes */ 'use strict'; describe('Annotation layer', function() { + function waitsForPromiseResolved(promise, successCallback) { + var resolved = false; + promise.then(function(val) { + resolved = true; + successCallback(val); + }, + function(error) { + // Shouldn't get here. + expect(error).toEqual('the promise should not have been rejected'); + }); + waitsFor(function() { + return resolved; + }, 20000); + } + describe('Annotation', function() { it('should set and get flags', function() { var dict = new Dict(); @@ -172,4 +188,33 @@ describe('Annotation layer', function() { expect(borderStyle.verticalCornerRadius).toEqual(0); }); }); + + describe('FileAttachmentAnnotation', function() { + var loadingTask; + var annotations; + + beforeEach(function() { + var pdfUrl = combineUrl(window.location.href, + '../pdfs/annotation-fileattachment.pdf'); + loadingTask = PDFJS.getDocument(pdfUrl); + waitsForPromiseResolved(loadingTask.promise, function(pdfDocument) { + waitsForPromiseResolved(pdfDocument.getPage(1), function(pdfPage) { + waitsForPromiseResolved(pdfPage.getAnnotations(), + function (pdfAnnotations) { + annotations = pdfAnnotations; + }); + }); + }); + }); + + afterEach(function() { + loadingTask.destroy(); + }); + + it('should correctly parse a file attachment', function() { + var annotation = annotations[0]; + expect(annotation.file.filename).toEqual('Test.txt'); + expect(annotation.file.content).toEqual(stringToBytes('Test attachment')); + }); + }); }); diff --git a/test/unit/util_spec.js b/test/unit/util_spec.js index bf9cedf93..bd4a0f2a4 100644 --- a/test/unit/util_spec.js +++ b/test/unit/util_spec.js @@ -1,10 +1,25 @@ /* globals expect, it, describe, combineUrl, Dict, isDict, Name, PDFJS, stringToPDFString, isExternalLinkTargetSet, LinkTarget, - removeNullCharacters */ + removeNullCharacters, getFilenameFromUrl */ 'use strict'; describe('util', function() { + describe('getFilenameFromUrl', function() { + it('should get the filename from an absolute URL', function() { + var url = 'http://server.org/filename.pdf'; + var result = getFilenameFromUrl(url); + var expected = 'filename.pdf'; + expect(result).toEqual(expected); + }); + + it('should get the filename from a relative URL', function() { + var url = '../../filename.pdf'; + var result = getFilenameFromUrl(url); + var expected = 'filename.pdf'; + expect(result).toEqual(expected); + }); + }); describe('combineUrl', function() { it('absolute url with protocol stays as is', function() { diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 3c6b4e43d..03494430a 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -72,6 +72,7 @@ .annotationLayer .highlightAnnotation, .annotationLayer .underlineAnnotation, .annotationLayer .squigglyAnnotation, -.annotationLayer .strikeoutAnnotation { +.annotationLayer .strikeoutAnnotation, +.annotationLayer .fileAttachmentAnnotation { cursor: pointer; } diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index 3f5a32900..6d985b8cc 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -21,6 +21,7 @@ * @property {HTMLDivElement} pageDiv * @property {PDFPage} pdfPage * @property {IPDFLinkService} linkService + * @property {DownloadManager} downloadManager */ /** @@ -35,6 +36,7 @@ var AnnotationLayerBuilder = (function AnnotationLayerBuilderClosure() { this.pageDiv = options.pageDiv; this.pdfPage = options.pdfPage; this.linkService = options.linkService; + this.downloadManager = options.downloadManager; this.div = null; } @@ -59,7 +61,8 @@ var AnnotationLayerBuilder = (function AnnotationLayerBuilderClosure() { div: self.div, annotations: annotations, page: self.pdfPage, - linkService: self.linkService + linkService: self.linkService, + downloadManager: self.downloadManager }; if (self.div) { diff --git a/web/pdf_attachment_view.js b/web/pdf_attachment_view.js index 9ac37d441..a2757c637 100644 --- a/web/pdf_attachment_view.js +++ b/web/pdf_attachment_view.js @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals getFileName, PDFJS */ +/* globals PDFJS */ 'use strict'; @@ -84,7 +84,7 @@ var PDFAttachmentView = (function PDFAttachmentViewClosure() { for (var i = 0; i < attachmentsCount; i++) { var item = attachments[names[i]]; - var filename = getFileName(item.filename); + var filename = PDFJS.getFilenameFromUrl(item.filename); var div = document.createElement('div'); div.className = 'attachmentsItem'; var button = document.createElement('button'); diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 539113a40..f03a13512 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -13,8 +13,7 @@ * limitations under the License. */ /* globals RenderingStates, PDFJS, DEFAULT_SCALE, CSS_UNITS, getOutputScale, - TextLayerBuilder, AnnotationLayerBuilder, Promise, - approximateFraction, roundToDivide */ + TextLayerBuilder, Promise, approximateFraction, roundToDivide */ 'use strict'; diff --git a/web/pdf_viewer.component.js b/web/pdf_viewer.component.js index 99b450ee8..22af0e5f7 100644 --- a/web/pdf_viewer.component.js +++ b/web/pdf_viewer.component.js @@ -15,7 +15,7 @@ /*jshint globalstrict: false */ /* globals PDFJS, PDFViewer, PDFPageView, TextLayerBuilder, PDFLinkService, DefaultTextLayerFactory, AnnotationLayerBuilder, PDFHistory, - DefaultAnnotationLayerFactory, getFileName, ProgressBar */ + DefaultAnnotationLayerFactory, DownloadManager, ProgressBar */ // Initializing PDFJS global object (if still undefined) if (typeof PDFJS === 'undefined') { @@ -29,6 +29,7 @@ if (typeof PDFJS === 'undefined') { //#include pdf_link_service.js //#include pdf_viewer.js //#include pdf_history.js +//#include download_manager.js PDFJS.PDFViewer = PDFViewer; PDFJS.PDFPageView = PDFPageView; @@ -39,6 +40,6 @@ if (typeof PDFJS === 'undefined') { PDFJS.DefaultAnnotationLayerFactory = DefaultAnnotationLayerFactory; PDFJS.PDFHistory = PDFHistory; - PDFJS.getFileName = getFileName; + PDFJS.DownloadManager = DownloadManager; PDFJS.ProgressBar = ProgressBar; }).call((typeof window === 'undefined') ? this : window); diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 2584a0287..baf28551d 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -40,6 +40,8 @@ var DEFAULT_CACHE_SIZE = 10; * @property {HTMLDivElement} container - The container for the viewer element. * @property {HTMLDivElement} viewer - (optional) The viewer element. * @property {IPDFLinkService} linkService - The navigation/linking service. + * @property {DownloadManager} downloadManager - (optional) The download + * manager component. * @property {PDFRenderingQueue} renderingQueue - (optional) The rendering * queue object. * @property {boolean} removePageBorders - (optional) Removes the border shadow @@ -92,6 +94,7 @@ var PDFViewer = (function pdfViewer() { this.container = options.container; this.viewer = options.viewer || options.container.firstElementChild; this.linkService = options.linkService || new SimpleLinkService(); + this.downloadManager = options.downloadManager || null; this.removePageBorders = options.removePageBorders || false; this.defaultRenderingQueue = !options.renderingQueue; @@ -757,7 +760,8 @@ var PDFViewer = (function pdfViewer() { return new AnnotationLayerBuilder({ pageDiv: pageDiv, pdfPage: pdfPage, - linkService: this.linkService + linkService: this.linkService, + downloadManager: this.downloadManager }); }, diff --git a/web/ui_utils.js b/web/ui_utils.js index 9b681ba9f..5bb0592ad 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -23,15 +23,6 @@ var MAX_AUTO_SCALE = 1.25; var SCROLLBAR_PADDING = 40; var VERTICAL_PADDING = 5; -function getFileName(url) { - var anchor = url.indexOf('#'); - var query = url.indexOf('?'); - var end = Math.min( - anchor > 0 ? anchor : url.length, - query > 0 ? query : url.length); - return url.substring(url.lastIndexOf('/', end) + 1, end); -} - /** * Returns scale factor for the canvas. It makes sense for the HiDPI displays. * @return {Object} The object with horizontal (sx) and vertical (sy) diff --git a/web/viewer.js b/web/viewer.js index dc748025f..764c0af09 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -12,15 +12,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals PDFJS, PDFBug, FirefoxCom, Stats, ProgressBar, - DownloadManager, getFileName, getPDFFileNameFromURL, - PDFHistory, Preferences, SidebarView, ViewHistory, Stats, - PDFThumbnailViewer, URL, noContextMenuHandler, SecondaryToolbar, - PasswordPrompt, PDFPresentationMode, PDFDocumentProperties, HandTool, - Promise, PDFLinkService, PDFOutlineView, PDFAttachmentView, - OverlayManager, PDFFindController, PDFFindBar, PDFViewer, - PDFRenderingQueue, PresentationModeState, parseQueryString, - RenderingStates, UNKNOWN_SCALE, DEFAULT_SCALE_VALUE, +/* globals PDFJS, PDFBug, FirefoxCom, Stats, ProgressBar, DownloadManager, + getPDFFileNameFromURL, PDFHistory, Preferences, SidebarView, + ViewHistory, Stats, PDFThumbnailViewer, URL, noContextMenuHandler, + SecondaryToolbar, PasswordPrompt, PDFPresentationMode, + PDFDocumentProperties, HandTool, Promise, PDFLinkService, + PDFOutlineView, PDFAttachmentView, OverlayManager, + PDFFindController, PDFFindBar, PDFViewer, PDFRenderingQueue, + PresentationModeState, parseQueryString, RenderingStates, + UNKNOWN_SCALE, DEFAULT_SCALE_VALUE, IGNORE_CURRENT_POSITION_ON_ZOOM: true */ 'use strict'; @@ -134,7 +134,8 @@ var PDFViewerApplication = { container: container, viewer: viewer, renderingQueue: pdfRenderingQueue, - linkService: pdfLinkService + linkService: pdfLinkService, + downloadManager: new DownloadManager() }); pdfRenderingQueue.setViewer(this.pdfViewer); pdfLinkService.setViewer(this.pdfViewer); @@ -485,7 +486,7 @@ var PDFViewerApplication = { setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) { this.url = url; try { - this.setTitle(decodeURIComponent(getFileName(url)) || url); + this.setTitle(decodeURIComponent(PDFJS.getFilenameFromUrl(url)) || url); } catch (e) { // decodeURIComponent may throw URIError, // fall back to using the unprocessed url in that case