Java后端将网页完美转为图片的方法

最近遇到开发需求,需要将SpringBoot后端的一个H5网页,转换为图片,并发送到指定的接口上。由于要考虑到要兼容各种CSS样式和AJAX请求的因素,因此用内嵌浏览器的实现方法往往会导致网页样式出不来。
因此思路是操作服务器本地的Chrome,访问网页再“截图”为图片。然而Java本身并没有合适的控制本地Chrome的API,而nodejs的puppeteer提供了可以控制headless chrome的API接口,其中就包括对网页进行截屏的API。因此,决定用Java控制nodejs完成网页转图片文件的操作,再用Java读取转换完的图片文件进行操作。
这里以ubuntu服务器为例说明步骤如下:

  1. 安装Chrome或Chromium。前者可以在Chrome官网下载deb包(Chrome官网),后者直接用apt-get就可以安装(具体步骤)。
  2. 安装nodejs,具体步骤可参照github上的说明。
  3. 将npm的库变更为淘宝的镜像。
  4. 运行以下命令安装puppeteer。如果直接安装puppeteer会自动再安装一个Chromium,为了避免这个问题可以只安装puppeteer-core。详细说明。
cnpm i puppeteer-core 
  1. 用Chrome访问需要转图片的H5网页,检查字体是否正常。如果发现字体缺失,可以从windows/fonts里copy字体到服务器上。如果原字体文件的格式为ttc,需要转换成ttf,可以安装fontforge,具体操作为
apt-get install fontforge

然后用fontforge将ttc另存为ttf,并将文件copy到服务器的/usr/share/fonts/truetype/xxx,其中xxx文件夹可以自行创建,然后运行以下命令,系统可以自动检测/usr/share/fonts及子目录下的ttf文件,刷新字体缓存。

sudo mkfontscale
sudo mkfontdir
sudo fc-cache -fv
fc-list
  1. 创建截图脚本screenshot.js,如下:
const puppeteer = require('puppeteer-core');
const os = require('os');

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
};
(async () => {
  const browser = await puppeteer.launch({
	  headless: true,
	  executablePath: process.argv[2]
	  });
  const page = await browser.newPage();
  await page.goto(process.argv[3]);
  // 仅指定宽度,高度根据返回结果确定
  await page.setViewport({width:parseInt(process.argv[4]), height:0});
  // 等待页面自动执行的AJAX
  await timeout(10000);
  const pagesize = await page.evaluate(() => {
	  // TODO: 在这里可以动态操作dom元素
	  return {
		  width: document.documentElement.scrollWidth,
		  height: document.documentElement.scrollHeight};
  });
  // 重新调整viewport大小,适配真实的页面
  await page.setViewport({width:pagesize.width, height:pagesize.height});
  const path = process.argv[5].replace('~',os.homedir());
  await page.screenshot({path: path, fullpage: true});
  console.log("file " + path + " is saved, page size:" + pagesize.width + "," + pagesize.height);
  await browser.close();
})();
  1. 在Java里调用node,利用processbuilder构造命令行参数,再创建执行进程,并等待返回结果。
protected String generateScreenShot(Properties prop) throws Exception {
	log.info("generate screenshot");
	ProcessBuilder pb = new ProcessBuilder("node",
						// 不指定这个参数的话,nodejs执行出现unhandled exception时,会block进程
					   "--unhandled-rejections=strict",
					   prop.getProperty("alert_screenshot_script_file"),
					   prop.getProperty("alert_screenshot_browser"),
					   prop.getProperty("alert_detail_url"),
					   prop.getProperty("alert_screenshot_width"),
					   prop.getProperty("alert_screenshot_file"));
	pb.directory(ResourceUtils.getFile("classpath:" + prop.getProperty("alert_screenshot_script_path")));
	// 将正常的output和error分开显示
	pb.redirectErrorStream(false);
	Process p = pb.start();
	// 为了支持分开显示,采用线程的方法,同时防止read()之类的操作阻塞主线程
	Thread t1 = printScreenShotInformation(p.getInputStream(), false);
	Thread t2 = printScreenShotInformation(p.getErrorStream(), true);
	// 等待执行
	int exitcode = p.waitFor();
	Thread.sleep(1000);
	// 如果有线程有read()没返回,可以强制退出
	t1.interrupt();
	t2.interrupt();
	String strFilePath = prop.getProperty("alert_screenshot_file").replaceFirst("^~", System.getProperty("user.home"));
	log.info(String.format("fininsh saving a screen shot to %s with exit code:%d", strFilePath, exitcode));
	return strFilePath;
}

protected Thread printScreenShotInformation(final InputStream in, boolean bIsErr) {
	Thread t = new Thread() {
		@Override
		public void run() {
			try {
				int n;
				StringBuilder sb = new StringBuilder();
				while ((n = in.read()) != -1) {
					char c = (char)n;
					if(c == '\r' || c == '\n') {
						if(sb.length() > 0) {
							if(bIsErr) {
								log.error(sb.toString());
							}
							else {
								log.info(sb.toString());
							}
							sb = new StringBuilder();
						}
					}
					else {
						sb.append(c);
					}
				}
				if(sb.length() > 0) {
					if(bIsErr) {
						log.error(sb.toString());
					}
					else {
						log.info(sb.toString());
					}
				}
			}
			catch(Exception e) {
				e.printStackTrace();
			}
		}
	};
	t.start();
	return t;
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
首先,你需要在前端将照片转为二进制数据。可以通过以下方式实现: ```javascript // 获取照片文件 const file = document.getElementById('photo').files[0]; // 读取文件并转为二进制数据 const reader = new FileReader(); reader.onload = (event) => { const binaryData = event.target.result; // 发送请求将二进制数据传给后端 }; reader.readAsBinaryString(file); ``` 在后端,你可以使用Java的JDBC API将二进制数据存入数据库中。假设你使用的是MySQL数据库,下面是一个将二进制数据存入Blob类型字段的示例代码: ```java import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public class PhotoUploader { public static void main(String[] args) { // 连接数据库 Connection conn = null; try { Class.forName("com.mysql.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/test"; String username = "root"; String password = "password"; conn = DriverManager.getConnection(url, username, password); } catch (Exception e) { e.printStackTrace(); return; } // 获取二进制数据 InputStream is = request.getInputStream(); // 存入数据库 PreparedStatement ps = null; try { ps = conn.prepareStatement("INSERT INTO photos (photo) VALUES (?)"); ps.setBinaryStream(1, is, is.available()); ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); } finally { try { ps.close(); } catch (Exception e) {} try { conn.close(); } catch (Exception e) {} } } } ``` 在上面的代码中,我们使用`ps.setBinaryStream()`方法将二进制数据存入Blob类型字段。注意,`is.available()`方法返回的是二进制数据的长度,所以我们可以直接将`InputStream`对象传给`setBinaryStream()`方法。最后,别忘了关闭`PreparedStatement`和`Connection`对象。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值