背景
前面Java 實現長圖文生成 中實現了一個基本的長圖文生成工具,但遺留了一些問題
- 文字中包含英文字符時,分行計算問題
- 暫不支持豎排文字展示
其中英文字符的計算已經修復,主要是通過FontMetric
來計算字符串實際占用繪制的長度,這一塊不做多講,本篇主要集中在豎排文字的支持
設計
有前面的基礎,在做豎排文字支持上,本以為是比較簡單就能接入的,而實際的實現過程中,頗為坎坷
1. 豎排文字繪制
首先需要支持豎排文字的繪制,使用Graphics2d
進行繪制時,暫不支持豎排繪制方式,因此我們需要自己來實現
而設計思路也比較簡單,一個字一個字的繪制,x坐標不變,y坐標依次增加
private void draw(Graphics2D g2d, String content, int x, int y, FontMetrics fontMetrics) {
int lastY = y;
for (int i = 0; i < content.length(); i ++) {
g2d.drawString(content.charAt(i) + "", x, lastY);
lastY += fontMetrics.charWidth(content.charAt(i)) + fontMetrics.getDescent();
}
}
2. 自動換行
豎排的自動換行相比較與水平有點麻煩的是間隔問題,首先看下FontMertric
的幾個參數 ascent
, descent
, height
舉一個例子來看如何進行自動換行
// 列容量
contain = 100
// FontMetric 相關信息:
fontMetric.ascent = 18;
fontMetric.descent = 4;
fontMetric.height = 22;
// 待繪制的內容為
content = "這是一個待繪制的文本長度,期待自動換行";
首先我們是需要獲取內容的總長度,中文還比較好說,都是方塊的,可以直接用 fontMetrics.stringWidth(content)
獲取內容長度(實際為寬度),然后需要加空格(即descent
)
所以計算最終的行數可以如下
// 72
int l = fontMetrics.getDescent() * (content.length() - 1);
// 5
int lineNum = (int) Math.ceil((fontMetrics.stringWidth(str) + l) / (float) lineLen);
根據上面的計算, l=72, lineNum=5;
然后就是一個字符一個字符的進行繪制,每次需要重新計算y坐標
tmpLen = fontMetrics.charWidth(str.charAt(i)) + fontMetrics.getDescent();
其次就是需要判斷是否要換行
lastTotal += tmpLen;
if(lastTotal > contain) {
// 換行
}
3. 從右到左支持
從左到右還比較好說,y坐標一直增加,當繪制的內容超過當前的圖片時,直接在擴展后的圖片上(0,0)位置進行繪制即可;
而從右到左則需要計算偏移量,如下圖
實現
1. 文本自動換行
實現一個公共方法,根據上面的思路用于文本的自動換行
public static String[] splitVerticalStr(String str, int lineLen, FontMetrics fontMetrics) {
// 字體間距所占用的高度
int l = fontMetrics.getDescent() * (str.length() - 1);
// 分的行數
int lineNum = (int) Math.ceil((fontMetrics.stringWidth(str) + l) / (float) lineLen);
if (lineNum == 1) {
return new String[]{str};
}
String[] ans = new String[lineNum];
int strLen = str.length();
int lastTotal = 0;
int lastIndex = 0;
int ansIndex = 0;
int tmpLen;
for (int i = 0; i < strLen; i++) {
tmpLen = fontMetrics.charWidth(str.charAt(i)) + fontMetrics.getDescent();
lastTotal += tmpLen;
if (lastTotal > lineLen) {
ans[ansIndex++] = str.substring(lastIndex, i);
lastIndex = i;
lastTotal = tmpLen;
}
}
if (lastIndex < strLen) {
ans[ansIndex] = str.substring(lastIndex);
}
return ans;
}
上面的實現,唯一需要注意的是,換行時,y坐標自增的場景下,需要計算 fontMetric.descent
的值,否則換行偏移會有問題
2. 垂直文本的繪制
1. 起始y坐標計算
因為我們支持集中不同的對齊方式,所以在計算起始的y坐標時,會有出入, 實現如下
- 上對齊,則 y = 上邊距
- 下對其, 則 y = 總高度 - 內容高度 - 下邊距
- 居中, 則 y = (總高度 - 內容高度) / 2
/**
* 垂直繪制時,根據不同的對其方式,計算起始的y坐標
*
* @param topPadding 上邊距
* @param bottomPadding 下邊距
* @param height 總高度
* @param strSize 文本內容對應繪制的高度
* @param style 對其樣式
* @return
*/
private static int calOffsetY(int topPadding, int bottomPadding, int height, int strSize, ImgCreateOptions.AlignStyle style) {
if (style == ImgCreateOptions.AlignStyle.TOP) {
return topPadding;
} else if (style == ImgCreateOptions.AlignStyle.BOTTOM) {
return height - bottomPadding - strSize;
} else {
return (height - strSize) >> 1;
}
}
2. 實際繪制y坐標計算
實際繪制中,y坐標還不能直接使用上面返回值,因為這個返回是字體的最上邊對應的坐標,因此需要將實際繪制y坐標,向下偏移一個字
realY = calOffsetY(xxx) + fontMetrics.getAscent();
//...
// 每當繪制完一個文本后,下個文本的Y坐標,需要加上這個文本所占用的高度+間距
realY += fontMetrics.charWidth(tmp.charAt(i)) + g2d.getFontMetrics().getDescent();
3. 換行時,x坐標計算
繪制方式的不同,從左到右與從右到左兩種場景下,自動換行后,新行的x坐標的增量計算方式也是不同的
- 從左到右:
int fontWidth = 字體寬度 + 行間距
- 從右到左:
int fontWidth = - (字體寬度 + 行間距)
完整的實現邏輯如下
/**
* 垂直文字繪制
*
* @param g2d
* @param content 待繪制的內容
* @param x 繪制的起始x坐標
* @param options 配置項
*/
public static void drawVerticalContent(Graphics2D g2d,
String content,
int x,
ImgCreateOptions options) {
int topPadding = options.getTopPadding();
int bottomPadding = options.getBottomPadding();
g2d.setFont(options.getFont());
FontMetrics fontMetrics = g2d.getFontMetrics();
// 實際填充內容的高度, 需要排除上下間距
int contentH = options.getImgH() - options.getTopPadding() - options.getBottomPadding();
String[] strs = splitVerticalStr(content, contentH, g2d.getFontMetrics());
int fontWidth = options.getFont().getSize() + options.getLinePadding();
if (options.getDrawStyle() == ImgCreateOptions.DrawStyle.VERTICAL_RIGHT) { // 從右往左繪制時,偏移量為負
fontWidth = -fontWidth;
}
g2d.setColor(options.getFontColor());
int lastX = x, lastY, startY;
for (String tmp : strs) {
lastY = 0;
startY = calOffsetY(topPadding, bottomPadding, options.getImgH(),
fontMetrics.stringWidth(tmp) + fontMetrics.getDescent() * (tmp.length() - 1), options.getAlignStyle())
+ fontMetrics.getAscent();
for (int i = 0; i < tmp.length(); i++) {
g2d.drawString(tmp.charAt(i) + "",
lastX,
startY + lastY);
lastY += g2d.getFontMetrics().charWidth(tmp.charAt(i)) + g2d.getFontMetrics().getDescent();
}
lastX += fontWidth;
}
}
3. 垂直圖片繪制
文本繪制實現之后,再來看圖片,就簡單很多了,因為沒有換行的問題,所以只需要計算y坐標的值即可
此外當圖片大于參數指定的高度時,對圖片進行按照高度進行縮放處理;當小于高度時,就原圖繪制即可
實現邏輯如下
public static int drawVerticalImage(BufferedImage source,
BufferedImage dest,
int x,
ImgCreateOptions options) {
Graphics2D g2d = getG2d(source);
int h = Math.min(dest.getHeight(), options.getImgH() - options.getTopPadding() - options.getBottomPadding());
int w = h * dest.getWidth() / dest.getHeight();
int y = calOffsetY(options.getTopPadding(),
options.getBottomPadding(),
options.getImgH(),
h,
options.getAlignStyle());
// xxx 傳入的x坐標,即 contentW 實際上已經包含了行間隔,因此不需額外添加
int drawX = x;
if (options.getDrawStyle() == ImgCreateOptions.DrawStyle.VERTICAL_RIGHT) {
drawX = source.getWidth() - w - drawX;
}
g2d.drawImage(dest, drawX, y, w, h, null);
g2d.dispose();
return w;
}
4. 封裝類的實現
正如前面一篇博文中實現的水平圖文生成的邏輯一樣,垂直圖文生成也采用之前的思路:
- 每次在文本繪制時,直接進行渲染;
- 記錄實際內容繪制的寬度(這個寬度包括左or右邊距)
- 每次繪制時,判斷當前的畫布是否容納得下所有的內容
- 容的下,直接繪制即可
- 容不下,則需要擴充畫布,生成一個更寬的畫布,將原來的內容重新渲染在新畫布上,然后在新畫布上進行內容的填充
因為從左到右和從右到左的繪制在計算x坐標的增量時,擴充畫布的重新繪制時,有些明顯的區別,所以為了邏輯清晰,將兩種場景分開,提供了兩個方法
實現步驟:
- 計算實際繪制內容占用的寬度
- 判斷是否需要擴充畫布(需要則擴充)
- 繪制文本
- 更新內容的寬度
private Builder drawVerticalLeftContent(String content) {
if (contentW == 0) { // 初始化邊距
contentW = options.getLeftPadding();
}
Graphics2D g2d = GraphicUtil.getG2d(result);
g2d.setFont(options.getFont());
FontMetrics fontMetrics = g2d.getFontMetrics();
String[] strs = StringUtils.split(content, "\n");
if (strs.length == 0) { // empty line
strs = new String[1];
strs[0] = " ";
}
int fontSize = fontMetrics.getFont().getSize();
int lineNum = GraphicUtil.calVerticalLineNum(strs, options.getImgH() - options.getBottomPadding() - options.getTopPadding(), fontMetrics);
// 計算填寫內容需要占用的寬度
int width = lineNum * (fontSize + options.getLinePadding());
if (result == null) {
result = GraphicUtil.createImg(
Math.max(width + options.getRightPadding() + options.getLeftPadding(), BASE_ADD_H),
options.getImgH(),
null);
g2d = GraphicUtil.getG2d(result);
} else if (result.getWidth() < contentW + width + options.getRightPadding()) {
// 超過原來圖片寬度的上限, 則需要擴充圖片長度
result = GraphicUtil.createImg(
result.getWidth() + Math.max(width + options.getRightPadding(), BASE_ADD_H),
options.getImgH(),
result);
g2d = GraphicUtil.getG2d(result);
}
// 繪制文字
int index = 0;
for (String str : strs) {
GraphicUtil.drawVerticalContent(g2d, str,
contentW + (fontSize + options.getLinePadding()) * (index ++)
, options);
}
g2d.dispose();
contentW += width;
return this;
}
private Builder drawVerticalRightContent(String content) {
if(contentW == 0) {
contentW = options.getRightPadding();
}
Graphics2D g2d = GraphicUtil.getG2d(result);
g2d.setFont(options.getFont());
FontMetrics fontMetrics = g2d.getFontMetrics();
String[] strs = StringUtils.split(content, "\n");
if (strs.length == 0) { // empty line
strs = new String[1];
strs[0] = " ";
}
int fontSize = fontMetrics.getFont().getSize();
int lineNum = GraphicUtil.calVerticalLineNum(strs, options.getImgH() - options.getBottomPadding() - options.getTopPadding(), fontMetrics);
// 計算填寫內容需要占用的寬度
int width = lineNum * (fontSize + options.getLinePadding());
if (result == null) {
result = GraphicUtil.createImg(
Math.max(width + options.getRightPadding() + options.getLeftPadding(), BASE_ADD_H),
options.getImgH(),
null);
g2d = GraphicUtil.getG2d(result);
} else if (result.getWidth() < contentW + width + options.getLeftPadding()) {
// 超過原來圖片寬度的上限, 則需要擴充圖片長度
int newW = result.getWidth() + Math.max(width + options.getLeftPadding(), BASE_ADD_H);
result = GraphicUtil.createImg(
newW,
options.getImgH(),
newW - result.getWidth(),
0,
result);
g2d = GraphicUtil.getG2d(result);
}
// 繪制文字
int index = 0;
int offsetX = result.getWidth() - contentW;
for (String str : strs) {
GraphicUtil.drawVerticalContent(g2d, str,
offsetX - (fontSize + options.getLinePadding()) * (++index)
, options);
}
g2d.dispose();
contentW += width;
return this;
}
對比從左到右與從右到左,區別主要是兩點
- 擴充時,在新畫布上繪制原畫布內容的x坐標計算,一個為0,一個為
新寬度-舊寬度
- offsetX 的計算
上面是文本繪制,圖片繪制比較簡單,基本上和水平繪制時,沒什么區別,只不過是擴充時的w,h計算不同罷了
private Builder drawVerticalImage(BufferedImage bufferedImage) {
int padding = options.getDrawStyle() == ImgCreateOptions.DrawStyle.VERTICAL_RIGHT ? options.getLeftPadding() : options.getRightPadding();
// 實際繪制圖片的寬度
int bfImgW = bufferedImage.getHeight() > options.getImgH() ? bufferedImage.getWidth() * options.getImgH() / bufferedImage.getHeight() : bufferedImage.getWidth();
if(result == null) {
result = GraphicUtil.createImg(
Math.max(bfImgW + options.getLeftPadding() + options.getRightPadding(), BASE_ADD_H),
options.getImgH(),
null);
} else if (result.getWidth() < contentW + bfImgW + padding) {
int realW = result.getWidth() + Math.max(bfImgW + options.getLeftPadding() + options.getRightPadding(), BASE_ADD_H);
int offsetX = options.getDrawStyle() == ImgCreateOptions.DrawStyle.VERTICAL_RIGHT ? realW - result.getWidth() : 0;
result = GraphicUtil.createImg(
realW,
options.getImgH(),
offsetX,
0,
null);
}
int w = GraphicUtil.drawVerticalImage(result, bufferedImage, contentW, options);
contentW += w + options.getLinePadding();
return this;
}
5. 輸出
上面是繪制的過程,繪制完畢之后,需要輸出為圖片的,因此對于這個輸出需要再適配一把
再前一篇的基礎上,輸出新增了簽名+背景的支持,這里一并說了
- 計算生成圖片的寬高
- 有簽名時,繪制簽名背景,在最下方繪制簽名文本
- 背景圖片
- 繪制填充內容
public BufferedImage asImage() {
int leftPadding = 0;
int topPadding = 0;
int bottomPadding = 0;
if (border) {
leftPadding = this.borderLeftPadding;
topPadding = this.borderTopPadding;
bottomPadding = this.borderBottomPadding;
}
int x = leftPadding;
int y = topPadding;
// 實際生成圖片的寬, 高
int realW, realH;
if (options.getImgW() == null) { // 垂直文本輸出
realW = contentW + options.getLeftPadding() + options.getRightPadding();
realH = options.getImgH();
} else { // 水平文本輸出
realW = options.getImgW();
realH = contentH + options.getBottomPadding();
}
BufferedImage bf = new BufferedImage((leftPadding << 1) + realW, realH + topPadding + bottomPadding, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = GraphicUtil.getG2d(bf);
// 繪制邊框
if (border) {
g2d.setColor(borderColor == null ? ColorUtil.OFF_WHITE : borderColor);
g2d.fillRect(0, 0, realW + (leftPadding << 1), realH + topPadding + bottomPadding);
// 繪制簽名
g2d.setColor(Color.GRAY);
// 圖片生成時間
String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
borderSignText = borderSignText + " " + date;
int fSize = Math.min(15, realW / (borderSignText.length()));
int addY = (borderBottomPadding - fSize) >> 1;
g2d.setFont(new Font(ImgCreateOptions.DEFAULT_FONT.getName(), ImgCreateOptions.DEFAULT_FONT.getStyle(), fSize));
g2d.drawString(borderSignText, x, y + addY + realH + g2d.getFontMetrics().getAscent());
}
// 繪制背景
if (options.getBgImg() == null) {
g2d.setColor(bgColor == null ? Color.WHITE : bgColor);
g2d.fillRect(x, y, realW, realH);
} else {
g2d.drawImage(options.getBgImg(), x, y, realW, realH, null);
}
// 繪制內容
if (options.getDrawStyle() == ImgCreateOptions.DrawStyle.VERTICAL_RIGHT) {
x = bf.getWidth() - result.getWidth() - x;
}
g2d.drawImage(result, x, y, null);
g2d.dispose();
return bf;
}
測試
測試case
@Test
public void testLocalGenVerticalImg() throws IOException {
int h = 300;
int leftPadding = 10;
int topPadding = 10;
int bottomPadding = 10;
int linePadding = 10;
Font font = new Font("手札體", Font.PLAIN, 18);
ImgCreateWrapper.Builder build = ImgCreateWrapper.build()
.setImgH(h)
.setDrawStyle(ImgCreateOptions.DrawStyle.VERTICAL_LEFT)
.setLeftPadding(leftPadding)
.setTopPadding(topPadding)
.setBottomPadding(bottomPadding)
.setLinePadding(linePadding)
.setFont(font)
.setAlignStyle(ImgCreateOptions.AlignStyle.TOP)
.setBgColor(Color.WHITE)
.setBorder(true)
.setBorderColor(0xFFF7EED6)
;
BufferedReader reader = FileReadUtil.createLineRead("text/poem.txt");
String line;
while ((line = reader.readLine()) != null) {
build.drawContent(line);
}
build.setAlignStyle(ImgCreateOptions.AlignStyle.BOTTOM)
.drawImage("/Users/yihui/Desktop/sina_out.jpg");
build.setFontColor(Color.BLUE).drawContent("后綴簽名").drawContent("灰灰自動生成");
BufferedImage img = build.asImage();
ImageIO.write(img, "png", new File("/Users/yihui/Desktop/2out.png"));
}
輸出圖片
再輸出一個從右到左的,居中顯示樣式
補充一張,豎排文字時,標點符號應該居右(之前完全沒意識到),修正的圖片樣式如下
其他
相關博文:《Java 實現長圖文生成》
項目地址:https://github.com/liuyueyi/quick-media
個人博客:一灰的個人博客
公眾號獲取更多: