使用 Ajax 和 PHP $_FILES 从 Canvas 元素发送图像

发布于 2024-12-07 19:33:18 字数 1474 浏览 0 评论 0原文

我需要能够将图像和一些表单字段从客户端画布元素发送到 PHP 脚本,最终以 $_POST 和 $_FILES 结束。当我像这样发送它时:

<script type="text/javascript">
var img = canvas.toDataURL("image/png");
...
ajax.setRequestHeader('Content-Type', "multipart/form-data; boundary=" + boundary_str);
var request_body = boundary + '\n' 
+ 'Content-Disposition: form-data; name="formfield"' + '\n' 
+ '\n' 
+ formfield + '\n' 
+ '\n' 
+ boundary + '\n'
+ 'Content-Disposition: form-data; name="async-upload"; filename="' 
+ "ajax_test64_2.png" + '"' + '\n' 
+ 'Content-Type: image/png' + '\n' 
+ '\n' 
+ img
+ '\n' 
+ boundary;
ajax.send(request_body);
</script>

$_POST 和 $_FILES 都返回填充,但 $_FILES 中的图像数据仍然需要像这样解码:

$loc = $_FILES['async-upload']['tmp_name'];
$file = fopen($loc, 'rb');
$contents = fread($file, filesize($loc));
fclose($file);
$filteredData=substr($contents, strpos($contents, ",")+1);
$unencodedData=base64_decode($filteredData);

...以便将其保存为可读的 PNG。这不是一个选项,因为我试图将图像传递给 Wordpress 的 media_handle_upload() 函数,该函数需要 $_FILES 的索引指向可读图像。我也无法相应地解码、保存和更改“tmp_name”,因为它违反了安全检查。

所以,我发现了这个: http://www.webtoolkit.info/javascript-base64.html 并尝试在客户端进行解码:

img_split = img.split(",",2)[1];
img_decoded = Base64.decode( img_split );

但由于某种原因,当它到达 PHP 时我仍然没有得到可读的文件。 所以问题是:“为什么?”或“我做错了什么?”或“这可能吗?” :-)

非常感谢任何帮助!

谢谢, 凯恩

I need to be able to send an image and some form fields from a client side canvas element to a PHP script, ending up in $_POST and $_FILES. When I send it like this:

<script type="text/javascript">
var img = canvas.toDataURL("image/png");
...
ajax.setRequestHeader('Content-Type', "multipart/form-data; boundary=" + boundary_str);
var request_body = boundary + '\n' 
+ 'Content-Disposition: form-data; name="formfield"' + '\n' 
+ '\n' 
+ formfield + '\n' 
+ '\n' 
+ boundary + '\n'
+ 'Content-Disposition: form-data; name="async-upload"; filename="' 
+ "ajax_test64_2.png" + '"' + '\n' 
+ 'Content-Type: image/png' + '\n' 
+ '\n' 
+ img
+ '\n' 
+ boundary;
ajax.send(request_body);
</script>

$_POST and $_FILES both come back populated, but the image data in $_FILES still needs decoding like this:

$loc = $_FILES['async-upload']['tmp_name'];
$file = fopen($loc, 'rb');
$contents = fread($file, filesize($loc));
fclose($file);
$filteredData=substr($contents, strpos($contents, ",")+1);
$unencodedData=base64_decode($filteredData);

...in order to save it as a readable PNG. This isn't an option as I'm trying to pass the image to Wordpress's media_handle_upload() function, which requires an index to $_FILES pointing to a readable image. I also can't decode, save and alter 'tmp_name' accordingly, as it falls foul of security checks.

So, I found this:
http://www.webtoolkit.info/javascript-base64.html
and tried to do the decode on the client side:

img_split = img.split(",",2)[1];
img_decoded = Base64.decode( img_split );

but for some reason I still don't end up with a readable file when it gets to the PHP.
So the question is: "Why?" or "What am I doing wrong?" or "Is this even possible?" :-)

Any help very much appreciated!

Thanks,
Kane

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

你在看孤独的风景 2024-12-14 19:33:18

不幸的是,如果没有一些中间编码,这在 JavaScript 中是不可能的。为了理解原因,我们假设您对数据进行了 Base64 解码并发布,就像您在示例中所描述的那样。有效 PHP 文件的前几行十六进制可能如下所示:

0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
0000010: 0000 0080 0000 0080 0806 0000 00c3 3e61  ..............>a

如果您查看上传的 PNG 文件的相同十六进制范围,它会如下所示:

0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
0000010: 0000 00c2 8000 0000 c280 0806 0000 00c3  ................

差异很微妙。比较第二行的第二列和第三列。在有效文件中,这四个字节是0x00 0x80 0x00 0x00。在您上传的文件中,同样的四个字节是 0x00 0xc2 0x80 0x00。为什么?

JavaScript 字符串是 UTF。这意味着任何 ASCII 二进制值 (0-127) 都用一个字节进行编码。但是,从 128 到 2047 的任何数字都会获得两个字节。上传文件中额外的 0xc2 是这种多字节编码的产物。如果您想确切了解发生这种情况的原因,可以阅读维基百科上的 UTF 编码的更多信息。

您无法阻止 JavaScript 字符串发生这种情况,因此您无法在不使用 base64 的情况下通过 AJAX 上传此二进制数据。

编辑:经过进一步挖掘,某些现代浏览器可以实现这一点。如果浏览器支持 XMLHttpRequest.prototype.sendAsBinary(Firefox 3 和 4),您可以使用它来发送图像,如下所示:

function postCanvasToURL(url, name, fn, canvas, type) {
  var data = canvas.toDataURL(type);
  data = data.replace('data:' + type + ';base64,', '');

  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  var boundary = 'ohaiimaboundary';
  xhr.setRequestHeader(
    'Content-Type', 'multipart/form-data; boundary=' + boundary);
  xhr.sendAsBinary([
    '--' + boundary,
    'Content-Disposition: form-data; name="' + name + '"; filename="' + fn + '"',
    'Content-Type: ' + type,
    '',
    atob(data),
    '--' + boundary + '--'
  ].join('\r\n'));
}

对于没有 sendAsBinary 的浏览器,但是有 Uint8Array (Chrome 和 WebKit),你可以像这样填充它:

if (XMLHttpRequest.prototype.sendAsBinary === undefined) {
  XMLHttpRequest.prototype.sendAsBinary = function(string) {
    var bytes = Array.prototype.map.call(string, function(c) {
      return c.charCodeAt(0) & 0xff;
    });
    this.send(new Uint8Array(bytes).buffer);
  };
}

Unfortunately, this isn't possible in JavaScript without some intermediate encoding. To understand why, let's assume you base64 decoded and posted the data, like you described in your example. The first few lines in hex of a valid PHP file might look like this:

0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
0000010: 0000 0080 0000 0080 0806 0000 00c3 3e61  ..............>a

If you looked at the same range of hex of your uploaded PNG file, it would look like this:

0000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
0000010: 0000 00c2 8000 0000 c280 0806 0000 00c3  ................

The differences are subtle. Compare the second and third columns of the second line. In the valid file, the four bytes are 0x00 0x80 0x00 0x00. In your uploaded file, the same four bytes are 0x00 0xc2 0x80 0x00. Why?

JavaScript strings are UTF. This means that any ASCII binary values (0-127) are encoded with one byte. However, anything from 128-2047 gets two bytes. That extra 0xc2 in the uploaded file is an artifact of this multibyte encoding. If you want to know exactly why this happens, you can read more about UTF encoding on Wikipedia.

You can't prevent this from happening with JavaScript strings, so you can't upload this binary data via AJAX without using base64.

EDIT: After some further digging, this is possible with some modern browsers. If a browser supports XMLHttpRequest.prototype.sendAsBinary (Firefox 3 and 4), you can use this to send the image, like so:

function postCanvasToURL(url, name, fn, canvas, type) {
  var data = canvas.toDataURL(type);
  data = data.replace('data:' + type + ';base64,', '');

  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  var boundary = 'ohaiimaboundary';
  xhr.setRequestHeader(
    'Content-Type', 'multipart/form-data; boundary=' + boundary);
  xhr.sendAsBinary([
    '--' + boundary,
    'Content-Disposition: form-data; name="' + name + '"; filename="' + fn + '"',
    'Content-Type: ' + type,
    '',
    atob(data),
    '--' + boundary + '--'
  ].join('\r\n'));
}

For browsers that don't have sendAsBinary, but do have Uint8Array (Chrome and WebKit), you can polyfill it like so:

if (XMLHttpRequest.prototype.sendAsBinary === undefined) {
  XMLHttpRequest.prototype.sendAsBinary = function(string) {
    var bytes = Array.prototype.map.call(string, function(c) {
      return c.charCodeAt(0) & 0xff;
    });
    this.send(new Uint8Array(bytes).buffer);
  };
}
荒芜了季节 2024-12-14 19:33:18

基于 Nathan 的出色回答,我能够找到它,以便它仍然通过 jQuery.ajax。只需将其添加到 ajax 请求中:

            xhr: function () {
                var myXHR = new XMLHttpRequest();
                if (myXHR.sendAsBinary == undefined) {
                    myXHR.legacySend = myXHR.send;
                    myXHR.sendAsBinary = function (string) {
                        var bytes = Array.prototype.map.call(string, function (c) {
                            return c.charCodeAt(0) & 0xff;
                        });
                        this.legacySend(new Uint8Array(bytes).buffer);
                    };
                }
                myXHR.send = myXHR.sendAsBinary;
                return myXHR;
            },

基本上,您只需返回一个被覆盖的 xhr 对象,以便“send”意味着“sendAsBinary”。那么 jQuery 就会做正确的事情。

Building on Nathan's excellent answer, I was able to finnagle it so that it is still going through jQuery.ajax. Just add this to the ajax request:

            xhr: function () {
                var myXHR = new XMLHttpRequest();
                if (myXHR.sendAsBinary == undefined) {
                    myXHR.legacySend = myXHR.send;
                    myXHR.sendAsBinary = function (string) {
                        var bytes = Array.prototype.map.call(string, function (c) {
                            return c.charCodeAt(0) & 0xff;
                        });
                        this.legacySend(new Uint8Array(bytes).buffer);
                    };
                }
                myXHR.send = myXHR.sendAsBinary;
                return myXHR;
            },

Basically, you just return back an xhr object that is overriden so that "send" means "sendAsBinary". Then jQuery does the right thing.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文