前言
使用java生成每日海报。
项目起因是巧合下遇到了一篇很棒的文档,说的是用程序来实现每日生成一个海报。如果之后加上自动发布的功能,简直就是太棒了啊!
样例图如下:
每日海报
思路
- 访问某词站的API获取网络图片,为的就是他已经裁剪好的大小。
- 加载个人品牌二维码,这里我是以个人的微信公众号的二维码。
- 获取某词爸的文字内容,此处使用Jsoup解析Html结果后取最新的句子。
- 利用 Graphics2D 在海报上打印中英文对照语、绘制个人专属二维码,打印当前日期
- 运行项目即可,也可以后期达成Jar包,单独运行一次就能生成一个。
我知道,读者你肯定还是希望能够让项目按时间自动生成,我不仅要求这些,还要能自动发布微信公众号和博客呢。
那些都是后期功能了,本期就关注于生成最关键的图片。
数据来源
因为时间过去很久了,原文中很多的Api已经不能够继续使用了。所以这里简单记录一下我所使用的数据来源。
某词爸
遗憾的是,通过网页访问GET请求只能够得到文字,无法拿到图片了
http://jinjin.online/tools/word_everyday/show.php?page_num=1
某词站
可以通过POST请求访问地址获取。
http://learn.baicizhan.com/api/activity/sentence/list
需要注意的是,要在请求头中加上Cookie
,并且其中要加上个人认证的access_token
字段才可以获取到数据。
所以如果读者想要复现的话,就需要在自己的手机上登入自己的账户,然后通过抓包的方式获取到指定数据才可以。
具体的抓包方式教程我附在文末了。
图片下载下来之后,可以确认分辨率是1125x1500,对我而言已经足够了。
功能实现
获取网络图片
首先通过抓包拿到自己的手机请求每日图片的Token,为了防止某些不必要的误会,这里不展开讲了。
public String getInfoList() {
return HttpUtils.doPost(urlConfig.getBczUrl(), "{\"size\":20}", HttpUtils.headersMap);
}
得到的结果是类似于这样的JSON体:
{
"date":1704902400000,
"sentence":{
"en":"What is an ocean but a multitude of drops.",
"cn":"不积小流,无以成江海。"
},
"backgroundImg":"https://7n.bczcdn.com/r/qeb7xm3jjhp9zhmtz8cgv3d254l24xh5.png",
"dakaNum":7817,
"sentenceSource":"-《云图》",
"collected":false,
"id":597,
"audio":"https://vol.bczcdn.com/r/us_What_is_20221001152932680_bc72af44e192cb6ee36c.mp3",
"daka":false
}
而我只需要其中的背景图片地址,将他保存为临时文件。
public File getBczInfoImage(BczDTO bczDTO) {
String backgroundImg = bczDTO.getBackgroundImg();
try {
// 获取输入流对象
InputStream inputStream = bczService.getBczInfoImage(bczDTO);
// 存储为临时文件
return FileUtils.createTmpFile(inputStream,
dirsConfig.getImagesDir(),
DateUtil.formatDate(new Date(), DateUtil.YYYYMMDD) + "_",
backgroundImg.substring(backgroundImg.lastIndexOf(Constant.DOT)));
} catch (Exception e) {
log.error("获取某词站临时图片发生错误:{},{}", e.getMessage(), e);
throw new ServiceException("获取某词站临时图片发生错误");
}
}
个人二维码
至于个人二维码我是用的微信公众号的二维码。
直接将图片放到了项目里,也可以通过项目配置文件指定绝对路径来找到你自己的二维码图片。
public File getQcCodeFile(String qcCodeName) {
return new File(qcCodeName);
}
获取网络文字
public Sentence getJscbInfo() {
Sentence sentence = new Sentence();
String html = jscbService.getInfo();
//用Jsoup解析html
Document document = Jsoup.parse(html);
//像js一样,通过class获取列表下的所有
Elements postItems = document.getElementsByClass("list-group");
postItems = postItems.get(postItems.size()-1).getElementsByTag("font");
// 此处是英文
sentence.setEn(postItems.get(1).text());
// 此处是中文
sentence.setCn(postItems.get(2).text());
return sentence;
}
解析后将中文和英文读取出来。
打印信息
核心代码如下。
try {
// 5 利用 Graphics2D 在图片上打印中英文对照语
log.info("开始在图像上打印中英文对照语");
Graphics2DPoster graphics2DPoster = Graphics2DUtils.drawImage(ImageIO.read(bczInfoImage));
graphics2DPoster.setZh(jscbInfo.getCn());
Graphics2DUtils.drawZhString(graphics2DPoster);
graphics2DPoster.setEn(jscbInfo.getEn());
Graphics2DUtils.drawEnString(graphics2DPoster);
log.info("在图像上打印中英文对照语成功");
// 6. 打印个人的二维码
BufferedImage qrcodeImage = ImageIO.read(qcCodeFile);
graphics2DPoster.setQrcodeImage(qrcodeImage);
Graphics2DUtils.drawQrcode(graphics2DPoster);
// 7. 右上角画日期
String dateString = Graphics2DUtils.drawDate(graphics2DPoster);
// 释放图形上下文,以及它正在使用的任何系统资源。
graphics2DPoster.getGraphics2d().dispose();
// 8. 最后保存成为文件
File posterFile = new File(dailyPosterDirsConfig.getImagesDir()+"/daily_poster_"+dateString+".png");
ImageIO.write(graphics2DPoster.getBgImage(), "png",posterFile );
log.info("绘制好封面图的海报" + posterFile.getAbsolutePath());
}catch (Exception e){
log.error("发生异常:{},{}",e.getMessage(),e);
SpringApplication.exit(DailyPosterApplication.applicationContext, () -> 0);
return;
}
工具类代码如下。
public class Graphics2DUtils {
/**
* 留白
*/
private static final int MARGIN = 25;
private static final int SUITABLE_WIDTH = 700;
/**
* @param bgImage
* @return {@code Graphics2DPoster}
* @throws IOException
*/
public static Graphics2DPoster drawImage(BufferedImage bgImage) throws IOException {
// 封面图的起始坐标
int pic_x = MARGIN, pic_y = MARGIN;
Graphics2D graphics2d = bgImage.createGraphics();
Graphics2DPoster graphics2dPoster = new Graphics2DPoster(graphics2d);
// 海报可容纳的宽度
graphics2dPoster.setSuitableWidth(SUITABLE_WIDTH);
graphics2dPoster.setBgImage(bgImage);
// 记录此时的 y 坐标
graphics2dPoster.setCurrentY(pic_y * 40);
return graphics2dPoster;
}
public static Graphics2DPoster drawZhString(Graphics2DPoster graphics2dPoster) throws IOException {
// 获取计算机上允许使用的中文字体
List<String> fontNames = Arrays
.asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
if (fontNames == null || !fontNames.contains(FontUtil.USE_FONT_NAME)) {
throw new RuntimeException("计算机上未安装" + FontUtil.USE_FONT_NAME + "的字体");
}
// 设置封面图和下方中文之间的距离
graphics2dPoster.addCurrentY(30);
Graphics2D graphics2d = graphics2dPoster.getGraphics2d();
graphics2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
// Font 的构造参数依次是字体名字,字体式样,字体大小
Font font = new Font(FontUtil.USE_FONT_NAME, Font.PLAIN, FontUtil.FONT_SIZE);
graphics2d.setFont(font);
graphics2d.setColor(new Color(255, 255, 255));
FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font);
graphics2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
String zhWrap = FontUtil.makeZhLineFeed(graphics2dPoster.getZh(), metrics, graphics2dPoster.getSuitableWidth());
String[] zhWraps = zhWrap.split("\n");
for (int i = 0; i < zhWraps.length; i++) {
graphics2dPoster.addCurrentY(metrics.getHeight());
graphics2d.drawString(zhWraps[i], MARGIN, graphics2dPoster.getCurrentY());
}
return graphics2dPoster;
}
public static void drawEnString(Graphics2DPoster graphics2dPoster) throws IOException {
// 设置封面图和下方中文之间的距离
graphics2dPoster.addCurrentY(20);
Graphics2D graphics2d = graphics2dPoster.getGraphics2d();
graphics2d.setColor(new Color(255, 255, 255));
FontDesignMetrics metrics = FontDesignMetrics.getMetrics(graphics2d.getFont());
String enWrap = FontUtil.makeEnLineFeed(graphics2dPoster.getEn(), metrics, graphics2dPoster.getSuitableWidth());
String[] enWraps = enWrap.split("\n");
for (int i = 0; i < enWraps.length; i++) {
graphics2dPoster.addCurrentY(metrics.getHeight());
graphics2d.drawString(enWraps[i], MARGIN, graphics2dPoster.getCurrentY());
}
}
/**
* 画二维码
*
* @param graphics2dPoster
*/
public static void drawQrcode(Graphics2DPoster graphics2dPoster) {
BufferedImage qrcodeImage = graphics2dPoster.getQrcodeImage();
BufferedImage bgImage = graphics2dPoster.getBgImage();
// 二维码起始坐标
int qrcode_x = bgImage.getWidth() - qrcodeImage.getWidth() - MARGIN;
int qrcode_y = bgImage.getHeight() - qrcodeImage.getHeight() - MARGIN;
Graphics2D graphics2d = graphics2dPoster.getGraphics2d();
graphics2d.drawImage(qrcodeImage, qrcode_x, qrcode_y, qrcodeImage.getWidth(), qrcodeImage.getHeight(), null);
}
/**
* 画日期
*
* @param graphics2dPoster
*/
public static String drawDate(Graphics2DPoster graphics2dPoster) {
BufferedImage bgImage = graphics2dPoster.getBgImage();
Date date = new Date();
String dateDay = DateUtil.formatDate(date, DateUtil.DD);
String dateYearAndMonth = DateUtil.formatDate(date, DateUtil.YYYY_MM);
Graphics2D graphics2d = graphics2dPoster.getGraphics2d();
graphics2dPoster.setCurrentY(MARGIN * 5);
// 先画日期
graphics2d.setFont(new Font(FontUtil.USE_FONT_NAME, Font.PLAIN, FontUtil.FONT_SIZE + 30));
graphics2d.drawString(dateDay, bgImage.getWidth() - MARGIN * 6, graphics2dPoster.getCurrentY());
graphics2dPoster.addCurrentY(66);
graphics2d.setFont(new Font(FontUtil.USE_FONT_NAME, Font.PLAIN, FontUtil.FONT_SIZE));
//再画年月
graphics2d.drawString(dateYearAndMonth, bgImage.getWidth() - MARGIN * 10, graphics2dPoster.getCurrentY());
return DateUtil.formatDate(date, DateUtil.YYYYMMDD);
}
}
最后就能够实现生成一个有自己的二维码的每日图片。
总结和感谢
示例文件
gitee:
每日海报模块:
https://gitee.com/JunKuangKuang/keenJavaTest-all/tree/master/keenTest-springBoot-parent/daily-poster
参考的开源项目:
https://github.com/qinggee/poster/tree/jinshanciba
更新记录
2023-01-12 发布初版
2024-01-10 编写本文
感谢
感谢现在的好奇,为了能成为更好的自己。
Java生成金山词霸的二维码分享海报
安卓手机抓包