Can I set the filename of a PDF object displayed in Chrome?
Asked Answered
S

4

51

In my Vue app I receive a PDF as a blob, and want to display it using the browser's PDF viewer.

I convert it to a file, and generate an object url:

const blobFile = new File([blob], `my-file-name.pdf`, { type: 'application/pdf' })
this.invoiceUrl = window.URL.createObjectURL(blobFile)

Then I display it by setting that URL as the data attribute of an object element.

<object
  :data="invoiceUrl"
  type="application/pdf"
  width="100%"
  style="height: 100vh;">
</object>

The browser then displays the PDF using the PDF viewer. However, in Chrome, the file name that I provide (here, my-file-name.pdf) is not used: I see a hash in the title bar of the PDF viewer, and when I download the file using either 'right click -> Save as...' or the viewer's controls, it saves the file with the blob's hash (cda675a6-10af-42f3-aa68-8795aa8c377d or similar).

The viewer and file name work as I'd hoped in Firefox; it's only Chrome in which the file name is not used.

Is there any way, using native Javascript (including ES6, but no 3rd party dependencies other than Vue), to set the filename for a blob / object element in Chrome?

[edit] If it helps, the response has the following relevant headers:

Content-Type: application/pdf; charset=utf-8
Transfer-Encoding: chunked
Content-Disposition: attachment; filename*=utf-8''Invoice%2016246.pdf;
Content-Description: File Transfer
Content-Encoding: gzip
Samoyed answered 29/11, 2018 at 21:55 Comment(3)
Do you receive the PDF by an HTTP Request from a particular server ?Tung
Yes, from my own server, via a GET request.Samoyed
can you add here how, you are fetching the file ? and did you try changing the Content-Disposition: inline; ?Tung
D
25

Chrome's extension seems to rely on the resource name set in the URI, i.e the file.ext in protocol://domain/path/file.ext.

So if your original URI contains that filename, the easiest might be to simply make your <object>'s data to the URI you fetched the pdf from directly, instead of going the Blob's way.

Now, there are cases it can't be done, and for these, there is a convoluted way, which might not work in future versions of Chrome, and probably not in other browsers, requiring to set up a Service Worker.

As we first said, Chrome parses the URI in search of a filename, so what we have to do, is to have an URI, with this filename, pointing to our blob:// URI.

To do so, we can use the Cache API, store our File as Request in there using our URL, and then retrieve that File from the Cache in the ServiceWorker.

Or in code,

From the main page

// register our ServiceWorker
navigator.serviceWorker.register('/sw.js')
  .then(...
...

async function displayRenamedPDF(file, filename) {
  // we use an hard-coded fake path
  // to not interfere with legit requests
  const reg_path = "/name-forcer/";
  const url = reg_path + filename;
  // store our File in the Cache
  const store = await caches.open( "name-forcer" );
  await store.put( url, new Response( file ) );

  const frame = document.createElement( "iframe" );
  frame.width = 400
  frame.height = 500;
  document.body.append( frame );
  // makes the request to the File we just cached
  frame.src = url;
  // not needed anymore
  frame.onload = (evt) => store.delete( url );
}

In the ServiceWorker sw.js

self.addEventListener('fetch', (event) => {
  event.respondWith( (async () => {
    const store = await caches.open("name-forcer");
    const req = event.request;
    const cached = await store.match( req );
    return cached || fetch( req );
  })() );
});

Live example (source)

Edit: This actually doesn't work in Chrome...

While it does set correctly the filename in the dialog, they seem to be unable to retrieve the file when saving it to the disk...
They don't seem to perform a Network request (and thus our SW isn't catching anything), and I don't really know where to look now.
Still this may be a good ground for future work on this.


And an other solution, I didn't took the time to check by myself, would be to run your own pdf viewer.

Mozilla has made its js based plugin pdf.js available, so from there we should be able to set the filename (even though once again I didn't dug there yet).


And as final note, Firefox is able to use the name property of a File Object a blobURI points to. So even though it's not what OP asked for, in FF all it requires is

const file = new File([blob], filename);
const url = URL.createObjectURL(file);
object.data = url;
Dioptric answered 3/12, 2018 at 12:1 Comment(7)
This is incredibly thorough--thank you. Knowing that service workers are an option is useful, and I think using pdf.js (or similar) might be the best solution for my case. It's also helpful to know that Chrome gets the file name from the URI (that makes sense, given the current behaviour).Samoyed
I implemented this solution. It works to display the PDF. But when clicking on the "Download" icon (either in Chrome or Edge), it says that the document doesn't exist. And I see no request going outside. Even by debugging the worker, the listener is not called. Of course, it works perfectly in Firefox. If someone knows why…Expediential
@AdrienClerc han you are right... I though I had a better solution which was working but actually, while it does set the filename in the dialog correctly it indeed fails to save the File on disk... They don't seem to make a new Request to the SW, so I don't know where it breaks...Dioptric
@Kaiido, I can't get your JS to work in FF. I'm a Java dev so maybe there is something I am missing but this is the code I'm trying in Javascript: let blob = new Blob([xml], {type: 'text/xml'}); let url = new URL(blob); window.open(url); This gives me this error: Uncaught TypeError: URL constructor: [object Blob] is not a valid URL. So I turned it into this: let blob = new Blob([xml], {type: 'text/xml'}); let url = URL.createObjectURL(blob); window.open(url); URL.revokeObjectURL(url); Which opens the xml in a new tab but with the normal GUID title.Bilabiate
@Bilabiate wow! thanks for noticing. That's a complete typo on my part, should have read const url = URL.createObjectURL(blob). It so obvious to me that even now I was still reading it like that in my answer without seeing what was actually there. Thanks for the heads up.Dioptric
@Dioptric thanks but it doesn't work though. I just get the normal blob title, i.e.: blob:localhost:8800/fdaceea9-e317-4af4-b2bf-e7db28e42044Bilabiate
@rbotel ... file, that should have been createObjectURL(file)Dioptric
R
18

In Chrome, the filename is derived from the URL, so as long as you are using a blob URL, the short answer is "No, you cannot set the filename of a PDF object displayed in Chrome." You have no control over the UUID assigned to the blob URL and no way to override that as the name of the page using the object element. It is possible that inside the PDF a title is specified, and that will appear in the PDF viewer as the document name, but you still get the hash name when downloading.

This appears to be a security precaution, but I cannot say for sure.

Of course, if you have control over the URL, you can easily set the PDF filename by changing the URL.

Rowlock answered 3/12, 2018 at 4:18 Comment(2)
Any changes at end of 2021 or still valid?Dustcloth
This is still true in 2022. you can set the download property of the anchor name to your file name to fix this but then the pdf will never open in the browserMelchizedek
C
3

Answer for 2023

The OP said their server was generating the file, but that doesn't align with the provided code snippets. For that reason, I'll include two answers here. One for people who want to override the filename of a file being sent from a server they control, and one for people who are generating and displaying a file on the client that they want to give a reasonable filename to.

Server-side filename override

This is relatively easy if you have control over the server. Just throw an extra HTTP header on the file when delivering it to the user:

Content-Disposition: inline; filename="filename.pdf"

When you send that header, the browser should use that filename as the default suggestion in the download/save dialog box. If you have non-ASCII characters in the filename, use this alternative syntax:

Content-Disposition: inline; filename*=UTF-8''l%C3%AAernaam.pdf

Client-side filename override

This is a frustrating problem for developers who want to generate a PDF inside the browser with something like pdf-lib while retaining control over the default filename offered to the user when they click the download/save button on the browser's built-in PDF viewer.

I wasted a bunch of time trying to get PDF.js to work in Svelte so it looks like the the default PDF viewer with just the download button overridden. I had one problem after the other. Everything seemed stacked against me and there were old Stack Overflow answers and blog posts casually mentioning their project that uses PDF.js to display PDFs. Things had changed (like the introduction of a top-level await that Svelte doesn't like and a change in philosophy about the release of the default viewer's source code). Was I really going to have to generate the files on the server just so I have control over the filename? That's not acceptable.

Overview

Thankfully, you it should be possible to solve this problem with the Cache API in combination with the Service Worker API. They are widely supported by modern browsers, so there shouldn't be any issues in terms of compatibility. IE doesn't support it, obviously, but that's not really worth mentioning.

Unfortunately, this won't work well in Chromium-based browsers because of a years-old bug

This technique allows you to leverage browser-based rendering while maintaining control over the filename with the caveat that Chromium will only download the file directly from the server. Please star the bug report if you want to see this fixed in Chromium.

Cache API

Basically, you add the document to a cache and assign it a URL on your server that is won't get in the way of any other files, then set an iframe's src to the URL once the cache finishes accepting it. That stores it where your service worker can find it and tells the browser to go looking for it there. Since you define the URL, you also control the filename (it's the last segment of the pathname).

Service Worker API

Then you create and register a service worker that checks the cache to see if the file exists before fetching it from the Internet. If it's in your cache, it's because you put it there, so don't worry about it getting confused with random files. When the browser goes looking for the file that you stored a moment ago, it'll find and return it as if you downloaded it off the Internet.

Conclusion

Service workers are easy to register in SvelteKit just by creating a file in the correct location, but it really doesn't take much to create and register one with any other framework. You just have to keep in mind that the browser will try to hang onto the service worker, forcing you to clear your site data when you want to make changes to it. So it's best to avoid putting code in there that might change frequently.

Chenille answered 20/11, 2023 at 2:23 Comment(0)
L
1

I believe Kaiido's answer expresses, briefly, the best solution here:

"if your original URI contains that filename, the easiest might be to simply make your object's data to the URI you fetched the pdf from directly"

Especially for those coming from this similar question, it would have helped me to have more description of a specific implementation (working for pdfs) that allows the best user experience, especially when serving files that are generated on the fly.

The trick here is using a two-step process that perfectly mimics a normal link or button click. The client must (step 1) request the file be generated and stored server-side long enough for the client to (step 2) request the file itself. This requires you have some mechanism supporting unique identification of the file on disk or in a cache.

Without this process, the user will just see a blank tab while file-generation is in-progress and if it fails, then they'll just get the browser's ERR_TIMED_OUT page. Even if it succeeds, they'll have a hash in the title bar of the PDF viewer tab, and the save dialog will have the same hash as the suggested filename.

Here's the play-by-play to do better:

  • You can use an anchor tag or a button for the "download" or "view in browser" elements

  • Step 1 of 2 on the client: that element's click event can make a request for the file to be generated only (not transmitted).

  • Step 1 of 2 on the server: generate the file and hold on to it. Return only the filename to the client.

  • Step 2 of 2 on the client:

    • If viewing the file in the browser, use the filename returned from the generate request to then invoke window.open('view_file/<filename>?fileId=1'). That is the only way to indirectly control the name of the file as shown in the tab title and in any subsequent save dialog.
    • If downloading, just invoke window.open('download_file?fileId=1').
  • Step 2 of 2 on the server:

    • view_file(filename, fileId) handler just needs to serve the file using the fileId and ignore the filename parameter. In .NET, you can use a FileContentResult like File(bytes, contentType);
    • download_file(fileId) must set the filename via the Content-Disposition header as shown here. In .NET, that's return File(bytes, contentType, desiredFilename);

client-side download example:

download_link_clicked() {
                // show spinner   
                ajaxGet(generate_file_url,
                    {},
                    (response) => {
                        // success!                        
                        // the server-side is responsible for setting the name 
                        // of the file when it is being downloaded                         
                        window.open('download_file?fileId=1', "_blank");
                        // hide spinner
                    },
                    () => { // failure
                       // hide spinner
                       // proglem, notify pattern
                    },
                    null
                );

client-side view example:

view_link_clicked() { 
                // show spinner
                ajaxGet(generate_file_url,
                    {},
                    (response) => {
                        // success!
                        let filename = response.filename;      
                        // simplest, reliable method I know of for controlling 
                        // the filename of the PDF when viewed in the browser
                        window.open('view_file/'+filename+'?fileId=1')
                        // hide spinner
                    },
                    () => { // failure
                       // hide spinner
                       // proglem, notify pattern
                    },
                    null
                );
Lowermost answered 27/4, 2022 at 17:0 Comment(1)
I think this question was more about Chrome PDF Viewer and setting the download filename there, but this could be a workaround. Too bad it requires a server-side component, I'd much prefer a front-end-only approach. I wonder if it's possible to ajaxGet a dynamically created URI in memory in a way that you could set filename? I personally had to switch to Blob because large image data URIs fail to load at all in the chrome PDF viewer at allUrquhart

© 2022 - 2024 — McMap. All rights reserved.