前面Java 實現長圖文生成 中實現了一個基本的長圖文生成工具,但遺留了一些問題java
其中英文字符的計算已經修復,主要是經過FontMetric
來計算字符串實際佔用繪製的長度,這一塊不作多講,本篇主要集中在豎排文字的支持git
有前面的基礎,在作豎排文字支持上,本覺得是比較簡單就能接入的,而實際的實現過程當中,頗爲坎坷github
首先須要支持豎排文字的繪製,使用Graphics2d
進行繪製時,暫不支持豎排繪製方式,所以咱們須要本身來實現app
而設計思路也比較簡單,一個字一個字的繪製,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(); } }
豎排的自動換行相比較與水平有點麻煩的是間隔問題,首先看下FontMertric
的幾個參數 ascent
, descent
, height
測試
舉一個例子來看如何進行自動換行字體
// 列容量 contain = 100 // FontMetric 相關信息: fontMetric.ascent = 18; fontMetric.descent = 4; fontMetric.height = 22; // 待繪製的內容爲 content = "這是一個待繪製的文本長度,期待自動換行";
首先咱們是須要獲取內容的總長度,中文還比較好說,都是方塊的,能夠直接用 fontMetrics.stringWidth(content)
獲取內容長度(實際爲寬度),而後須要加空格(即descent
)ui
因此計算最終的行數能夠以下this
// 72 int l = fontMetrics.getDescent() * (content.length() - 1); // 5 int lineNum = (int) Math.ceil((fontMetrics.stringWidth(str) + l) / (float) lineLen);
根據上面的計算, l=72, lineNum=5;
.net
而後就是一個字符一個字符的進行繪製,每次須要從新計算y座標
tmpLen = fontMetrics.charWidth(str.charAt(i)) + fontMetrics.getDescent();
其次就是須要判斷是否要換行
lastTotal += tmpLen; if(lastTotal > contain) { // 換行 }
從左到右還比較好說,y座標一直增長,當繪製的內容超過當前的圖片時,直接在擴展後的圖片上(0,0)位置進行繪製便可;
而從右到左則須要計算偏移量,以下圖
實現一個公共方法,根據上面的思路用於文本的自動換行
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
的值,不然換行偏移會有問題
由於咱們支持集中不一樣的對齊方式,因此在計算起始的y座標時,會有出入, 實現以下
/** * 垂直繪製時,根據不一樣的對其方式,計算起始的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; } }
實際繪製中,y座標還不能直接使用上面返回值,由於這個返回是字體的最上邊對應的座標,所以須要將實際繪製y座標,向下偏移一個字
realY = calOffsetY(xxx) + fontMetrics.getAscent(); //... // 每當繪製完一個文本後,下個文本的Y座標,須要加上這個文本所佔用的高度+間距 realY += fontMetrics.charWidth(tmp.charAt(i)) + g2d.getFontMetrics().getDescent();
繪製方式的不一樣,從左到右與從右到左兩種場景下,自動換行後,新行的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; } }
文本繪製實現以後,再來看圖片,就簡單不少了,由於沒有換行的問題,因此只須要計算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; }
正如前面一篇博文中實現的水平圖文生成的邏輯同樣,垂直圖文生成也採用以前的思路:
由於從左到右和從右到左的繪製在計算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; }
對比從左到右與從右到左,區別主要是兩點
新寬度-舊寬度
上面是文本繪製,圖片繪製比較簡單,基本上和水平繪製時,沒什麼區別,只不過是擴充時的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; }
上面是繪製的過程,繪製完畢以後,須要輸出爲圖片的,所以對於這個輸出須要再適配一把
再前一篇的基礎上,輸出新增了簽名+背景的支持,這裏一併說了
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
我的博客:一灰的我的博客
公衆號獲取更多: