From 25ee0e8572f3b3498af6bb5b62fb407a5aba7a5f Mon Sep 17 00:00:00 2001 From: Samuel Chantaraud Date: Tue, 18 Mar 2014 16:32:47 -0400 Subject: [PATCH] Preliminary attachments support Added a partial Filespec support Added getAttachments in API Added a new attachments view in UI (with a new icon by @shorlander) --- .../firefox/content/PdfStreamConverter.jsm | 6 +- l10n/en-US/viewer.properties | 2 + l10n/fr/viewer.properties | 2 + src/core/obj.js | 115 ++++++++++++++++++ src/core/worker.js | 8 ++ src/display/api.js | 17 +++ web/download_manager.js | 7 ++ web/firefoxcom.js | 12 ++ web/images/toolbarButton-viewAttachments.png | Bin 0 -> 384 bytes .../toolbarButton-viewAttachments@2x.png | Bin 0 -> 871 bytes web/viewer.css | 26 +++- web/viewer.html | 55 +++++---- web/viewer.js | 80 +++++++++++- 13 files changed, 292 insertions(+), 38 deletions(-) create mode 100644 web/images/toolbarButton-viewAttachments.png create mode 100644 web/images/toolbarButton-viewAttachments@2x.png diff --git a/extensions/firefox/content/PdfStreamConverter.jsm b/extensions/firefox/content/PdfStreamConverter.jsm index 4a4611bb5..c0f2bf3b7 100644 --- a/extensions/firefox/content/PdfStreamConverter.jsm +++ b/extensions/firefox/content/PdfStreamConverter.jsm @@ -227,7 +227,8 @@ ChromeActions.prototype = { // the original url. var originalUri = NetUtil.newURI(data.originalUrl); var filename = data.filename; - if (typeof filename !== 'string' || !/\.pdf$/i.test(filename)) { + if (typeof filename !== 'string' || + (!/\.pdf$/i.test(filename) && !data.isAttachment)) { filename = 'document.pdf'; } var blobUri = data.blobUrl ? NetUtil.newURI(data.blobUrl) : originalUri; @@ -273,7 +274,8 @@ ChromeActions.prototype = { var listener = { extListener: null, onStartRequest: function(aRequest, aContext) { - this.extListener = extHelperAppSvc.doContent('application/pdf', + this.extListener = extHelperAppSvc.doContent((data.isAttachment ? '' : + 'application/pdf'), aRequest, frontWindow, false); this.extListener.onStartRequest(aRequest, aContext); }, diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index a10bb8f22..dadfe4143 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -89,6 +89,8 @@ toggle_sidebar.title=Toggle Sidebar toggle_sidebar_label=Toggle Sidebar outline.title=Show Document Outline outline_label=Document Outline +attachments.title=Show Attachments +attachments_label=Attachments thumbs.title=Show Thumbnails thumbs_label=Thumbnails findbar.title=Find in Document diff --git a/l10n/fr/viewer.properties b/l10n/fr/viewer.properties index 0bf3e1df4..41d1860cf 100644 --- a/l10n/fr/viewer.properties +++ b/l10n/fr/viewer.properties @@ -64,6 +64,8 @@ toggle_sidebar.title=Afficher/Masquer le panneau latéral toggle_sidebar_label=Afficher/Masquer le panneau latéral outline.title=Afficher les signets outline_label=Signets du document +attachments.title=Afficher les pièces jointes +attachments_label=Pièces jointes thumbs.title=Afficher les vignettes thumbs_label=Vignettes findbar.title=Rechercher dans le document diff --git a/src/core/obj.js b/src/core/obj.js index cd1cf70a9..f8c9752d8 100644 --- a/src/core/obj.js +++ b/src/core/obj.js @@ -454,6 +454,30 @@ var Catalog = (function CatalogClosure() { } return shadow(this, 'destinations', dests); }, + get attachments() { + var xref = this.xref; + var attachments, nameTreeRef; + var obj = this.catDict.get('Names'); + if (obj) { + nameTreeRef = obj.getRaw('EmbeddedFiles'); + } + + if (nameTreeRef) { + var nameTree = new NameTree(nameTreeRef, xref); + var names = nameTree.getAll(); + for (var name in names) { + if (!names.hasOwnProperty(name)) { + continue; + } + var fs = new FileSpec(names[name], xref); + if (!attachments) { + attachments = {}; + } + attachments[stringToPDFString(name)] = fs.serializable; + } + } + return shadow(this, 'attachments', attachments); + }, get javaScript() { var xref = this.xref; var obj = this.catDict.get('Names'); @@ -1315,6 +1339,97 @@ var NameTree = (function NameTreeClosure() { return NameTree; })(); +/** + * "A PDF file can refer to the contents of another file by using a File + * Specification (PDF 1.1)", see the spec (7.11) for more details. + * NOTE: Only embedded files are supported (as part of the attachments support) + * TODO: support the 'URL' file system (with caching if !/V), portable + * collections attributes and related files (/RF) + */ +var FileSpec = (function FileSpecClosure() { + function FileSpec(root, xref) { + if (!root || !isDict(root)) { + return; + } + this.xref = xref; + this.root = root; + if (root.has('FS')) { + this.fs = root.get('FS'); + } + this.description = root.has('Desc') ? + stringToPDFString(root.get('Desc')) : + ''; + if (root.has('RF')) { + warn('Related file specifications are not supported'); + } + this.contentAvailable = true; + if (!root.has('EF')) { + this.contentAvailable = false; + warn('Non-embedded file specifications are not supported'); + } + } + + function pickPlatformItem(dict) { + // Look for the filename in this order: + // UF, F, Unix, Mac, DOS + if (dict.has('UF')) { + return dict.get('UF'); + } else if (dict.has('F')) { + return dict.get('F'); + } else if (dict.has('Unix')) { + return dict.get('Unix'); + } else if (dict.has('Mac')) { + return dict.get('Mac'); + } else if (dict.has('DOS')) { + return dict.get('DOS'); + } else { + return null; + } + } + + FileSpec.prototype = { + get filename() { + if (!this._filename && this.root) { + var filename = pickPlatformItem(this.root) || 'unnamed'; + this._filename = stringToPDFString(filename). + replace(/\\\\/g, '\\'). + replace(/\\\//g, '/'). + replace(/\\/g, '/'); + } + return this._filename; + }, + get content() { + if (!this.contentAvailable) { + return null; + } + if (!this.contentRef && this.root) { + this.contentRef = pickPlatformItem(this.root.get('EF')); + } + var content = null; + if (this.contentRef) { + var xref = this.xref; + var fileObj = xref.fetchIfRef(this.contentRef); + if (fileObj && isStream(fileObj)) { + content = fileObj.getBytes(); + } else { + warn('Embedded file specification points to non-existing/invalid ' + + 'content'); + } + } else { + warn('Embedded file specification does not have a content'); + } + return content; + }, + get serializable() { + return { + filename: this.filename, + content: this.content + }; + } + }; + return FileSpec; +})(); + /** * A helper for loading missing data in object graphs. It traverses the graph * depth first and queues up any objects that have missing data. Once it has diff --git a/src/core/worker.js b/src/core/worker.js index 2b6115885..78a88f4ab 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -306,6 +306,14 @@ var WorkerMessageHandler = PDFJS.WorkerMessageHandler = { } ); + handler.on('GetAttachments', + function wphSetupGetAttachments(data, deferred) { + pdfManager.ensureCatalog('attachments').then(function(attachments) { + deferred.resolve(attachments); + }, deferred.reject); + } + ); + handler.on('GetData', function wphSetupGetData(data, deferred) { pdfManager.requestLoadedStream(); pdfManager.onLoadedStream().then(function(stream) { diff --git a/src/display/api.js b/src/display/api.js index fb82f5c8b..103285bfb 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -259,6 +259,13 @@ var PDFDocumentProxy = (function PDFDocumentProxyClosure() { getDestinations: function PDFDocumentProxy_getDestinations() { return this.transport.getDestinations(); }, + /** + * @return {Promise} A promise that is resolved with a lookup table for + * mapping named attachments to their content. + */ + getAttachments: function PDFDocumentProxy_getAttachments() { + return this.transport.getAttachments(); + }, /** * @return {Promise} A promise that is resolved with an array of all the * JavaScript strings in the name tree. @@ -1046,6 +1053,16 @@ var WorkerTransport = (function WorkerTransportClosure() { return promise; }, + getAttachments: function WorkerTransport_getAttachments() { + var promise = new PDFJS.LegacyPromise(); + this.messageHandler.send('GetAttachments', null, + function transportAttachments(attachments) { + promise.resolve(attachments); + } + ); + return promise; + }, + startCleanup: function WorkerTransport_startCleanup() { this.messageHandler.send('Cleanup', null, function endCleanup() { diff --git a/web/download_manager.js b/web/download_manager.js index eb455b862..20835aab6 100644 --- a/web/download_manager.js +++ b/web/download_manager.js @@ -66,6 +66,13 @@ var DownloadManager = (function DownloadManagerClosure() { download(url + '#pdfjs.action=download', filename); }, + downloadData: function DownloadManager_downloadData(data, filename, + contentType) { + + var blobUrl = PDFJS.createObjectURL(data, contentType); + download(blobUrl, filename); + }, + download: function DownloadManager_download(blob, url, filename) { if (!URL) { // URL.createObjectURL is not supported diff --git a/web/firefoxcom.js b/web/firefoxcom.js index 8ccb87570..4a5489354 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -83,6 +83,18 @@ var DownloadManager = (function DownloadManagerClosure() { }); }, + downloadData: function DownloadManager_downloadData(data, filename, + contentType) { + var blobUrl = PDFJS.createObjectURL(data, contentType); + + FirefoxCom.request('download', { + blobUrl: blobUrl, + originalUrl: blobUrl, + filename: filename, + isAttachment: true + }); + }, + download: function DownloadManager_download(blob, url, filename) { var blobUrl = window.URL.createObjectURL(blob); diff --git a/web/images/toolbarButton-viewAttachments.png b/web/images/toolbarButton-viewAttachments.png new file mode 100644 index 0000000000000000000000000000000000000000..fcd0b268a475662d421e9144764a09d20faf4155 GIT binary patch literal 384 zcmV-`0e}99P)4t*wuc`Tb2cq{&%P`@>A8<^(NL(>Ho}s&vtk+Fua<7E9$@Hp92jMpDMqu z1xm9(o%fjY|H;!pyNX@EE&si$I{ZV$w@o%SaA_c5db|2dDUjx3Fk~?LRQ7GXy(6+T z5KNBxck+Mrf8+l~|GU4fb8ui_Wk3UhZHZr2{6GKy)RPIe_L$OOB@D*mE*uUFn%Jcw efQf+-n=k;HAC|A!G+~AS0000kdg0009nNkl*n00Qixp6B9(Lgy*p~IIal_z=X^QudHJ1}^E~hK9-{v*5Qt#- z{wq$zD|}{GH?lE%s@NMj=*7nYb>AD_j2@`4U~2pZK7P|=?WfhWa$EeT3Rw9Hh&-bW zD8u?uo`(+us$E+sK$!9qNA}mjiannACYWKd9vtP?fqKUMeoc$2Ps8-R%me35Fq%um zp^WFkGEkSa*@?4|$HO?Jot$^0%Um2BA@|{OVft3P(}fdRlhI{Yy5}3w1-<2wgX227 z?aGN9-apEq*X6k926S2;hJ-VIjji%#jKd#?5Fy-oaTj42)FiKP!+Aa0Eky#q3WHrS z&iNu3zh*f&F2FF{d|@BC?nrFlJJD(?6nG9g?KO#UW1K5QhplRFIOFG`=iX%>pji{^ z-VU9qz`?m3bl9t%cmdjM&o@Skr{NqrIBnh`3UuYMd>h`IrNZi0&}Nq>jPnO*wLKH~ zS+sLaH;<5OCm;$f(3&M7;b%mwQCjRToVXM^TV?bJZ{;;N57Qi1KA+d1$#g#?oV*YX z-EUliW?Mx}6!~(QXyG-tk4@l2gQw8={YAt|@*=XGhvvcIU;I9_wsHr@N$8-ymqPR0 z)PAT6YQ-7ycHEmULEV%fa9{2on!Y?4<;}cGa-7_?FlM3wDoO0fsC+L}!%wjSVcZ=w z_B|H(320F-O(pjyfPNwBQ1@ub$S7d0{uVndbS$Y2pNj>aig$QhoIye4KGFhsC2Bjx zW8)cOX{s^be77=d(Hem_qP8@hf@#ta^+=)YOLlq^M9V0Q7Lo`l&_G$5;fb@TI)>MS z6_`FDJ1HzXuVtqxggl2h)K&r|mk+1y2_Ixa4TUU=Lg|m;m9}F@hFV^Mbk@$1Y+oMb zWvE9@)n9QTb16yvrKn-`XkbaxFs$HTmZbeH7wWfoU3q4&gqNcl)q|C~%exc)`0cLM xV0mA@ASQ16s;xe2X({ .toolbarButton { 0 0 1px hsla(0,0%,0%,.05); z-index: 199; } +.splitToolbarButton > .toolbarButton { + position: relative; +} html[dir='ltr'] .splitToolbarButton > .toolbarButton:first-child, html[dir='rtl'] .splitToolbarButton > .toolbarButton:last-child { position: relative; @@ -948,6 +951,10 @@ html[dir="rtl"] #viewOutline.toolbarButton::before { content: url(images/toolbarButton-viewOutline-rtl.png); } +#viewAttachments.toolbarButton::before { + content: url(images/toolbarButton-viewAttachments.png); +} + #viewFind.toolbarButton::before { content: url(images/toolbarButton-search.png); } @@ -1165,7 +1172,8 @@ a:focus > .thumbnail > .thumbnailSelectionRing, color: hsla(0,0%,100%,1); } -#outlineView { +#outlineView, +#attachmentsView { position: absolute; width: 192px; top: 0; @@ -1185,7 +1193,8 @@ html[dir='rtl'] .outlineItem > .outlineItems { margin-right: 20px; } -.outlineItem > a { +.outlineItem > a, +.attachmentsItem > a { text-decoration: none; display: inline-block; min-width: 95%; @@ -1199,15 +1208,18 @@ html[dir='rtl'] .outlineItem > .outlineItems { white-space: normal; } -html[dir='ltr'] .outlineItem > a { +html[dir='ltr'] .outlineItem > a, +html[dir='ltr'] .attachmentsItem > a { padding: 2px 0 5px 10px; } -html[dir='rtl'] .outlineItem > a { +html[dir='rtl'] .outlineItem > a, +html[dir='rtl'] .attachmentsItem > a { padding: 2px 10px 5px 0; } -.outlineItem > a:hover { +.outlineItem > a:hover, +.attachmentsItem > a:hover { background-color: hsla(0,0%,100%,.02); background-image: linear-gradient(hsla(0,0%,100%,.05), hsla(0,0%,100%,0)); background-clip: padding-box; @@ -1746,6 +1758,10 @@ html[dir='rtl'] #documentPropertiesContainer .row > * { content: url(images/toolbarButton-viewOutline-rtl@2x.png); } + #viewAttachments.toolbarButton::before { + content: url(images/toolbarButton-viewAttachments@2x.png); + } + #viewFind.toolbarButton::before { content: url(images/toolbarButton-search@2x.png); } diff --git a/web/viewer.html b/web/viewer.html index 6297668a3..5959ebac7 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -105,6 +105,9 @@ http://sourceforge.net/adobe/cmap/wiki/License/ +
@@ -112,6 +115,8 @@ http://sourceforge.net/adobe/cmap/wiki/License/
+ @@ -137,53 +142,53 @@ http://sourceforge.net/adobe/cmap/wiki/License/