使Firefox和Chrome下载图片时以特定名称保存

20

给定 https://www.example.com/image-list

...
<a href="/image/1337">
  <img src="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"/>
</a>
<a href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png"
   download="1337 - Hello world!.png">
  Download
</a>
...

这是一个用户脚本环境,所以我无法访问服务器配置。因此:

  1. 我无法使服务器接受用户友好的文件名,如https://static.example.com/full/86fb269d190d2c85f6e0468ceca42a20 - 1337 - Hello World!.png
  2. 我无法配置跨域资源共享。由于设计上,www.example.comstatic.example.com之间被CORS隔离。

如何使Firefox和Chrome在用户单击“下载”链接时显示带有建议文件名"1337 - Hello world!.png" 的“保存文件为”对话框?

经过一些尝试和搜索,我了解到以下问题:

  1. Firefox完全忽略某些图像MIME类型上存在的download属性。
  2. Firefox完全忽略跨站点链接上存在的download属性。
  3. Chrome完全忽略跨站点链接上download属性的值。

对我来说,所有这些都毫无意义,看起来都像是“让我们对该功能施加随机荒谬的限制”,但由于我的环境,我不得不接受它们。

有解决问题的任何方法吗?


背景:我正在为一个使用MD5哈希作为文件名的图像板编写用户脚本。我希望更轻松地使用用户友好的文件名进行保存。任何能让我更接近这一目标的方法都会有所帮助。

我猜我可以通过使用对象URL到blob和具有黑客CORS头的本地代理来避开限制,但这个设置显然是不合理的。通过canvas进行保存可能有效(在这种情况下,图像是否也受CORS的保护?),但它将强制进行双重有损压缩或有损转无损转换,鉴于JPEG文件,这两者都不太好。


1
比原来的字符串更用户友好。提到这个字符串“md5-id-title.ext”,是因为如果我可以访问服务器配置,这将很容易实现。你需要做的就是让服务器忽略MD5哈希和文件扩展名之间的所有符号。实际上我正在考虑的是“artist-id.ext”(所涉及的图像板不提供标题或原始文件名),但这并不重要。 - Athari
1
@RokoC.Buljan 的帖子页面。包括图片、评论等等。这只是为了背景,只有第二个链接是重要的。 - Athari
1
“(在这种情况下,CORS是否也保护图像?)” - 是的,如果您将来自跨域来源的图像绘制到画布上,则画布将变得“受污染”,这将防止您将画布内容“导出”为新图像。(当然,远程服务器可以解除这些限制,但这似乎不是一个选项。)https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image - CBroe
@Athari,您提供的示例中域名为www.example.comstatic.example.com。您的环境是否类似于这种情况,即这两个域是同一父级超级域的子域(例如example.com)? - pseudosavant
自己注意:使用GreaseMonkey的GM.xmlHttpRequest绕过愚蠢的CORS。然后使用下面建议的其中一种方法下载文件(可能是blob)。 - Athari
显示剩余4条评论
6个回答

9

所有现代浏览器将忽略锚点中的下载属性,用于跨域URL。

参考文献: https://html.spec.whatwg.org/multipage/links.html#downloading-resources

根据规范制定者的说法,这代表了一个安全漏洞,因为用户可能会被欺骗而在浏览安全站点时下载恶意文件,误以为该文件也来自同一安全站点。

关于在Firefox浏览器中实现此功能的任何有趣对话都可以在此找到: https://bugzilla.mozilla.org/show_bug.cgi?id=676619


[由Athari编辑]

来自规范的引用:

这可能是危险的,因为例如,恶意服务器可能试图让用户不知情地下载私人信息,然后重新上传到恶意服务器,通过欺骗用户认为数据来自恶意服务器。

因此,对于用户来说,有必要告知用户所请求的资源来自不同的来源,并防止混淆,应忽略来自潜在敌对接口来源的任何建议文件名。

神秘场景的澄清:

CORS下载的更严重问题是,如果恶意站点强制从合法站点下载文件,并以某种方式获得其内容。例如,我下载用户gmail收件箱页面并查看其邮件。在这种情况下,恶意站点必须欺骗用户下载文件并将其上传回服务器。假设我们有一个gmail.com/inbox.html,其中包含所有用户邮件消息,攻击者站点提供一个优惠券文件的下载链接,应该上传到另一个恶意站点。该优惠券将声称为新iPad提供30%的折扣。下载链接实际上将指向gmail.com/inbox.html并将其下载为“30off.coupon”。如果用户下载此文件并在未检查其内容的情况下上传它,则恶意站点将获得用户的“优惠券”和其收件箱内容。
重要提示:
  1. Google最初没有通过CORS限制download属性,并明确反对此举。后来被迫调整Chrome的实现。

    Google反对使用CORS进行此操作。

  2. 曾提出过给用户一个关于跨域下载的警告的替代方案,但被忽略了。

    在从其他来源下载时可以有通知或允许/拒绝机制(例如,在地理位置API的情况下)。或者在具有下载属性的跨源请求中不发送cookie。

  3. 一些开发人员认为限制太强了,严重限制了该功能的使用,并且这种情况非常复杂,那些会这样做的用户很容易下载并运行可执行文件。他们的意见被忽视了。

    反对允许跨域下载的理由在于,[恶意]网站(例如discountipads.com)的访问者可能会不知不觉地从包含他们自己个人信息(例如gmail.com)的网站下载文件,并使用误导性的名称(例如“discount.coupon”)将其保存到他们的磁盘上,然后转到另一个恶意页面,在那里手动上传他们刚刚下载的同一文件。我认为这是非常牵强附会的,任何会被这种微不足道的诡计所迷惑的人也许根本不应该上网。我的意思是...点击此处下载我们的特别优惠,然后通过我们的特别表单重新上传它!认真的?下载我们的特别优惠,然后通过Yahoo地址将其发送到电子邮件!那些会上当的人甚至知道如何使用电子邮件附件吗?

    我完全支持浏览器安全,但如果Chromium的好人们对此没有问题,我不明白为什么Firefox必须彻底禁用它。至少我希望在about:config中有一个偏好设置,以启用“高级”用户的跨域@download(默认为false)。更好的做法是类似于确认框:“尽管此页面已加密,您通过此表单提交的信息不会被加密”或:“此页面正在请求安装插件”或:“从网络下载的文件可能会损坏您的计算机”甚至:“此页面的安全证书无效”......你知道我是什么意思吧?有无数种方法可以提高用户的意识并告知他们这可能不安全。一个额外的单击和短暂的(或长时间的?)延迟足以让他们评估风险。

    随着Web的发展,CDN的使用和高级Web应用程序的存在以及需要管理跨服务器托管的文件的需求增长,像@download这样的功能将变得越来越重要。当Chrome等浏览器完全支持它而Firefox不支持时,这对Firefox来说并不是一种胜利。

    简而言之,我认为通过简单地在跨域场景中忽略属性来减轻@download的潜在恶意用途是一个非常糟糕的举动。我并不是说风险完全不存在,相反:我是说人们在日常生活中在线上做的事情有很多风险...下载任何文件都是其中之一。为什么不通过经过深思熟虑的用户体验

    总体来说,考虑到CDN的广泛使用和有意将用户生成的内容放在不同的域名下,download属性的主要用途是为blob下载(URL.createObjectURL)等指定文件名。它不能在许多配置中使用,对用户脚本也没有太大用处。

如果您使用CDN来托管文件/图像(这是强烈推荐的做法),那么CORS方面的过度安全措施会使“下载”属性变得完全无用。这又是安全性摧毁用户体验的另一个案例。 - 3Dom

5
尝试以下方法:
1. 首先将外部图片下载到您的服务器上。 2. 从您的服务器返回已获取的图片。 3. 动态创建一个带有“download”名称和“.click()”的锚点! 虽然上面只是一个简短的提示列表...不过,可以试试这个方法: 在www.example.com上放置一个fetch-image.php文件,内容如下:
<?php
$url = $_GET["url"];                     // the image URL
$info = getimagesize($url);              // get image data
header("Content-type: ". $info['mime']); // act as image with right MIME type
readfile($url);                          // read binary image data
die();

或者使用任何其他能实现相同功能的服务器端语言。
上述代码应该返回任何外部图片,因为它们位于您的域名上。
在您的“images-list”页面上,您现在可以尝试以下操作:
<a 
  href="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png" 
  download="1337 - Hello world!.png">DOWNLOAD</a>

这是JS代码:
function fetchImageAndDownload (e) {
    e.preventDefault(); // Prevent default browser action 

    const url = this.getAttribute("href");       // Anchor href 
    const downloadName = this.download;          // Anchor download name
    
    const img = document.createElement("img");   // Create in-memory image
    img.addEventListener("load", () => {
        const a = document.createElement("a");   // Create in-memory anchor
        a.href = img.src;                        // href toward your server-image
        a.download = downloadName;               // :)
        a.click();                               // Trigger click (download)
    });
    img.src = 'fetch-image.php?url='+ url;       // Request image from your server
}

document.querySelectorAll("[download]").forEach((el) => {
    el.addEventListener("click", fetchImageAndDownload);
});

你应该最终看到下载的图片名为1337 - Hello world!.png,而不是像之前那样86fb269d190d2c85f6e0468ceca42a20.png
注意:我不确定同时向fetch-image.php发送请求的影响,请务必进行测试。

这个方法可以实现,但我不喜欢通过我的服务器代理所有的图片,因为这可能会产生大量的流量。 - Athari
非常好的发现!绝对没错。 - Roko C. Buljan

4
如果您可以访问后端和前端代码,以下步骤可能会对您有所帮助。
我不确定您使用的是哪种后端语言,因此我将仅解释需要做什么,而不提供代码示例。
在后端中,为了预览您的代码,如果您从查询字符串中获取到?download=true这样的内容,则您的后端应将文件打包为内容,换句话说,您会使用content-disposition响应头。 这将为您打开向内容添加其他属性的可能性,例如filename,因此它可能类似于此:
Content-Disposition: attachment; filename="filename.jpg"

现在,在前端,任何应该作为下载按钮的链接都需要包含?download=true查询参数和target="_blank"属性,这将在浏览器中临时打开另一个标签页以用于下载目的。
<a href="/image/1337">
  <img src="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"/>
</a>
<a href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png?download=true" target="_blank" title="1337 - Hello world!.png">
  Download Full size
</a>

我知道如果用户点击下载链接,这个功能可以在没有CORS设置的情况下正常工作,但我从未测试过浏览器中的“另存为”对话框......而且重新构建这个功能需要一些时间,所以请您试一试。


1
相关的Chromium API...

https://developer.chrome.com/extensions/downloads#event-onDeterminingFilename

示例...
chrome.downloads.onDeterminingFilename.addListener(function(item,suggest){

 suggest({filename:"downloads/"+item.filename}); // suggest only the folder

 suggest({filename:"downloads/image23.png"}); // suggest folder and filename

});

糟糕...您在服务器端,但我假设您在客户端!不过,如果有人需要,我会把这里留下。


1

你可以尝试这样做

var downloadHandler = function(){
    var url = this.dataset.url;
    var name = this.dataset.name;
    // by this you can automaticaly convert any supportable image type to other, it is destination image format
    var mime = this.dataset.type || 'image/jpg';
    var image = new Image();
    //We need image and canvas for converting url to blob.
    //Image is better then recieve blob through XHR request, because of crossOrigin mode
image.crossOrigin = "Anonymous";
   
    
    image.onload = function(oEvent) {
      //draw image on canvas
      var canvas = document.createElement('canvas');
      canvas.width = this.naturalWidth;
      canvas.height = this.naturalHeight;
      canvas.getContext('2d').drawImage(this, 0, 0, canvas.width, canvas.height);
      // get image from canvas as blob
      var binStr = atob( canvas.toDataURL(mime).split(',')[1] ),
            len = binStr.length,
            arr = new Uint8Array(len);

        for (var i = 0; i < len; i++ ) {
          arr[i] = binStr.charCodeAt(i);
        }

      var blob = new Blob( [arr], {type: mime} );
      //IE not works with a.click() for downloading
      if (window.navigator && window.navigator.msSaveOrOpenBlob)     {
      window.navigator.msSaveOrOpenBlob(blob, name);
      } else {
          var a = document.createElement("a");  
          a.href = URL.createObjectURL(blob);                     
          a.download = name;              
          a.click();  
      }
    };

    image.src = url;
}

document.querySelector("[download]").addEventListener("click", downloadHandler)
<button 
data-name="file.png" 
data-url="https://tpc.googlesyndication.com/simgad/14257743829768205599"
data-type="image/png"
download>
download
</button>

Another modern way for modern browsers (except Internet Explorer)
var downloadHandler = function(){
  var url = this.dataset.url;
  var name = this.dataset.name;
  fetch(url).then(function(response) {
    return response.blob();
  }).then(function(blob) {
    //IE and edge not works with a.click() for downloading
      if (window.navigator && window.navigator.msSaveOrOpenBlob)     {
      window.navigator.msSaveOrOpenBlob(blob, name);
      } else {
          var a = document.createElement("a");  
          a.href = URL.createObjectURL(blob);                     
          a.download = name;              
          a.click();  
      }
  });
};

document.querySelector("[download]").addEventListener("click", downloadHandler)
<button 
data-name="file.png" 
data-url="https://tpc.googlesyndication.com/simgad/14257743829768205599"
download>
download
</button>


1
Canvas + base64 会修改图片(在您的情况下,下载的文件比原始文件更大),并抹掉图像数据,您可能会耗尽物理画布像素。是的,如果这些都与 OP 相关,否则 - 看起来还可以。 - Roko C. Buljan
@roko-c-buljan 是的,你说得对:) 另一种方法是通过XHR请求接收图像并将其打包为blob。 - Alex Nikulin
5
这个解决方案意味着文件作为跨源兼容资源提供,这使得整个过程变得无用,因为如果是这样的话,直接使用“下载”即可。 - Kaiido
@Kaiido,我同意你的看法,我的帖子并没有完全满足要求。但是我会将这个帖子留在这里供其他人参考:)也许有人会觉得我的帖子有用。 - Alex Nikulin

-3

我能够使用“另存为”输入框重命名base64图像。

我认为你最好创建自己的“另存为”框。当用户点击“下载”时,显示一个“文件名:{输入框}”和一个保存按钮。让保存按钮更改文件名,然后调用下载函数。

function save2() {
    var gh = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAADNQTFRFAAAAPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09EvTTBAAAABB0Uk5TAA8fLz9PX29/j5+vv8/f7/rfIY4AAARsSURBVHja7VvZkusqDAyrAQvQ/3/teZjExrHDZoGrbl0/z0RtLY1oya/X/8+Nh/NHzYuAGPRz9llERET1GACJiIgYngaAkmvNnwTgERHtgwDefngawDofgDkAwPlp4I4AzHQA/gggstk0jF/P7ELQ3wBQPBoBRPRzTyI8P/bBGvh79FMstFXCvDSAt0kHzyBYNv7jj/iAx48DEiwzEfj0AFi/EEw4F+2B/5mfXQsbB4ZDQOKGwM2ioE+4hUdEm3jCjzybRbw4gIXkrxfbySnqCfYhS48rG23fs/wRGYdcGIQv1PsOcIgTkp//xTcs4WbyTEjs67pmFfh8+3+X1s0Jy3z7rxezaZ9EdTjI2MC1MpA37LqN65kjdoJuPmtUUpC40NmvLy2WntM3OcH09RupE8KdMLjefufgBE1gvz2blnj/2pDY7wikSPold9M+dCVSWpDuln1HUMLuCfsHEndP2H+9uO+kJEfVaicNq+zin9udxY6gQcrRlFeNHUG1oCfpjpIjAtmaukQXHRabpJwdMNlFSzZFdL3Dv4WkrlH4lyH6Y6jOgj0BSPUGWV0InrQAztISr2UgahFe1r3XJgHRC9C+qhK3CqC/4H6Sm1XV64ApCKt5NegOgFTGGGPMIlnhx22NA64zhUsppTxVMtcuvY5hcCqX31DhgAu+EgZ+WLjSjoPJvF6mBH5lIFvC7wHBJ7kAAAByjFdkAvdDg0o7/PPByiOCSSIvbfhBo6HExvES/ftwjOs7v7iyoZCl0qhMhHWpDQoX9QvH/xJd+osriAbr9ZktEQONCm3yAD5EEU833YWIlgsA1PD5UwGAGz4DLAAIw0eAeQBs/CTaZi2o8VNYyAIwP2qAHsCSZYGR6xD5xtgPTwGeBzB+I0Xlj+Oajo2kCEK+GRqfg2sWwEAaKhCNLDdsRCkgnwLg8kEeDyDmLQwHoAp3w+EA1kJPPBoAL6lEYnAZmuLtfCwRbToZLwEYNP7X5Vs33NEFuI15BS6U7+auuydmGkoKXI1Kt9RlIZPHIIllLbfzWwboCm2AF480b7WUQkipDWySkhPlg7ggU9apWPFqkWzV2TZC1Am1a1UMltMWW8F6Xve4qpRCX86U3ZQkcEtFF79UKtW8RSJnsvr+IDK7N23HRScH+mrtWQ/RCF3D+DYOaM337bOKftvQ78iKps3fjbDIrkeX22cVLqAKAovVFfD1DzRi/V4AgbWmDMW8ivmO7Qto9FlV/FvGr5xsZilj3/hXI00UTPcKi6PYgkrXR5qnb/72ZuRho03fSF5E1xOGg7qvb5VPz2akTmcbnT48LExDCysycxitdGfRcWUbar2gvj59cDfqyH3NoMpNyt+k5r77t1B+tb/eZNzJtTt1y+4umXM49b9g1AmFUPvloDdzqsppDweA/RuSOoDLv6D7GvRAKPUP5ceo3DWbX4nFXm5iy8ubEfqCWiut22HDDqZcyBuP6zL6s0euLVzbBqunfWbFpTZmhfdjjVFy9seO/6nnH0Mpp/3TjvofAAAAAElFTkSuQmCC"

    var a  = document.createElement('a');
    a.href = gh;
    a.download = document.getElementById("input").value;
   

    a.click()
    
}
Filename: <input id="input" name="input" placeholder="enter image name"></insput><button onclick="save2()">Save</button>

上述代码将允许您为用户命名文件。如果您希望用户能够自己命名文件,则需要创建一个输入字段,通过监听功能或onkeychange更新a.download ='imagetest.png';。我设法通过"a.download = document.getElementById("input").value;"使其正常工作。getElementByID的功能真是令人惊叹。

浏览器在为人们下载命名文件方面非常有限。我想这可能是被忽视的功能缺失。

如果您想更进一步,可以使用display:hidden;隐藏“文件名:”输入字段。并且您可以拥有一个“下载”按钮,具有onclick函数,该函数将“文件名:输入/保存”div设置为不再设置为display:hidden;。这也可以通过getElementById完成。



我注意到使用URL而不是base64重命名图像似乎存在问题。因此,我尝试将我的代码与您的集成,但在单击下载链接时,下面的任何代码都无法正常工作。我仍然认为它足够接近,如果这是您想要采取的路线,您可能希望调整一下:

<script>
    function save2() {
        var gh = "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"

        var a  = document.createElement('a');
        a.href = gh;
        a.download = document.getElementById("input").value;



        a.click()

    }

    function downloadit(){



               var x = document.getElementById("input").value;
                  document.getElementById("dl").getAttribute("download") = x;

    }





         document.getElementById("dl").onclick = function() {
    document.getElementById("dl").download= a.download = document.getElementById("input").value;
    return false;

    }
</script>

<a href="/image/1337">
  <img src="//static.example.com/thumbnails/86fb269d190d2c85f6e0468ceca42a20.png"/>
</a>
<a onclick='downloadit()' id="dl" href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png"
   download="1337 - Hello world!.png">
  Download
</a>

    Filename: <input id="input" name="input" placeholder="enter image name"></insput><button onclick="save2()">Save</button>



最后,我使用以下脚本成功地重命名了您的example.com图像。但是,当我尝试对一个有效的googleimage进行操作时,似乎无法重命名。因此,您可能也需要尝试一下这个。

        document.getElementById("dl").onclick = function() {
    document.getElementById("dl").download=document.getElementById("input").value;
   
        
    }
  Filename: <input id="input" name="input" placeholder="enter image name"></input>
<a id="dl" href="//static.example.com/full/86fb269d190d2c85f6e0468ceca42a20.png"
   download="1337 - Hello world!.png">
  Download
</a>

  


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接