js获取图片base64

前言

最近遇到了一个需求,要求在python的selenium自动化打开的浏览器中,执行js代码,实现获取图片的base64,并返回给python变量。

看似很简单的一个需求,实际上包含了很多js相关的知识点和开发上的技术点。

今天这篇文章,主要写写整个过程中有用的知识点。

原始方法

其实最开始我没有考虑用js获取图片,所以用pyautogui键盘操作下载图片,代码如下:

import pyautogui
from selenium.webdriver import ActionChains

driver.get(url)
ActionChains(driver).move_to_element(driver.find_element_by_tag_name('img')).context_click().perform()
pyautogui.typewrite(['v'])
time.sleep(1)
pyautogui.typewrite(['enter'])

这里面,主要运用电脑上的快捷键,在选择图片后,右键,然后按v键,就会弹出保存窗口,再按enter,就可以按照默认信息保存。

这种方式是模拟手动,原理简单,但是,在多次重复这个操作时,会发现偶尔会出现一个bug,就是按v键会与保存冲突,使得电脑以为将图片名命名为v,然后整个过程就无法继续进行了。

为了解决上述问题,我才采用js的方法,才有了后面的内容。

第一版js代码

var source = document.getElementsByTagName('img')[0];
// 在网页元素中找到图片资源
var img=new Image();
// 创建一个image元素
img.src=source.src;
// 设置img的图片链接为网页图片链接
img.crossOrigin='anonymous';
// 设置跨域的配置,这个是比较常见的,如果不了解,读者可以自行百度
var xhr=new XMLHttpRequest();
// 初始化一个http请求
xhr.open('GET',source.src,false);
// 构造请求
xhr.send(null);
// 发送请求
xhr.open('GET',source.src,false); // 重复上面步骤,为了确定加载到资源
xhr.send(null);
var canvas = document.createElement('canvas');  
// 创建canvas元素
var ctx=canvas.getContext('2d');
// 获取canvas二维属性
ctx.drawImage(img,0,0);
// 将img画到canvas上
var dataURL = canvas.toDataURL('image/png');
// 利用canvas转换图片为base64形式
return dataURL;

上面的代码运行后,发现获取到的base64还原出的图片是黑色的,运行很多次都这样。所以我又在浏览器console下一条一条执行,结果发现能够给出正常的base64,但是在浏览器直接解析后,发现图片虽然能够正常显示,但是图片的大小是被裁剪的。

仔细思考后,我感觉可能是因为获取的图片的问题,但是看了network的加载记录,发现好像并不是这样,加载的图片都是完整的。所以,可能是canvas画图的时候出了一些问题,所以就去了解canvas的drawImage函数。

研究一番,发现,这个函数第一个参数是图片信息,第二和三个参数是画布的坐标,表示从哪个位置开始画,然后还可以有第四和五个参数,表示要画出的图的宽和高,所以我就修改了那句为

ctx.drawImage(img,0,0,source.naturalWidth,source.naturalHeight);
// 这里面的source是指网页的图片,然后naturalWidth是原宽,naturalHeight是原高

这里就把整个图画上去,但是经过实验后,我发现画出来的图还是裁剪后的图,而且,每次的图的大小都是一样,这就使我明白,会不会是画布的大小出了问题。

经过查阅资料,我发现canvas是有初始大小的,而且如果不改的话,是不会变的。所以,我又在画图前设置canvas的画布大小。

canvas.width=source.naturalWidth;
canvas.height=source.naturalHeight;

经过这次,我在浏览器端一条一条执行时,发现代码执行没问题,最后结果都正确。我以为第一天的工作结束了,解决了这个问题,明天简单弄弄就好了,但是一个更麻烦的问题在路上了。

注意:上面的在console下一条一条执行代码和在python中执行js代码的结果是不一样的。因为一条一条执行,中间是有时间消耗的,不好异步任务可以在此期间完成。

第二版js代码

var source = document.getElementsByTagName('img')[0];
// 在网页元素中找到图片资源
var img=new Image();
// 创建一个image元素
img.src=source.src;
// 设置img的图片链接为网页图片链接
img.crossOrigin='anonymous';
// 设置跨域的配置,这个是比较常见的,如果不了解,读者可以自行百度
var xhr=new XMLHttpRequest();
// 初始化一个http请求
xhr.open('GET',source.src,false);
// 构造请求
xhr.send(null);
// 发送请求
xhr.open('GET',source.src,false); // 重复上面步骤,为了确定加载到资源
xhr.send(null);
var canvas = document.createElement('canvas');  
// 创建canvas元素
canvas.width=source.naturalWidth;
// 设置画布宽
canvas.height=source.naturalHeight;
// 设置画布高
var ctx=canvas.getContext('2d');
// 获取canvas二维属性
ctx.drawImage(img,0,0,source.naturalWidth,source.naturalHeight);
// 将img画到canvas上
var dataURL = canvas.toDataURL('image/png');
// 利用canvas转换图片为base64形式
return dataURL;

上面代码是前面修改后、我以为正确的代码,但是,第二天放到python中运行后,发现结果还是不对,图片仍然是黑色的,不正确。

我说,难道是我保存的昨天的代码不对吗?所以还是重复错误的步骤,在浏览器下一条一条的运行。结果是正确的,所以我就很纳闷,一样的代码怎么会有不同的结果?

我不理解,但是我感觉可以试试将代码包装从函数的形式,在浏览器端运行。观察一下结果,发现结果给出的内容确实不是图片正常的base64,只能转为黑色的图片。

为了验证我的结果,我还运行了多次,结果都一样。我还将浏览器的正常结果、错误结果和python端的结果进行对比,我发现正确结果是很长的,而两个错误结果一样长度,都很短。

故事到这,陷入了僵局,我不知道该怎么办了。

正好到晚饭点了,所以就先去吃饭吧,吃饭的过程中,看看canvas的文档,了解一下具体的使用方法,说不定会有一些意外的收获。

在晚饭期间,我看到canvas画图的一些注意事项:若调用 drawImage 时,图片没装载完,那什么都不会发生(在一些旧的浏览器中可能会抛出异常)。因此你应该用 load 事件来保证不会在加载完毕之前使用这个图片。(来源于https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Using_images)

官方文档给出了案例:

var img = new Image();   // 创建 img 元素
img.src = 'myImage.png'; // 设置图片源地址
img.onload = function(){
  // 执行 drawImage 语句
}

我没有直接将这个作为解决方案,我又多看了一些论坛,发现大部分都是这样实现的。

后面,将这个代码加到我的代码中,我发现return语句在外面的话,还是会返回错值;return语句里面的话,是可以正常执行的,但是返回的值是无法返回到外层的。

于是,我灵机一动,直接return img.onload…

在浏览器下确实成功了,但是,在python这边,却还不对,返回的是一个函数对象。

到此,我明白,要等图片装载完成后,再执行drawimage语句,然后再返回才行。但是,js代码不会等,只要执行了就会继续往下执行下一条,不管你是否执行完成。

第三版js代码

为了解决加载完成的问题,我在想是不是有可以休眠等待的函数,查阅之后,发现可以自己构造一个sleep函数。

const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
sleep(1000); // 休眠1s

但是,加在上面代码后面,返回结果还是那样,感觉无法停止代码执行。

经过查阅资料,发现还有一个setTimeout函数,可以设置一段时间后再执行函数,但是输入值只能是函数,这就要求在函数里面返回最后的base64。但是,实际实验了一下,发现这个方式行不通,不返回内容。

然后,继续查阅资料,发现可以通过throw error抛出内容,我当然也尝试了,发现都是uncaught。我当时就纳闷,然后把setTimeout函数也换成了img.onload,但是结果都不对。

中间经过大量的实验,省略n次尝试。。。

最后,我明白了只有在try里面直接执行throw error代码,才能够catch到。至于利用try包裹etTimeout和img.load等耗时异步语句,都不能正常catch,因为正常执行到throw error时,已经结束了外面了try{}catch{}语句的执行,所以根本catch不到。

上面的一大堆过程,让我明白了,根本原因是在异步代码的执行完成前能否不继续执行下面的代码。

最终的js代码

var source = document.getElementsByTagName('img')[0];
var img=new Image();
img.src=source.src;
img.crossOrigin='anonymous';
var canvas = document.createElement('canvas');
canvas.width=source.naturalWidth;
canvas.height=source.naturalHeight;
var ctx=canvas.getContext('2d');
function httpPromise(){
   return new Promise((resolve,reject) => {
       if(img.complete || img.naturalHeight!==0)
       {
         ctx.drawImage(img,0,0,source.naturalWidth,source.naturalHeight);
         var dataURL = canvas.toDataURL('image/png');
         resolve(dataURL);
        }
        else{
         img.onload=function(){ 
                ctx.drawImage(img,0,0,source.naturalWidth,source.naturalHeight);
                var dataURL = canvas.toDataURL('image/png');
                resolve(dataURL);
               }
         }
    })
}
return httpPromise().then((dataURL)=>{return dataURL;}

最后这版代码做了一些改动,首先我删除了xhr加载的代码,因为这个加载过程和最后的结果没有关系,不影响最后的结果;然后,我添加了Promise异步处理机制,运用这个,就可以等待异步执行结束后,收集所有的结果,并且返回(我记得java也有类似的库可以解决异步等待的问题);最后,我添加了判断机制,防止img.onload因为缓存的原因不响应,所以分成了两种情况进行处理,但都是将结果收集,在return语句处返回。

插一嘴:这里需要return语句,是因为python里面的driver.excute_script函数是将我们的js代码包装成函数,需要返回内容进行处理,否则返回值就直接为None。

上面就是我通过自己摸索,实现的这样一个需求,虽然经过了很多磕磕绊绊,但是结果还是好的。其实,有一篇博客的代码我觉得是优于我的代码。

大佬的代码

// https://blog.csdn.net/yaxuan88521/article/details/122794055
let c = document.createElement('canvas');
let ctx = c.getContext('2d');
let img = document.getElementsByTagName('img')[0];
c.height=img.naturalHeight;
c.width=img.naturalWidth;
ctx.drawImage(img, 0, 0,img.naturalWidth, img.naturalHeight);
let base64String = c.toDataURL();
return base64String;

上面大佬的代码,是直接获取加载后的图片元素,效果是优于我的代码,毕竟减少了一次加载的时间消耗。

总结

本文通过笔者在js获取图片base64的过程中,遇到的问题。
总的说来,可以分为以下几点:

  1. canvas绘图需要修改画布大小,否则无法画完整。
  2. canvas的drawimage函数,需要等待图片加载完成,否则就会直接返回画布。
  3. js代码本身是同步执行的,但是如果实现了一些异步函数,如setTimeout和img.onload等,可能会出现返回结果不正确的情况,所以,如果需要返回值,最好利用funture构造,最后接收返回值。
  4. 对于同一个需求,最好选择更为方便的方法实现,否则会浪费时间和资源。(笔者在这个地方花费时间有点多,但是对于js也有了更深的认识)

最后,本文主要介绍了js获取图片base64的问题,中间过程有些曲折,内容可能有点凌乱,但是技术点还是给出了,希望各位读者能够从中有所收获。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
社会演员多的头像社会演员多普通用户
上一篇 2023年11月28日
下一篇 2023年11月28日

相关推荐