使用 image-combiner 开源项目实现对海报图片的生成
1:gitee 项目地址
image-combiner: ImageCombiner是一个专门用于Java服务端图片合成的工具,没有很复杂的功能,简单实用,从实际业务场景出发,提供简单的接口,几行代码即可实现图片拼合(当然用于合成水印也可以),素材上支持图片、文本、矩形三种,支持定位、缩放、旋转、圆角、透明度、颜色、字体、字号、删除线、居中绘制、文本自动换行等特性,足够覆盖图片合成的日常需求。https://gitee.com/dromara/image-combiner#242-%E5%A6%82%E4%BD%95%E6%96%87%E5%AD%97%E7%AB%96%E6%8E%92
2:ImageCombiner怎么使用
ImageCombiner使用起来相当简单,主要的类只用一个,new一个ImageCombiner对象,指定背景图片和输出格式,然后加入各种素材元素,设置元素的位置、大小和效果(如圆角、颜色、透明度等),调用combine()方法即可。combine()方法直接返回BufferedImage对象,也可以调用getCombinedImageStream()获得流,方便上传oss等后续操作,或者调用save()方法保存到本地,调试的时候比较方便。
3:使用安装
<dependency><groupId>com.freewayso</groupId><artifactId>image-combiner</artifactId><version>2.6.8</version>
</dependency>
4:使用示例
public void simpleDemo() throws Exception {//合成器(指定背景图和输出格式,整个图片的宽高和相关计算依赖于背景图,所以背景图的大小是个基准)ImageCombiner combiner = new ImageCombiner("http://xxx.com/image/bg.jpg", OutputFormat.JPG);//加图片元素combiner.addImageElement("http://xxx.com/image/product.png", 0, 300);//加文本元素combiner.addTextElement("周末大放送", 60, 100, 960);//执行图片合并combiner.combine();//可以获取流(并上传oss等)InputStream is = combiner.getCombinedImageStream();//也可以保存到本地//combiner.save("d://image.jpg");
5:完整示例
public void demo() throws Exception {//图片元素可以是Url(支持file协议),也可以是BufferImage对象String bgImageUrl = "http://xxx.com/image/bg.jpg"; //背景图(url)String qrCodeUrl = "file:///d:/qrCode.png"; //二维码(file协议)String productImageUrl = "https://xxx.com/image/product.jpg"; //商品图BufferedImage waterMark = ImageIO.read(new URL("https://xxx.com/image/waterMark.jpg")); //水印图BufferedImage avatar = ImageIO.read(new File("d:/avatar.jpg")); //头像String title = "# 最爱的家居"; //标题文本String content = "苏格拉底说:“如果没有那个桌子,可能就没有那个水壶”"; //内容文本//创建合成器(指定背景图和输出格式,整个图片的宽高和相关计算依赖于背景图,所以背景图的大小是个基准)ImageCombiner combiner = new ImageCombiner(bgImageUrl, 1500, 0, ZoomMode.Height, OutputFormat.JPG); //v1.1.4之后可以指定背景图新宽高了(不指定则默认用图片原宽高)//针对背景和整图的设置combiner.setBackgroundBlur(30); //设置背景高斯模糊(毛玻璃效果)combiner.setCanvasRoundCorner(100); //设置整图圆角(输出格式必须为PNG)combiner.setQuality(.8f); //设置图片保存质量(0.0~1.0,Java9以下仅jpg格式有效)//标题(默认字体为阿里普惠、黑色,也可以自己指定Font对象)combiner.addTextElement(title, 0, 150, 1400).setCenter(true) //居中绘制(会忽略x坐标,改为自动计算).setAlpha(.8f) //透明度(0.0~1.0).setRotate(45) //旋转(0~360).setColor(Color.Red) //颜色.setDirection(Direction.RightLeft) //绘制方向(从右到左,用于需要右对齐场景).setAutoFitWidth(200); //自适应最大宽度(超出则自动缩小字体)//副标题(v2.6.3版本开始支持加载项目内字体文件,可以不用在服务器安装,性能略低)combiner.addTextElement("年度狂欢", "/font/yahei.ttf", 0, 150, 1450)//内容(设置文本自动换行,需要指定最大宽度(超出则换行)、最大行数(超出则丢弃)、行高)combiner.addTextElement(content, "微软雅黑", Font.BOLD, 40, 150, 1480).setSpace(.5f) //字间距.setStrikeThrough(true) //删除线.setAutoBreakLine(837, 2, 60); //自动换行(还有一个LineAlign参数可以指定对齐方式)//商品图(设置坐标、宽高和缩放模式,若按宽度缩放,则高度按比例自动计算)combiner.addImageElement(productImageUrl, 0, 160, 837, 0, ZoomMode.Width).setCenter(true) //居中绘制(会忽略x坐标,改为自动计算).setRoundCorner(46) //设置圆角//头像(圆角设置一定的大小,可以把头像变成圆的)combiner.addImageElement(avatar, 200, 1200).setRoundCorner(200); //圆角//水印(设置透明度,0.0~1.0)combiner.addImageElement(waterMark, 630, 1200).setAlpha(.8f) //透明度(0.0~1.0).setRotate(45) //旋转(0~360).setBlur(20) //高斯模糊(1~100).setRepeat(true, 100, 50); //平铺绘制(可设置水平、垂直间距)//加入圆角矩形元素(版本>=1.2.0),作为二维码的底衬combiner.addRectangleElement(138, 1707, 300, 300).setColor(Color.WHITE).setRoundCorner(50) //该值大于等于宽高时,就是圆形,如设为300.setAlpha(.8f).setGradient(Color.yellow,Color.blue,GradientDirection.LeftRight); //颜色渐变.setBorderSize(5); //设置border大小就是空心,不设置就是实心//二维码(强制按指定宽度、高度缩放)combiner.addImageElement(qrCodeUrl, 138, 1707, 186, 186, ZoomMode.WidthHeight);//价格(元素对象也可以直接new,然后手动加入待绘制列表)TextElement textPrice = new TextElement("¥1290", 60, 230, 1300);textPrice.setColor(Color.red); //红色textPrice.setStrikeThrough(true); //删除线combiner.addElement(textPrice); //加入待绘制集合//执行图片合并combiner.combine();//可以获取流(并上传oss等)InputStream is = combiner.getCombinedImageStream();//也可以保存到本地//combiner.save("d://image.jpg");
}
6:个人项目案例
1: 第一张效果
package com.example.juc.posterTemplate;import com.freewayso.image.combiner.ImageCombiner;
import com.freewayso.image.combiner.enums.LineAlign;
import com.freewayso.image.combiner.enums.OutputFormat;
import com.freewayso.image.combiner.enums.ZoomMode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("PosterOne")
public class PosterOne {@GetMappingpublic void PosterOne(HttpServletResponse response) throws Exception {// 记录开始时间long startTime = System.nanoTime(); // 获取开始时间ClassLoader loader = Thread.currentThread().getContextClassLoader();// BufferedImage backgroundImage = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/67868d805e20240426145820.png")));BufferedImage backgroundImage = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/293231.jpg")));System.out.println("宽度"+ backgroundImage.getWidth());System.out.println("高度"+ backgroundImage.getHeight());// 创建 Graphics2D 对象Graphics2D g2d = backgroundImage.createGraphics();Color color = new Color(0, 88, 38);// 设置透明度int alpha = 180; // 透明度(取值范围:0 - 255)AlphaComposite alphaComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) alpha / 255);g2d.setComposite(alphaComposite);// 设置背景色g2d.setColor(color);// 填充整个图片区域g2d.fillRect(0, 0, backgroundImage.getWidth(), backgroundImage.getHeight());//合成器(指定背景图和输出格式,整个图片的宽高和相关计算依赖于背景图,所以背景图的大小是个基准)ImageCombiner combiner = new ImageCombiner(backgroundImage, 1372, 1978, ZoomMode.WidthHeight, OutputFormat.JPG); //v1.1.4之后可以指定背景图新宽高了(不指定则默认用图片原宽高)//加图片元素BufferedImage backgroundImageTwo = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/cb18e0953720240426150230.png")));// 读取二维码BufferedImage erweima = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/d4eaffbdb920240426150125.png")));//读取头像BufferedImage touxiang = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/44415e306a4802dd43c161e872a1fc56.png")));combiner.addImageElement(backgroundImageTwo, 50, 50, 230, 60, ZoomMode.WidthHeight); //中大logocombiner.addImageElement(erweima, 50, 1280, 230, 230, ZoomMode.WidthHeight); //二维码(强制按指定宽度、高度缩放)//头像(圆角设置一定的大小,可以把头像变成圆的)combiner.addImageElement(touxiang, 800, 800, 400, 400, ZoomMode.WidthHeight).setRoundCorner(400); //圆角//加文本元素combiner.addTextElement("物理与天文学院学术报告 第259期", "宋体", Font.BOLD, 50, 50, 180).setColor(Color.white) ;//加文本元素combiner.addTextElement("MIMO天线及其空口测试技术", "", Font.BOLD, 120, 50, 280).setColor(Color.white).setSpace(0.1f).setAutoBreakLine(1300, 5, 120); //自动换行(还有一个LineAlign参数可以指定对齐方式)combiner.addTextElement("2024", "Source Han Sans CN", Font.BOLD, 50, 0, 830).setColor(Color.white).setRotate(90); // 旋转combiner.addTextElement("10.18", "Source Han Sans CN", Font.BOLD, 120, 125, 810).setColor(Color.white);combiner.addTextElement("14:30 ~ 15:30", "Source Han Sans CN", Font.BOLD, 60, 50, 950).setColor(Color.white);combiner.addTextElement("(周四)", "", Font.BOLD, 60, 20, 1030).setColor(Color.white);combiner.addTextElement("电信楼101讲学厅", "宋体", Font.BOLD, 60, 50, 1730).setColor(Color.white);combiner.addTextElement("中山大学(东校园)", "", Font.BOLD, 40, 50, 1830).setColor(Color.white);//添加文字,并设置为自动换行,且行宽设为0combiner.addTextElement("主讲人", "宋体", 50, 1250, 750).setAutoBreakLine(0, 50, 60).setColor(Color.white);;combiner.addTextElement("欧阳上官", "宋体",50, 1180, 750).setAutoBreakLine(0, 50, 60, LineAlign.Center).setColor(Color.white);;//执行图片合并combiner.combine();//可以获取流(并上传oss等)InputStream is = combiner.getCombinedImageStream();combiner.setQuality(1.0f); //设置图片保存质量(0.0~1.0,Java9以下仅jpg格式有效)//也可以保存到本地combiner.save("F://image.jpg");InputStream download = combiner.getCombinedImageStream();writeFile(response, download);long endTime = System.nanoTime(); // 获取结束时间long elapsedTime = endTime - startTime; // 计算时间差(纳秒)// 将纳秒转换为秒double seconds = TimeUnit.NANOSECONDS.toSeconds(elapsedTime);System.out.printf(String.valueOf(seconds));}/*** 将输入流输出到页面*/public static void writeFile(HttpServletResponse resp, InputStream inputStream) {OutputStream out = null;try {out = resp.getOutputStream();int len = 0;byte[] b = new byte[1024];while ((len = inputStream.read(b)) != -1) {out.write(b, 0, len);}out.flush();} catch (IOException e) {e.printStackTrace();} finally {try {if (out != null) {out.close();}} catch (Exception e) {e.printStackTrace();}}}}
2:生成效果
3:第二张代码
package com.example.juc.posterTemplate;import com.example.juc.utils.ThumbnailsUtils;
import com.freewayso.image.combiner.ImageCombiner;
import com.freewayso.image.combiner.enums.LineAlign;
import com.freewayso.image.combiner.enums.OutputFormat;
import com.freewayso.image.combiner.enums.ZoomMode;
import net.coobird.thumbnailator.Thumbnails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.font.FontDesignMetrics;import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("posterTwo")
public class PosterTwo {@GetMappingpublic void posterTwo(HttpServletResponse response) throws Exception {// 记录开始时间long startTime = System.nanoTime(); // 获取开始时间ClassLoader loader = Thread.currentThread().getContextClassLoader();BufferedImage backgroundImage = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/75f82fa97520240426151751.png")));// BufferedImage backgroundImage = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/293231.jpg")));/*** 调整图片的宽高*/BufferedImage image = new BufferedImage(1472, 2102, BufferedImage.TYPE_INT_RGB);Graphics2D g2dd = image.createGraphics();g2dd.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);// 绘制原始图像到新的 BufferedImage,并进行缩放Image imageScaledInstance = backgroundImage.getScaledInstance(1472, 2102, Image.SCALE_SMOOTH);g2dd.drawImage(imageScaledInstance, 0, 0, null);g2dd.dispose();/*** 调整图片的宽高*/Color color = new Color(0, 88, 38);// 创建颜色并设置透明度int alpha = 240; // 透明度(取值范围:0 - 255)// 创建 Graphics2D 对象Graphics2D g2d = image.createGraphics();// 设置透明度AlphaComposite alphaComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) alpha / 255);g2d.setComposite(alphaComposite);// 设置左上角背景色g2d.setColor(color);// 填充整个图片区域g2d.fillRect(0, 0, 450, 230);// 设置背景色Graphics2D g3d = image.createGraphics();// 设置最下面的背景色g3d.setColor(color);// 填充整个图片区域g3d.fillRect(0, 1980, 1472, 230);//合成器(指定背景图和输出格式,整个图片的宽高和相关计算依赖于背景图,所以背景图的大小是个基准)ImageCombiner combiner = new ImageCombiner(image,1472, 2102,ZoomMode.WidthHeight, OutputFormat.JPG); //v1.1.4之后可以指定背景图新宽高了(不指定则默认用图片原宽高)//加黄色背景 图片元素BufferedImage backgroundImageTwo = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/d2bb4a861020240426151628.png")));// 添加logo 图片BufferedImage backgroundImageThree = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/305653dc5420240426152302.png")));//中山大学logoBufferedImage backgroundImageFour = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/cb18e0953720240426150230.png")));//绿色的线BufferedImage backgroundImageFive = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/29ba7b8c1d20240426152032.png")));//二维码BufferedImage backgroundImageSix = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/d4eaffbdb920240426150125.png")));//白色的圈圈BufferedImage backgroundImageSeven = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/fa4ae6dd3320240426152414.png")));// 头像BufferedImage backgroundImageEight = ImageIO.read(Objects.requireNonNull(loader.getResourceAsStream("images/44415e306a4802dd43c161e872a1fc56.png")));combiner.addImageElement(backgroundImageTwo, 0, 230, 450, 1750, ZoomMode.WidthHeight); //(强制按指定宽度、高度缩放)combiner.addImageElement(backgroundImageThree, 0, 0, 300, 250, ZoomMode.WidthHeight); //(强制按指定宽度、高度缩放)combiner.addImageElement(backgroundImageFour, 1200, 2020); //(强制按指定宽度、高度缩放)combiner.addImageElement(backgroundImageFive, 38, 1400, 370, 2, ZoomMode.WidthHeight); //(强制按指定宽度、高度缩放)combiner.addImageElement(backgroundImageFive, 38, 1600, 370, 2, ZoomMode.WidthHeight); //(强制按指定宽度、高度缩放)combiner.addImageElement(backgroundImageSix, 50, 1750, 190, 190, ZoomMode.WidthHeight); //(强制按指定宽度、高度缩放)combiner.addImageElement(backgroundImageSeven, 950, 400, 400, 400, ZoomMode.WidthHeight); //(强制按指定宽度、高度缩放)combiner.addImageElement(backgroundImageEight, 970, 420, 360, 360, ZoomMode.WidthHeight).setRoundCorner(400);; //(强制按指定宽度、高度缩放)//添加文字,并设置为自动换行,且行宽设为0combiner.addTextElement("主讲人", "宋体", 50, 1100, 180).setAutoBreakLine(0, 50, 60).setColor(Color.white);;combiner.addTextElement("欧阳上官", "宋体",50, 1030, 180).setAutoBreakLine(0, 50, 60, LineAlign.Center).setColor(Color.white);//加文本元素combiner.addTextElement("主讲人:", "宋体", Font.BOLD, 40, 40, 2010).setColor(Color.white);combiner.addTextElement("李剑锋", "宋体", Font.BOLD, 40, 200, 2010).setColor(Color.white);//加文本元素// TODO 标题Graphics2D graphics = image.createGraphics();drawCenteredText(graphics, "逸仙生命讲坛人第五十九讲", 100, 180);Graphics2D graphicsTwo = backgroundImageTwo.createGraphics();drawMyString(graphicsTwo, "时空相分离调控的职务细胞信号转导", color);//TODO 时间 2024Font fontYear = new Font("Source Han Sans CN", Font.PLAIN, 40);graphicsTwo.setColor(Color.BLACK);qinxie(fontYear, graphicsTwo); //字体倾斜graphicsTwo.drawString("2024", 40, 1230);//TODO 时间 月份 10.23 01.23Font fontMonth = new Font("Source Han Sans CN", Font.BOLD, 70);graphicsTwo.setColor(Color.BLACK);qinxie(fontMonth, graphicsTwo);String posterMonth = "10.23";graphicsTwo.drawString(posterMonth, 230, 1230);//TODO 具体时间 20:12 ~ 11:00 周五Font fontDate = new Font("Source Han Sans CN", Font.BOLD, 43);graphicsTwo.setColor(Color.BLACK);qinxie(fontDate, graphicsTwo);graphicsTwo.drawString("20:12 ~ 11:00 周五", 40, 1330);// TODO 具体举办地点Font fontDd = new Font("宋体", Font.BOLD, 35);graphicsTwo.setFont(fontDd);graphicsTwo.setColor(Color.BLACK);qinxie(fontDd, graphicsTwo);drawWordAndLineFeed(graphicsTwo, fontDd,"中山大学(东校园-电信楼101讲学厅", 40, 1430, 330);// TODO 主讲人详细介绍信息drawMyLectureString(graphics, "我啥事打卡机大街上的情况为其较为气温气温其味无穷我的撒登记卡水电气网红,诶打算近期我后端数据啊按地区为背景墙网格袋安山东划算的挥洒的企鹅企鹅群。");// 设置字体和颜色Font font = new Font("宋体", Font.PLAIN, 50);g2d.setFont(font);g2d.setColor(Color.BLACK);graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);graphics.dispose();//执行图片合并combiner.combine();//可以获取流(并上传oss等)combiner.setQuality(1.0f); //设置图片保存质量(0.0~1.0,Java9以下仅jpg格式有效)//也可以保存到本地combiner.save("F://imageTwo.jpg");InputStream download = combiner.getCombinedImageStream();writeFile(response, download);long endTime = System.nanoTime(); // 获取结束时间long elapsedTime = endTime - startTime; // 计算时间差(纳秒)// 将纳秒转换为秒double seconds = TimeUnit.NANOSECONDS.toSeconds(elapsedTime);System.out.printf(String.valueOf(seconds));}public static BufferedImage resizeImageOne(BufferedImage originalImage, int targetWidth, int targetHeight) throws Exception {ByteArrayOutputStream outputStream = new ByteArrayOutputStream();Thumbnails.of(originalImage).size(targetWidth, targetHeight).outputFormat("JPG").outputQuality(1).toOutputStream(outputStream);byte[] data = outputStream.toByteArray();ByteArrayInputStream inputStream = new ByteArrayInputStream(data);return ImageIO.read(inputStream);}/*** 将输入流输出到页面*/public static void writeFile(HttpServletResponse resp, InputStream inputStream) {OutputStream out = null;try {out = resp.getOutputStream();int len = 0;byte[] b = new byte[1024];while ((len = inputStream.read(b)) != -1) {out.write(b, 0, len);}out.flush();} catch (IOException e) {e.printStackTrace();} finally {try {if (out != null) {out.close();}} catch (Exception e) {e.printStackTrace();}}}/*** @Author xialijun* @Description 海报横向文字写字换行算法* @Date 18:08 2024/4/24* @Param 参数 Graphics2D 对象 、font 字体设置 、 文字、 x轴左边、 y轴坐标 、每行字体的换行宽度**/public static void drawWordAndLineFeed(Graphics2D g2d, Font font, String words, int wordsX, int wordsY, int wordsWidth) {FontDesignMetrics metrics = FontDesignMetrics.getMetrics(font);// 获取字符的最高的高度int height = metrics.getHeight();int width = 0;int count = 0;int total = words.length();String subWords = words;int b = 0;for (int i = 0; i < total; i++) {// 统计字符串宽度 并与 预设好的宽度 作比较if (width <= wordsWidth) {width += metrics.charWidth(words.charAt(i)); // 获取每个字符的宽度count++;} else {// 画 除了最后一行的前几行String substring = subWords.substring(0, count);g2d.drawString(substring, wordsX, wordsY + (b * height));subWords = subWords.substring(count);b++;width = 0;count = 0;}// 画 最后一行字符串if (i == total - 1) {g2d.drawString(subWords, wordsX, wordsY + (b * height));}}}/*** 字体倾斜*/public static void qinxie(Font fontMonth,Graphics2D graphics){// 创建 AffineTransform 对象AffineTransform month = new AffineTransform();// 设置水平倾斜month.shear(-0.2, 0); // + 向左 - 向右倾斜// 应用变换到字体fontMonth = fontMonth.deriveFont(month);graphics.setFont(fontMonth);}public static void drawCenteredText(Graphics graphics, String text, int oneY, int twoY) {// 设置字体和颜色Font font = new Font("宋体", Font.BOLD, 50);graphics.setFont(font);graphics.setColor(Color.WHITE);// 获取字体渲染上下文FontMetrics fm = graphics.getFontMetrics(font);// 截取第二行文字的部分String textTwo = text.substring(text.indexOf("第"));// 获取第一行文字的宽度String textOne = text.substring(0, text.indexOf("第"));// 获取第二行文字的宽度int textTwoWidth = fm.stringWidth(textTwo);// 计算第一行文字的起始x坐标,使其水平居中int oneX = 50;// 计算第二行文字的起始x坐标,使其水平居中int twoX = (450 - textTwoWidth) / 2;// 绘制第一行文字graphics.drawString(textOne, oneX, oneY);// 绘制第二行文字graphics.drawString(textTwo, twoX, twoY);}/*** 部分文字 垂直排序时,不满多列,最后一列居中显示* @param textGraphics Graphics 对象* @param text 要传入的海报描述*/public static void drawMyString(Graphics textGraphics, String text,Color color) {// 每列显示的汉字数量int columnSize = 7;// 文字之间的垂直间距int verticalSpacing = 105;// 获取字体渲染上下文FontMetrics fm = textGraphics.getFontMetrics();// 获取字体的高度int fontHeight = fm.getHeight();System.out.println(fontHeight);// 计算每列的宽度int columnWidth = fontHeight + verticalSpacing;// 设置初始位置int x = 280;int y = 250;Font fontFour = new Font("Source Han Sans CN", Font.BOLD, 100);textGraphics.setFont(fontFour);textGraphics.setColor(color);// 绘制文字int charCount = 0;int totalColumns = (int)Math.ceil((double)text.length() / columnSize); // 总列数int totalRows = Math.min(columnSize, text.length()); // 总行数int remainingChars = text.length() % columnSize; // 最后一列剩余字符数for (int columnIndex = 0; columnIndex < totalColumns; columnIndex++) {for (int rowIndex = 0; rowIndex < totalRows; rowIndex++) {if (charCount >= text.length()) break;char ch = text.charAt(charCount);// 计算当前位置int cx = x - columnIndex * columnWidth;int cy = y + rowIndex * fontHeight + rowIndex * verticalSpacing; // 加入垂直偏移量// 计算当前位置
// int cx = x - columnIndex * columnWidth;
// int cy = y + rowIndex * fontHeight + rowIndex * verticalSpacing + columnIndex ;// 如果是最后一列并且不满 7 个字符,则需要将剩余字符居中if (columnIndex == totalColumns - 1 && remainingChars > 0) {int extraVerticalSpace = (columnSize - remainingChars) * (fontHeight + verticalSpacing) / 2;cy += extraVerticalSpace;}// 绘制文字textGraphics.drawString(String.valueOf(ch), cx, cy);charCount++;}}}/*** 在Graphics2D对象上绘制竖排文字** @param textGraphics Graphics2D对象* @param text 要绘制的文字*/public static void drawMyLectureString(Graphics textGraphics, String text) {// 每列显示的汉字数量int columnSize = 25;// 文字之间的垂直间距int verticalSpacing = 20;// 获取字体渲染上下文FontMetrics fm = textGraphics.getFontMetrics();// 获取字体的高度int fontHeight = fm.getHeight() - 30;// 计算每列的宽度int columnWidth = fontHeight + verticalSpacing;// 设置初始位置int x = 800;int y = 230;Font fontFour = new Font("宋体", Font.BOLD, 40);textGraphics.setFont(fontFour);textGraphics.setColor(Color.WHITE);// 计算总列数int totalColumns = (int) Math.ceil((double) text.length() / columnSize); // 总列数// 绘制文字int charCount = 0;for (int rowIndex = 0; rowIndex < columnSize; rowIndex++) {for (int columnIndex = 0; columnIndex < totalColumns; columnIndex++) {int charIndex = rowIndex * totalColumns + columnIndex;if (charIndex >= text.length()) break;char ch = text.charAt(charIndex);// 计算当前位置int cx = x - columnIndex * columnWidth;int cy = y + rowIndex * fontHeight + rowIndex * verticalSpacing; // 加入垂直偏移量// 绘制文字textGraphics.drawString(String.valueOf(ch), cx, cy);}}}}
4:生成效果
相关文章:

使用 image-combiner 开源项目实现对海报图片的生成
1:gitee 项目地址 image-combiner: ImageCombiner是一个专门用于Java服务端图片合成的工具,没有很复杂的功能,简单实用,从实际业务场景出发,提供简单的接口,几行代码即可实现图片拼合(当然用于…...

【缓存】框架层常见问题和对策
缓存是为了加快读写速度,再了解redis这类框架层的缓存应用之前,我们不妨先思考下操作系统层面的缓存解决方案,这样有助于我们更深的理解缓存,哪些是系统层面的,哪些是服务层面。 以下是一些常见的缓存问题及其解决方案…...

【FAS】《CN103106397B》
原文 CN103106397B-基于亮瞳效应的人脸活体检测方法-授权-2013.01.19 华南理工大学 方法 / 点评 核心方法用的是传统的形态学和模板匹配,亮点是双红外发射器做差分 差分:所述FPGA芯片控制两组红外光源(一近一远)交替亮灭&…...

3D按F3为什么显示不出模型?---模大狮模型网
对于3D建模软件的用户来说,按下F3键通常是用来显示或隐藏模型的功能之一。然而,有时当按下F3键时,却无法正确显示模型,这可能会让用户感到困惑。模大狮将探讨这种情况发生的可能原因以及解决方法,帮助设计师们更好地理…...

C++设计模式——Adapter适配器模式
一,适配器模式简介 适配器模式是一种结构型设计模式,用于将已有接口转换为调用者所期望的另一种接口。 适配器模式让特定的API接口可以适配多种场景。例如,现有一个名为"Reader()"的API接口只能解析txt格式的文件,给这…...

Python文本处理利器:jieba库全解析
文章目录 Python文本处理利器:jieba库全解析第一部分:背景和功能介绍第二部分:库的概述第三部分:安装方法第四部分:常用库函数介绍1. 精确模式分词2. 全模式分词3. 搜索引擎模式分词4. 添加自定义词典5. 关键词提取 第…...

【C/C++】C语言如何实现类似C++的智能指针?
在C中,智能指针是为了自动化资源管理而引入的工具。比如std::unique_ptr和std::shared_ptr等,它们管理着所持有对象的生命周期,可以在智能指针被销毁时自动释放其所持有的资源。在C语言中,虽然没有直接的智能指针概念,…...

九大微服务监控工具详解
Prometheus Prometheus 是一个开源的系统监控、和报警工具包,Prometheus 被设计用来监控“微服务架构”。 主要解决: 监控和告警:Prometheus 可以对系统、和应用程序进行实时监控,并在出现问题时发送告警;数据收集和…...

java aliyun oss上传和下载工具类
java aliyun oss上传和下载工具类 依赖 <dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.8.0</version></dependency>工具类 import com.alibaba.fastjson.JSON; import c…...

P7 品牌管理
逆向生成页面 新增菜单—商品系统的品牌管理 —product/brand 在代码生成器得到的文件中, main-resources-src-views-modules-product brand.vue、brand-add-or-update.vue放到category.vue同级vue文件有新增、删除按钮,但页面未显示,是因…...

C语言详解(动态内存管理)1
Hi~!这里是奋斗的小羊,很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~~ 💥💥个人主页:奋斗的小羊 💥💥所属专栏:C语言 🚀本系列文章为个人学习…...

106.网络游戏逆向分析与漏洞攻防-装备系统数据分析-在UI中显示装备与技能信息
免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 如果看不懂、不知道现在做的什么,那就跟着做完看效果,代码看不懂是正常的,只要会抄就行,抄着抄着就能懂了 内容…...

AWS EMR Serverless
AWS概述 EMR Serverless 简介 在AWS概述一文中简单介绍过AWS EMR, 它是AWS提供的云端大数据平台。借助EMR可以设置集群以便在几分钟内使用大数据框架处理和分析数据。创建集群可参考官方文档:Amazon EMR 入门。但集群创建之后需要一直运行,用户需要管理…...

Java面试题:Redis持久化问题
Redis持久化问题 RDB (Redis Database Backup File) Redis数据快照 将内存中的所有数据都记录到磁盘中做快照 当Redis实例故障重启时,从磁盘读取快照文件恢复数据 使用 save/bgsave命令进行手动快照 save使用主进程执行RDB,对所有命令都进行阻塞 bgsave使用子进程执行R…...

【Java】解决Java报错:ClassCastException
文章目录 引言1. 错误详解2. 常见的出错场景2.1 错误的类型转换2.2 泛型集合中的类型转换2.3 自定义类和接口转换 3. 解决方案3.1 使用 instanceof 检查类型3.2 使用泛型3.3 避免不必要的类型转换 4. 预防措施4.1 使用泛型和注解4.2 编写防御性代码4.3 使用注解和检查工具 5. 示…...

OpenCV-最小外接圆cv::minEnclosingCircle
作者:翟天保Steven 版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处 函数原型 void minEnclosingCircle(InputArray points, Point2f& center, float& radius); 参数说明 InputArray类型的…...

大小堆运用巧解数据流的中位数
一、思路 我们将所有数据平分成两份,前面那一部分用小堆来存,后面的部分用大堆来存,这样我们就能立刻拿到中间位置的值。 如果是奇数个数字,那么我们就将把中间值放在前面的大堆里,所以会有两种…...

AI能力边界不断扩展,将对国家安全产生深远影响
文 | 中国信息安全测评中心 王欣 随着 ChatGPT 的发布及相关应用的落地,人工智能技术给全球各个行业带来了一波又一波冲击。GPT-4 多模态大型语言模型更是将人工智能的能力提升到新的高度,无论从技术先进性还是应用实践能力来看,此模型均可被…...

【UnityShader入门精要学习笔记】第十六章 Unity中的渲染优化技术 (上)
本系列为作者学习UnityShader入门精要而作的笔记,内容将包括: 书本中句子照抄 个人批注项目源码一堆新手会犯的错误潜在的太监断更,有始无终 我的GitHub仓库 总之适用于同样开始学习Shader的同学们进行有取舍的参考。 文章目录 移动平台上…...

GPT-4o:免费且更快的模型
OpenAI GPT-4o 公告 OpenAI 推出了增强版 GPT-4 模型——OpenAI GPT-4o,用于支持 ChatGPT。首席技术官 Mira Murati 表示,更新后的模型速度更快,并在文本、视觉和音频处理方面有了显著提升。GPT-4o 将免费向所有用户开放,付费用户…...

docker部署fastdfs
我的镜像包地址 链接:https://pan.baidu.com/s/1j5E5O1xdyQVfJhsOevXvYg?pwdhcav 提取码:hcav docker load -i gofast.tar.gz拉取gofast docker pull sjqzhang/go-fastdfs启动gofast docker run -d --name fastdfs -p 8080:8080 -v /opt/lijia/lijia…...

【劲舞团game】
编写《劲舞团》这样的游戏代码是一个复杂的过程,涉及到游戏引擎的使用、图形渲染、物理模拟、音频处理、网络通信等多个方面。以下是一个非常简化的步骤,用于说明如何开始编写一个基于Unity引擎的简单舞蹈游戏: 1. 准备开发环境 下载并安装…...

Day15—图像爬虫与简单处理
图像爬虫是一种专门用于从互联网上下载图像的网络爬虫。除了文本内容,图像也是网站中的重要组成部分,它们可以用于多种目的,如图像识别、内容分析、数据备份等。 环境准备 首先,确保你的环境中已安装Python和必要的库。如果没有安装Pillow库,可以通过以下命令安装:pip in…...

Rust基础学习-Rust中的文件操作
文件结构 在Rust中,std::fs::File 结构体代表一个文件。它允许我们对文件执行读/写操作。文件 I/O 是通过提供与文件系统交互的功能的 std::fs 模块执行的。 File 结构体中的所有方法都返回std::io::Result的变体,或者简单地是 Result 枚举。这里会涉及…...

Activator.CreateInstance 与 Type.InvokeMember的区别
文章目录 一、使用 Activator.CreateInstance 创建实例1、使用 Activator.CreateInstance 的优点和缺点2、使用 Activator.CreateInstance 的代码示例 二、使用 Type.InvokeMember 创建实例1、使用 Type.InvokeMember 的优点和缺点2、使用 Type.InvokeMember 的代码示例 三、Ac…...

Java18+App端采用uniapp+开发工具 idea hbuilder智能上门家政系统源码,一站式家政服务平台开发家政服务
Java18App端采用uniapp开发工具 idea hbuilder智能上门家政系统源码,一站式家政服务平台开发 家政服务 家政服务是一个专为家政服务人员设计的平台,该平台旨在提供便捷、高效的工作机会,同时确保服务质量和客户体验。 以下是关于家政服务师…...

【MySQL】探索 MySQL 的 GROUP_CONCAT 函数
缘分让我们相遇乱世以外 命运却要我们危难中相爱 也许未来遥远在光年之外 我愿守候未知里为你等待 我没想到为了你我能疯狂到 山崩海啸没有你根本不想逃 我的大脑为了你已经疯狂到 脉搏心跳没有你根本不重要 🎵 邓紫棋《光年之外》 什么是 GRO…...

SpringBoot整合RabbitMQ (持续更新中)
RabbitMQ 官网地址:RabbitMQ: One broker to queue them all | RabbitMQ RabbitMQ 与 Erlang 版本兼容关系 3.13.0 26.0 26.2.x The 3.13 release series is compatible with Erlang 26. OpenSSL 3 support in Erlang is considered to be mature and ready for…...

瑞鑫RK3588 画中画 OSD 效果展示
这些功能本来在1126平台都实现过 但是迁移到3588平台之后 发现 API接口变化较大 主要开始的时候会比较费时间 需要找到变动接口对应的新接口 之后 就比较好操作了 经过几天的操作 已实现 效果如下...

【全开源】防伪溯源一体化管理系统源码(FastAdmin+ThinkPHP+Uniapp)
🔍防伪溯源一体化管理系统:守护品质,追溯无忧 一款基于FastAdminThinkPHP和Uniapp进行开发的多平台(微信小程序、H5网页)溯源、防伪、管理一体化独立系统,拥有强大的防伪码和溯源码双码生成功能࿰…...