在我们使用app的时候有很多情况需要分享,比如背单词每日打卡,而分享实际上就是将当前页面截图然后保存下来,这里我们就需要模拟APP来实现Web的生成长图并查看。
在这里我们需要用到wkhtmltopdf,这是一个将html页面转换成image或者pdf的工具,具体命令是类似wkhtmltoimage --quality 75 https://www.baidu.com d:/work/data,其中wkhtmltoimage表示将html转换成image,–quality 75表示将图片压缩到75%,再后面跟的是html的url和图片保存的路径。
配置
在配置上我们需要将wkhtmltopdf的指令和路径配置到application.properties中
# wk
wk.image.command=d:/work/wkhtmltopdf/bin/wkhtmltoimage
wk.image.storage=d:/work/data/wk-images
然后,wkhtmltopdf并不会自己创建目录,因此我们要确保目录是存在的。我们可以写一个config类,然后写一个初始化方法,使用@PostConstruct注解确保只会在服务启动时初始化一次,在该方法中检查路径是否存在,不存在就创建。
@Configuration
public class WkConfig {
private static final Logger logger = LoggerFactory.getLogger(WkConfig.class);
@Value("${wk.image.storage}")
private String wkImageStorage;
@PostConstruct
public void init(){
// 创建WK图片目录
File file = new File(wkImageStorage);
if (!file.exists()){
file.mkdir();
logger.info("创建WK图片目录:" + wkImageStorage);
}
}
}
Controller
我们采用消息队列来实现生成长图功能,当controller收到一个生成长图的请求时我们将任务放入消息队列中,然后然后一个可以访问图片的路径即可。
这里为了简便,我们就要求用户将路径以参数的方式传过来,我们获取url,然后使用UUID生成随机的文件名,将这些参数放入Event实例中,然后返回一个带长图路径的map即可,对于路径我们设置为domain + contextPath + “/share/image/” + fileName。
然后我们在写一个Controller用于输出png文件即可。这里和之前显示头像是一致的。具体就是用File指向文件,然后使用FileInputStream装饰File实例,将内容读取到byte数组buffer中去,然后再将buffer数组中的写到response的OutputStream输出流实例中,循环中读取完全部数据。
@Controller
public class ShareController implements CommunityConstant {
private static final Logger logger = LoggerFactory.getLogger(ShareController.class);
@Autowired
private EventProducer eventProducer;
@Value("${community.path.domain}")
private String domain;
@Value("${server.servlet.context-path}")
private String contextPath;
@Value("${wk.image.storage}")
private String wkImageStorage;
@RequestMapping(path = "/share",method = RequestMethod.GET)
@ResponseBody
public String share(String htmlUrl){
// 文件名
String fileName = CommunityUtil.generateUUID();
// 异步生成长图
Event event = new Event()
.setTopic(TOPIC_SHARE)
.setData("htmlUrl",htmlUrl)
.setData("fileName",fileName)
.setData("suffix",".png");
eventProducer.fireEvent(event);
// 返回访问路径
Map<String, Object> map = new HashMap<>();
map.put("shareUrl", domain + contextPath + "/share/image/" + fileName);
return CommunityUtil.getJSONString(200,null, map);
}
// 获取长图
@RequestMapping(path = "/share/image/{fileName}",method = RequestMethod.GET)
public void getShareImage(@PathVariable("fileName") String fileName, HttpServletResponse response){
if (StringUtils.isBlank(fileName)){
throw new IllegalArgumentException("文件名不能为空");
}
response.setContentType("image/png");
File file = new File(wkImageStorage+"/"+fileName+".png");
try {
OutputStream os = response.getOutputStream();
FileInputStream fis = new FileInputStream(file);
byte[] buffer = new byte[1024];
int b = 0;
while ((b = fis.read(buffer)) != -1){
os.write(buffer,0,b);
}
} catch (IOException e) {
logger.error("获取长图失败:" + e.getMessage());
}
}
}
生成长图
我们需要消费Topic为share的任务来生成长图。
使用ConsumerRecord实例来获取event,检查空值和消息格式和之前一致,然后我们得到htmlUrl,fileName,suffix,拼接出wkhtmltoimage命令,然后调用Runtime.getRuntime().exec(cmd)即可,注意该语句是请求操作系统去执行cmd指令,因此是异步的。
// 消费分享时间
@KafkaListener(topics = TOPIC_SHARE)
public void handleShareMessage(ConsumerRecord record){
if (record == null || record.value() == null ){
logger.error("消息的内容为空!");
return;
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null){
logger.error("消息格式错误!");
return;
}
String htmlUrl = (String) event.getData().get("htmlUrl");
String fileName = (String) event.getData().get("fileName");
String suffix = (String) event.getData().get("suffix");
String cmd = wkImageCommand + " --quality 75 " + htmlUrl + " " + wkImageStorage
+ "/" + fileName + suffix;
try {
Runtime.getRuntime().exec(cmd);
logger.info("生成长图成功:" + cmd);
} catch (IOException e) {
logger.error("生成长图失败:" + e.getMessage());
e.printStackTrace();
}
}