(() -> {
+ pProcess.waitFor();
+ return pProcess.exitValue();
+ });
+ exec.submit(processTask);
+
+
+ // 设置超时时间,防止死等
+ int rc = processTask.get(EXEC_TIME_OUT, TimeUnit.SECONDS);
+
+
+ // just to be on the safe side
+ try {
+ pProcess.getInputStream().close();
+ pProcess.getOutputStream().close();
+ pProcess.getErrorStream().close();
+ } catch (Exception e) {
+ log.error("close stream error! e: {}", e);
+ }
+
+ return rc;
+ }
+
+
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Let the OutputConsumer process the output of the command.
+ *
+ * 方便后续对输出流的扩展
+ */
+
+ private void processOutput(InputStream pInputStream,
+ InputStreamConsumer pConsumer) throws IOException {
+ pConsumer.consume(pInputStream);
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Let the ErrorConsumer process the stderr-stream.
+ *
+ * 方便对后续异常流的处理
+ */
+
+ private void processError(InputStream pInputStream,
+ InputStreamConsumer pConsumer) throws IOException {
+ pConsumer.consume(pInputStream);
+ }
+
+
+ private static class InputStreamConsumer {
+ static ProcessUtil instance = new ProcessUtil();
+
+ static InputStreamConsumer DEFAULT_CONSUMER = new InputStreamConsumer();
+
+ void consume(InputStream stream) throws IOException {
+ StringBuilder builder = new StringBuilder();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream), BUFFER_SIZE);
+
+ String temp;
+ while ((temp = reader.readLine()) != null) {
+ builder.append(temp);
+ }
+
+
+ if (log.isDebugEnabled()) {
+ log.debug("cmd process input stream: {}", builder.toString());
+ }
+ reader.close();
+ }
+ }
+
+
+ private static class CustomThreadFactory implements ThreadFactory {
+
+ private String name;
+
+ private AtomicInteger count = new AtomicInteger(0);
+
+ public CustomThreadFactory(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public Thread newThread(Runnable r) {
+ return new Thread(r, name + "-" + count.addAndGet(1));
+ }
+ }
+
+}
+```
+
+**说明**
+
+- 内部类方式的单例模式
+- 线程池,开独立的线程来处理命令行的输出流、异常流
+ - 如果不清空这两个流,可能直接导致rt随着并发数的增加而线性增加
+- 独立的线程执行命令行操作,支持超时设置
+ - 超时设置,确保服务不会挂住
+ - 异步执行命令行操作,可以并发执行后续的步骤
+
+
+## 填坑之旅
+
+上面实现了一个较好用的封装类,但是在实际的开发过程中,有些问题有必要单独的拎出来说一说
+
+### 1. `-y` 参数
+> 覆盖写,如果输出的文件名对应的文件已经存在,这个参数就表示使用新的文件覆盖老的
+
+在控制台执行转码时,会发现这种场景会要求用户输入一个y/n来表是否继续转码,所以在代码中,如果不加上这个参数,将一直得不到执行
+
+
+### 2. mac/ios 的音频长度与实际不符合
+
+将 amr 音频转换 mp3 格式音频,如果直接使用命令`ffmpeg -i test.amr -y out.mp3`
+
+会发现输出的音频时间长度比实际的小,但是在播放的时候又是没有问题的;测试在mac和iphone会有这个问题
+
+解决方案,加一个参数 `write_xing 0`
+
+
+### 3. 并发访问时,RT线性增加
+
+执行命令: `ffmpeg -i song.ogg -y -write_xing 0 song.mp3`
+
+当我们没有手动清空输出流,异常流时,会发现并发请求量越高,rt越高
+
+主要原因是输出信息 & 异常信息没有被消费,而缓存这些数据的空间是有限制的,因此上面我们的`ProcessUtil`类中,有两个任务来处理输出流和异常流
+
+还有一种方法就是加一个参数
+
+`ffmpeg -i song.ogg -y -write_xing 0 song.mp3 -loglevel quiet`
+
+
+## 其他
+
+项目源码: [https://github.com/liuyueyi/quick-media](https://github.com/liuyueyi/quick-media)
diff --git "a/docs/\346\217\222\344\273\266/audio/\346\246\202\350\247\210.md" "b/docs/\346\217\222\344\273\266/audio/\346\246\202\350\247\210.md"
new file mode 100644
index 00000000..3bf12917
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/audio/\346\246\202\350\247\210.md"
@@ -0,0 +1 @@
+# 概览
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/date/\346\246\202\350\247\210.md" "b/docs/\346\217\222\344\273\266/date/\346\246\202\350\247\210.md"
new file mode 100644
index 00000000..3bf12917
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/date/\346\246\202\350\247\210.md"
@@ -0,0 +1 @@
+# 概览
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/image/Java\345\256\236\347\216\260\347\253\226\346\216\222\351\225\277\345\233\276\346\226\207\347\224\237\346\210\220.md" "b/docs/\346\217\222\344\273\266/image/Java\345\256\236\347\216\260\347\253\226\346\216\222\351\225\277\345\233\276\346\226\207\347\224\237\346\210\220.md"
new file mode 100644
index 00000000..a70cf106
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/image/Java\345\256\236\347\216\260\347\253\226\346\216\222\351\225\277\345\233\276\346\226\207\347\224\237\346\210\220.md"
@@ -0,0 +1,586 @@
+# Java实现竖排长图文生成
+## 背景
+前面[Java 实现长图文生成](https://my.oschina.net/u/566591/blog/1514644) 中实现了一个基本的长图文生成工具,但遗留了一些问题
+
+- 文字中包含英文字符时,分行计算问题
+- 暂不支持竖排文字展示
+
+其中英文字符的计算已经修复,主要是通过`FontMetric`来计算字符串实际占用绘制的长度,这一块不做多讲,本篇主要集中在竖排文字的支持
+
+
+## 设计
+
+> 有前面的基础,在做竖排文字支持上,本以为是比较简单就能接入的,而实际的实现过程中,颇为坎坷
+
+### 1. 竖排文字绘制
+
+首先需要支持竖排文字的绘制,使用`Graphics2d`进行绘制时,暂不支持竖排绘制方式,因此我们需要自己来实现
+
+而设计思路也比较简单,一个字一个字的绘制,x坐标不变,y坐标依次增加
+
+```java
+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`
+
+![https://static.oschina.net/uploads/img/201709/05181941_gJBV.jpg](https://static.oschina.net/uploads/img/201709/05181941_gJBV.jpg)
+
+举一个例子来看如何进行自动换行
+
+```java
+// 列容量
+contain = 100
+
+// FontMetric 相关信息:
+fontMetric.ascent = 18;
+fontMetric.descent = 4;
+fontMetric.height = 22;
+
+// 待绘制的内容为
+content = "这是一个待绘制的文本长度,期待自动换行";
+```
+
+首先我们是需要获取内容的总长度,中文还比较好说,都是方块的,可以直接用 `fontMetrics.stringWidth(content)` 获取内容长度(实际为宽度),然后需要加空格(即`descent`)
+
+所以计算最终的行数可以如下
+
+```java
+// 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坐标
+
+```java
+tmpLen = fontMetrics.charWidth(str.charAt(i)) + fontMetrics.getDescent();
+```
+
+其次就是需要判断是否要换行
+
+```java
+lastTotal += tmpLen;
+if(lastTotal > contain) {
+ // 换行
+}
+```
+
+### 3. 从右到左支持
+
+从左到右还比较好说,y坐标一直增加,当绘制的内容超过当前的图片时,直接在扩展后的图片上(0,0)位置进行绘制即可;
+
+而从右到左则需要计算偏移量,如下图
+
+![offset](https://static.oschina.net/uploads/img/201709/05182209_f6NC.jpg)
+
+## 实现
+
+### 1. 文本自动换行
+
+实现一个公共方法,根据上面的思路用于文本的自动换行
+
+```java
+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
+
+```java
+/**
+ * 垂直绘制时,根据不同的对其方式,计算起始的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坐标,向下偏移一个字
+
+```java
+realY = calOffsetY(xxx) + fontMetrics.getAscent();
+
+//...
+
+// 每当绘制完一个文本后,下个文本的Y坐标,需要加上这个文本所占用的高度+间距
+realY += fontMetrics.charWidth(tmp.charAt(i)) + g2d.getFontMetrics().getDescent();
+```
+
+
+#### 3. 换行时,x坐标计算
+绘制方式的不同,从左到右与从右到左两种场景下,自动换行后,新行的x坐标的增量计算方式也是不同的
+
+- 从左到右:`int fontWidth = 字体宽度 + 行间距`
+- 从右到左:`int fontWidth = - (字体宽度 + 行间距)`
+
+
+
+#### 完整的实现逻辑如下
+
+```java
+ /**
+ * 垂直文字绘制
+ *
+ * @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坐标的值即可
+
+
+此外当图片大于参数指定的高度时,对图片进行按照高度进行缩放处理;当小于高度时,就原图绘制即可
+
+实现逻辑如下
+
+```java
+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坐标的增量时,扩充画布的重新绘制时,有些明显的区别,所以为了逻辑清晰,将两种场景分开,提供了两个方法
+
+实现步骤:
+
+1. 计算实际绘制内容占用的宽度
+2. 判断是否需要扩充画布(需要则扩充)
+3. 绘制文本
+4. 更新内容的宽度
+
+```java
+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计算不同罢了
+
+```java
+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,
+ result);
+ }
+
+ int w = GraphicUtil.drawVerticalImage(result, bufferedImage, contentW, options);
+ contentW += w + options.getLinePadding();
+ return this;
+}
+```
+
+### 5. 输出
+
+上面是绘制的过程,绘制完毕之后,需要输出为图片的,因此对于这个输出需要再适配一把
+
+再前一篇的基础上,输出新增了签名+背景的支持,这里一并说了
+
+- 计算生成图片的宽高
+- 有签名时,绘制签名背景,在最下方绘制签名文本
+- 背景图片
+- 绘制填充内容
+
+```java
+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
+
+```java
+@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"));
+}
+```
+
+输出图片
+
+![https://static.oschina.net/uploads/img/201709/05182105_2smp.jpg](https://static.oschina.net/uploads/img/201709/05182105_2smp.jpg)
+
+再输出一个从右到左的,居中显示样式
+
+![https://static.oschina.net/uploads/img/201709/05182138_My1E.png](https://static.oschina.net/uploads/img/201709/05182138_My1E.png)
+
+
+## 其他
+相关博文:[《Java 实现长图文生成》](https://my.oschina.net/u/566591/blog/1514644)
+
+项目地址:[https://github.com/liuyueyi/quick-media](https://github.com/liuyueyi/quick-media)
+
+
+个人博客:[一灰的个人博客](http://blog.zbang.online:8080)
+
+公众号获取更多:
+
+![个人信息](https://static.oschina.net/uploads/img/201708/12175649_wn2r.png "个人信息")
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/image/Java\345\256\236\347\216\260\351\225\277\345\233\276\346\226\207\347\224\237\346\210\220.md" "b/docs/\346\217\222\344\273\266/image/Java\345\256\236\347\216\260\351\225\277\345\233\276\346\226\207\347\224\237\346\210\220.md"
new file mode 100644
index 00000000..b4c9de4c
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/image/Java\345\256\236\347\216\260\351\225\277\345\233\276\346\226\207\347\224\237\346\210\220.md"
@@ -0,0 +1,721 @@
+# Java实现长图文生成
+> 很久很久以前,就觉得微博的长图文实现得非常有意思,将排版直接以最终的图片输出,收藏查看分享都很方便,现在则自己动手实现一个简单版本的
+
+## 目标
+
+首先定义下我们预期达到的目标:根据文字 + 图片生成长图文
+
+### 目标拆解
+
+- 支持大段文字生成图片
+- 支持插入图片
+- 支持上下左右边距设置
+- 支持字体选择
+- 支持字体颜色
+- 支持左对齐,居中,右对齐
+
+
+### 预期结果
+
+我们将通过spring-boot搭建一个生成长图文的http接口,通过传入参数来指定各种配置信息,下面是一个最终调用的示意图
+
+![演示图](https://static.oschina.net/uploads/img/201708/18180654_y9iv.gif "演示图")
+
+
+## 设计&实现
+
+> 长图文的生成,采用awt进行文字绘制和图片绘制
+
+
+### 1. 参数选项 `ImgCreateOptions`
+
+根据我们的预期目标,设定配置参数,基本上会包含以下参数
+
+```java
+@Getter
+@Setter
+@ToString
+public class ImgCreateOptions {
+
+ /**
+ * 绘制的背景图
+ */
+ private BufferedImage bgImg;
+
+
+ /**
+ * 生成图片的宽
+ */
+ private Integer imgW;
+
+
+ private Font font = new Font("宋体", Font.PLAIN, 18);
+
+ /**
+ * 字体色
+ */
+ private Color fontColor = Color.BLACK;
+
+
+ /**
+ * 两边边距
+ */
+ private int leftPadding;
+
+ /**
+ * 上边距
+ */
+ private int topPadding;
+
+ /**
+ * 底边距
+ */
+ private int bottomPadding;
+
+ /**
+ * 行距
+ */
+ private int linePadding;
+
+
+ private AlignStyle alignStyle;
+
+ /**
+ * 对齐方式
+ */
+ public enum AlignStyle {
+ LEFT,
+ CENTER,
+ RIGHT;
+
+
+ private static Map map = new HashMap<>();
+
+ static {
+ for(AlignStyle style: AlignStyle.values()) {
+ map.put(style.name(), style);
+ }
+ }
+
+
+ public static AlignStyle getStyle(String name) {
+ name = name.toUpperCase();
+ if (map.containsKey(name)) {
+ return map.get(name);
+ }
+
+ return LEFT;
+ }
+ }
+}
+```
+
+
+### 2. 封装类 `ImageCreateWrapper`
+
+封装配置参数的设置,绘制文本,绘制图片的操作方式,输出样式等接口
+
+```java
+public class ImgCreateWrapper {
+
+
+ public static Builder build() {
+ return new Builder();
+ }
+
+
+ public static class Builder {
+ /**
+ * 生成的图片创建参数
+ */
+ private ImgCreateOptions options = new ImgCreateOptions();
+
+
+ /**
+ * 输出的结果
+ */
+ private BufferedImage result;
+
+
+ private final int addH = 1000;
+
+
+ /**
+ * 实际填充的内容高度
+ */
+ private int contentH;
+
+
+ private Color bgColor;
+
+ public Builder setBgColor(int color) {
+ return setBgColor(ColorUtil.int2color(color));
+ }
+
+ /**
+ * 设置背景图
+ *
+ * @param bgColor
+ * @return
+ */
+ public Builder setBgColor(Color bgColor) {
+ this.bgColor = bgColor;
+ return this;
+ }
+
+
+ public Builder setBgImg(BufferedImage bgImg) {
+ options.setBgImg(bgImg);
+ return this;
+ }
+
+
+ public Builder setImgW(int w) {
+ options.setImgW(w);
+ return this;
+ }
+
+ public Builder setFont(Font font) {
+ options.setFont(font);
+ return this;
+ }
+
+ public Builder setFontName(String fontName) {
+ Font font = options.getFont();
+ options.setFont(new Font(fontName, font.getStyle(), font.getSize()));
+ return this;
+ }
+
+
+ public Builder setFontColor(int fontColor) {
+ return setFontColor(ColorUtil.int2color(fontColor));
+ }
+
+ public Builder setFontColor(Color fontColor) {
+ options.setFontColor(fontColor);
+ return this;
+ }
+
+ public Builder setFontSize(Integer fontSize) {
+ Font font = options.getFont();
+ options.setFont(new Font(font.getName(), font.getStyle(), fontSize));
+ return this;
+ }
+
+ public Builder setLeftPadding(int leftPadding) {
+ options.setLeftPadding(leftPadding);
+ return this;
+ }
+
+ public Builder setTopPadding(int topPadding) {
+ options.setTopPadding(topPadding);
+ contentH = topPadding;
+ return this;
+ }
+
+ public Builder setBottomPadding(int bottomPadding) {
+ options.setBottomPadding(bottomPadding);
+ return this;
+ }
+
+ public Builder setLinePadding(int linePadding) {
+ options.setLinePadding(linePadding);
+ return this;
+ }
+
+ public Builder setAlignStyle(String style) {
+ return setAlignStyle(ImgCreateOptions.AlignStyle.getStyle(style));
+ }
+
+ public Builder setAlignStyle(ImgCreateOptions.AlignStyle alignStyle) {
+ options.setAlignStyle(alignStyle);
+ return this;
+ }
+
+
+ public Builder drawContent(String content) {
+ // xxx
+ return this;
+ }
+
+
+ public Builder drawImage(String img) {
+ BufferedImage bfImg;
+ try {
+ bfImg = ImageUtil.getImageByPath(img);
+ } catch (IOException e) {
+ log.error("load draw img error! img: {}, e:{}", img, e);
+ throw new IllegalStateException("load draw img error! img: " + img, e);
+ }
+
+ return drawImage(bfImg);
+ }
+
+
+ public Builder drawImage(BufferedImage bufferedImage) {
+
+ // xxx
+ return this;
+ }
+
+
+ public BufferedImage asImage() {
+ int realH = contentH + options.getBottomPadding();
+
+ BufferedImage bf = new BufferedImage(options.getImgW(), realH, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g2d = bf.createGraphics();
+
+ if (options.getBgImg() == null) {
+ g2d.setColor(bgColor == null ? Color.WHITE : bgColor);
+ g2d.fillRect(0, 0, options.getImgW(), realH);
+ } else {
+ g2d.drawImage(options.getBgImg(), 0, 0, options.getImgW(), realH, null);
+ }
+
+ g2d.drawImage(result, 0, 0, null);
+ g2d.dispose();
+ return bf;
+ }
+
+
+ public String asString() throws IOException {
+ BufferedImage img = asImage();
+ return Base64Util.encode(img, "png");
+ }
+}
+```
+
+
+上面具体的文本和图片绘制实现没有,后面详细讲解,这里主要关注的是一个参数 `contentH`, 表示实际绘制的内容高度(包括上边距),因此最终生成图片的高度应该是
+
+`int realH = contentH + options.getBottomPadding();`
+
+
+其次简单说一下上面的图片输出方法:`com.hust.hui.quickmedia.common.image.ImgCreateWrapper.Builder#asImage`
+
+- 计算最终生成图片的高度(宽度由输入参数指定)
+- 绘制背景(如果没有背景图片,则用纯色填充)
+- 绘制实体内容(即绘制的文本,图片)
+
+
+### 3. 内容填充 `GraphicUtil`
+> 具体的内容填充,区分为文本绘制和图片绘制
+
+#### 设计
+
+1. 考虑到在填充的过程中,可以自由设置字体,颜色等,所以在我们的绘制方法中,直接实现掉内容的绘制填充,即 `drawXXX` 方法真正的实现了内容填充,执行完之后,内容已经填充到画布上了
+
+2. 图片绘制,考虑到图片本身大小和最终结果的大小可能有冲突,采用下面的规则
+ - 绘制图片宽度 <=(指定生成图片宽 - 边距),全部填充
+ - 绘制图片宽度 >(指定生成图片宽 - 边距),等比例缩放绘制图片
+
+3. 文本绘制,换行的问题
+ - 每一行允许的文本长度有限,超过时,需要自动换行处理
+
+
+#### 文本绘制
+
+考虑基本的文本绘制,流程如下
+
+- 创建`BufferImage`对象
+- 获取`Graphic2d`对象,操作绘制
+- 设置基本配置信息
+- 文本按换行进行拆分为字符串数组, 循环绘制单行内容
+ - 计算当行字符串,实际绘制的行数,然后进行拆分
+ - 依次绘制文本(需要注意y坐标的变化)
+
+
+
+下面是具体的实现
+
+```java
+public static int drawContent(Graphics2D g2d,
+ String content,
+ int y,
+ ImgCreateOptions options) {
+
+ int w = options.getImgW();
+ int leftPadding = options.getLeftPadding();
+ int linePadding = options.getLinePadding();
+ Font font = options.getFont();
+
+
+ // 一行容纳的字符个数
+ int lineNum = (int) Math.floor((w - (leftPadding << 1)) / (double) font.getSize());
+
+ // 对长串字符串进行分割成多行进行绘制
+ String[] strs = splitStr(content, lineNum);
+
+ g2d.setFont(font);
+
+ g2d.setColor(options.getFontColor());
+ int index = 0;
+ int x;
+ for (String tmp : strs) {
+ x = calOffsetX(leftPadding, w, tmp.length() * font.getSize(), options.getAlignStyle());
+ g2d.drawString(tmp, x, y + (linePadding + font.getSize()) * index);
+ index++;
+ }
+
+
+ return y + (linePadding + font.getSize()) * (index);
+}
+
+/**
+ * 计算不同对其方式时,对应的x坐标
+ *
+ * @param padding 左右边距
+ * @param width 图片总宽
+ * @param strSize 字符串总长
+ * @param style 对其方式
+ * @return 返回计算后的x坐标
+ */
+private static int calOffsetX(int padding,
+ int width,
+ int strSize,
+ ImgCreateOptions.AlignStyle style) {
+ if (style == ImgCreateOptions.AlignStyle.LEFT) {
+ return padding;
+ } else if (style == ImgCreateOptions.AlignStyle.RIGHT) {
+ return width - padding - strSize;
+ } else {
+ return (width - strSize) >> 1;
+ }
+}
+
+
+/**
+ * 按照长度对字符串进行分割
+ *
+ * fixme 包含emoj表情时,兼容一把
+ *
+ * @param str 原始字符串
+ * @param splitLen 分割的长度
+ * @return
+ */
+public static String[] splitStr(String str, int splitLen) {
+ int len = str.length();
+ int size = (int) Math.ceil(len / (float) splitLen);
+
+ String[] ans = new String[size];
+ int start = 0;
+ int end = splitLen;
+ for (int i = 0; i < size; i++) {
+ ans[i] = str.substring(start, end > len ? len : end);
+ start = end;
+ end += splitLen;
+ }
+
+ return ans;
+}
+```
+
+
+上面的实现比较清晰了,图片的绘制则更加简单
+
+#### 图片绘制
+
+只需要重新计算下待绘制图片的宽高即可,具体实现如下
+
+
+```java
+/**
+ * 在原图上绘制图片
+ *
+ * @param source 原图
+ * @param dest 待绘制图片
+ * @param y 待绘制的y坐标
+ * @param options
+ * @return 绘制图片的高度
+ */
+public static int drawImage(BufferedImage source,
+ BufferedImage dest,
+ int y,
+ ImgCreateOptions options) {
+ Graphics2D g2d = getG2d(source);
+ int w = Math.min(dest.getWidth(), options.getImgW() - (options.getLeftPadding() << 1));
+ int h = w * dest.getHeight() / dest.getWidth();
+
+ int x = calOffsetX(options.getLeftPadding(),
+ options.getImgW(), w, options.getAlignStyle());
+
+ // 绘制图片
+ g2d.drawImage(dest,
+ x,
+ y + options.getLinePadding(),
+ w,
+ h,
+ null);
+ g2d.dispose();
+
+ return h;
+}
+
+public static Graphics2D getG2d(BufferedImage bf) {
+ Graphics2D g2d = bf.createGraphics();
+
+ g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
+ g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
+ g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
+ g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+ g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+ g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
+
+ return g2d;
+}
+```
+
+
+### 4. 内容渲染
+前面只是给出了单块内容(如一段文字,一张图片)的渲染,存在一些问题
+
+- 绘制的内容超过画布的高度如何处理
+- 文本绘制要求传入的文本没有换行符,否则换行不生效
+- 交叉绘制的场景,如何重新计算y坐标
+
+---
+
+解决这些问题则是在 `ImgCreateWrapper` 的具体绘制中进行了实现,先看文本的绘制
+
+- 根据换行符对字符串进行拆分
+- 计算绘制内容最终转换为图片时,所占用的高度
+- 重新生成画布 `BufferedImage result`
+ - 如果result为空,则直接生成
+ - 如果最终生成的高度,超过已有画布的高度,则生成一个更高的画布,并将原来的内容绘制上去
+- 迭代绘制单行内容
+
+```java
+public Builder drawContent(String content) {
+ String[] strs = StringUtils.split(content, "\n");
+ if (strs.length == 0) { // empty line
+ strs = new String[1];
+ strs[0] = " ";
+ }
+
+ int fontSize = options.getFont().getSize();
+ int lineNum = calLineNum(strs, options.getImgW(), options.getLeftPadding(), fontSize);
+ // 填写内容需要占用的高度
+ int height = lineNum * (fontSize + options.getLinePadding());
+
+ if (result == null) {
+ result = GraphicUtil.createImg(options.getImgW(),
+ Math.max(height + options.getTopPadding() + options.getBottomPadding(), BASE_ADD_H),
+ null);
+ } else if (result.getHeight() < contentH + height + options.getBottomPadding()) {
+ // 超过原来图片高度的上限, 则需要扩充图片长度
+ result = GraphicUtil.createImg(options.getImgW(),
+ result.getHeight() + Math.max(height + options.getBottomPadding(), BASE_ADD_H),
+ result);
+ }
+
+
+ // 绘制文字
+ Graphics2D g2d = GraphicUtil.getG2d(result);
+ int index = 0;
+ for (String str : strs) {
+ GraphicUtil.drawContent(g2d, str,
+ contentH + (fontSize + options.getLinePadding()) * (++index)
+ , options);
+ }
+ g2d.dispose();
+
+ contentH += height;
+ return this;
+}
+
+
+/**
+ * 计算总行数
+ *
+ * @param strs 字符串列表
+ * @param w 生成图片的宽
+ * @param padding 渲染内容的左右边距
+ * @param fontSize 字体大小
+ * @return
+ */
+private int calLineNum(String[] strs, int w, int padding, int fontSize) {
+ // 每行的字符数
+ double lineFontLen = Math.floor((w - (padding << 1)) / (double) fontSize);
+
+
+ int totalLine = 0;
+ for (String str : strs) {
+ totalLine += Math.ceil(str.length() / lineFontLen);
+ }
+
+ return totalLine;
+}
+```
+
+
+上面需要注意的是画布的生成规则,特别是高度超过上限之后,重新计算图片高度时,需要额外注意新增的高度,应该为基本的增量与(绘制内容高度+下边距)的较大值
+
+```
+int realAddH = Math.max(bufferedImage.getHeight() + options.getBottomPadding() + options.getTopPadding(), BASE_ADD_H)
+```
+
+
+
+重新生成画布实现 `com.hust.hui.quickmedia.common.util.GraphicUtil#createImg`
+
+```java
+public static BufferedImage createImg(int w, int h, BufferedImage img) {
+ BufferedImage bf = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g2d = bf.createGraphics();
+
+ if (img != null) {
+ g2d.setComposite(AlphaComposite.Src);
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2d.drawImage(img, 0, 0, null);
+ }
+ g2d.dispose();
+ return bf;
+}
+```
+
+
+上面理解之后,绘制图片就比较简单了,基本上行没什么差别
+
+
+```java
+public Builder drawImage(String img) {
+ BufferedImage bfImg;
+ try {
+ bfImg = ImageUtil.getImageByPath(img);
+ } catch (IOException e) {
+ log.error("load draw img error! img: {}, e:{}", img, e);
+ throw new IllegalStateException("load draw img error! img: " + img, e);
+ }
+
+ return drawImage(bfImg);
+}
+
+
+public Builder drawImage(BufferedImage bufferedImage) {
+
+ if (result == null) {
+ result = GraphicUtil.createImg(options.getImgW(),
+ Math.max(bufferedImage.getHeight() + options.getBottomPadding() + options.getTopPadding(), BASE_ADD_H),
+ null);
+ } else if (result.getHeight() < contentH + bufferedImage.getHeight() + options.getBottomPadding()) {
+ // 超过阀值
+ result = GraphicUtil.createImg(options.getImgW(),
+ result.getHeight() + Math.max(bufferedImage.getHeight() + options.getBottomPadding() + options.getTopPadding(), BASE_ADD_H),
+ result);
+ }
+
+ // 更新实际高度
+ int h = GraphicUtil.drawImage(result,
+ bufferedImage,
+ contentH,
+ options);
+ contentH += h + options.getLinePadding();
+ return this;
+}
+```
+
+### 5. http接口
+
+上面实现的生成图片的公共方法,在 `quick-media` 工程中,利用spring-boot搭建了一个web服务,提供了一个http接口,用于生成长图文,最终的成果就是我们开头的那个gif图的效果,相关代码就没啥好说的,有兴趣的可以直接查看工程源码,链接看最后
+
+## 测试验证
+
+上面基本上完成了我们预期的目标,接下来则是进行验证,测试代码比较简单,先准备一段文本,这里拉了一首诗
+
+```text
+招魂酹翁宾旸
+郑起
+
+君之在世帝敕下,君之谢世帝敕回。
+魂之为变性原返,气之为物情本开。
+於戏龙兮凤兮神气盛,噫嘻鬼兮归兮大块埃。
+身可朽名不可朽,骨可灰神不可灰。
+采石捉月李白非醉,耒阳避水子美非灾。
+长孙王吉命不夭,玉川老子诗不徘。
+新城罗隐在奇特,钱塘潘阆终崔嵬。
+阴兮魄兮曷往,阳兮魄兮曷来。
+君其归来,故交寥落更散漫。
+君来归来,帝城绚烂可徘徊。
+君其归来,东西南北不可去。
+君其归来。
+春秋霜露令人哀。
+花之明吾无与笑,叶之陨吾实若摧。
+晓猿啸吾闻泪堕,宵鹤立吾见心猜。
+玉泉其清可鉴,西湖其甘可杯。
+孤山暖梅香可嗅,花翁葬荐菊之隈。
+君其归来,可伴逋仙之梅,去此又奚之哉。
+```
+
+测试代码
+
+```java
+@Test
+public void testGenImg() throws IOException {
+ int w = 400;
+ int leftPadding = 10;
+ int topPadding = 40;
+ int bottomPadding = 40;
+ int linePadding = 10;
+ Font font = new Font("宋体", Font.PLAIN, 18);
+
+ ImgCreateWrapper.Builder build = ImgCreateWrapper.build()
+ .setImgW(w)
+ .setLeftPadding(leftPadding)
+ .setTopPadding(topPadding)
+ .setBottomPadding(bottomPadding)
+ .setLinePadding(linePadding)
+ .setFont(font)
+ .setAlignStyle(ImgCreateOptions.AlignStyle.CENTER)
+// .setBgImg(ImageUtil.getImageByPath("qrbg.jpg"))
+ .setBgColor(0xFFF7EED6)
+ ;
+
+
+ BufferedReader reader = FileReadUtil.createLineRead("text/poem.txt");
+ String line;
+ int index = 0;
+ while ((line = reader.readLine()) != null) {
+ build.drawContent(line);
+
+ if (++index == 5) {
+ build.drawImage(ImageUtil.getImageByPath("https://static.oschina.net/uploads/img/201708/12175633_sOfz.png"));
+ }
+
+ if (index == 7) {
+ build.setFontSize(25);
+ }
+
+ if (index == 10) {
+ build.setFontSize(20);
+ build.setFontColor(Color.RED);
+ }
+ }
+
+ BufferedImage img = build.asImage();
+ String out = Base64Util.encode(img, "png");
+ System.out.println("");
+}
+```
+
+
+输出图片
+
+
+![测试结果图](https://static.oschina.net/uploads/img/201708/18180717_MrRM.png "测试结果图")
+
+## 其他
+
+
+项目地址: [https://github.com/liuyueyi/quick-media](https://github.com/liuyueyi/quick-media)
+
+
+个人博客:[一灰的个人博客](http://blog.zbang.online:8080)
+
+公众号获取更多:
+
+![个人信息](https://static.oschina.net/uploads/img/201708/12175649_wn2r.png "个人信息")
+
+
+
diff --git "a/docs/\346\217\222\344\273\266/image/\345\233\276\347\211\207\345\220\210\346\210\220.md" "b/docs/\346\217\222\344\273\266/image/\345\233\276\347\211\207\345\220\210\346\210\220.md"
new file mode 100644
index 00000000..8e4947ac
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/image/\345\233\276\347\211\207\345\220\210\346\210\220.md"
@@ -0,0 +1,424 @@
+# 图片合成
+
+> 利用Java的绘图方法,实现图片合成
+
+在开始之前,先定一个小目标,我们希望通过图片合成的方式,创建一个类似下面样式的图片
+
+![](https://static.oschina.net/uploads/img/201710/13203703_6IVg.jpg)
+
+### I. 设计思路
+> 首先解析一下我们的目标实现图片合成,那么这些合成的基本组成单元有些什么?
+
+**组成基本单元**
+
+- 图片
+- 文字
+- 几何图形
+
+也就是说,我们可以将任意个图片,文字,几何图形,按照自己的意愿进行拼接,那么问题就转变成两个
+
+- 基本单元如何在画布上渲染
+- 基本单元之间如何配合使用
+
+### II. 基本单元绘制
+
+首先定义一个基本单元的接口,之后所有组合的元素都继承自这个接口
+
+接口`IMergeCell`只定义一个绘制的方法,用于实现该基本单元的绘制方式
+
+```java
+public interface IMergeCell {
+ void draw(Graphics2D g2d);
+}
+```
+
+#### 1. 图片绘制
+
+绘制图片,一般来讲需要知道:
+
+- 绘制的坐标(x,y)
+- 绘制图片的宽高(w,h),当目标是绘制原图时,宽高一般为图片本身的宽高
+
+结合上面两点,图片组成单元的定义如下: `ImgCell`
+
+```java
+@Data
+@Builder
+public class ImgCell implements IMergeCell {
+
+ private BufferedImage img;
+
+ private Integer x, y, w, h;
+
+ @Override
+ public void draw(Graphics2D g2d) {
+ if (w == null) {
+ w = img.getWidth();
+ }
+
+ if (h == null) {
+ h = img.getHeight();
+ }
+
+ g2d.drawImage(img, x, y, w, h, null);
+ }
+}
+```
+
+#### 2. 文本绘制
+> 图片绘制比较简单,相比而言,文字绘制就麻烦一点,主要是文本绘制的对齐方式,竖排还是横排布局
+
+**首先分析我们需要的基本信息**
+
+- 考虑对齐方式(居中对齐,靠左,靠上,靠右,靠下)
+ - 因此需要确定文本绘制的区域,所以需要两个坐标 (startX, startY), (endX, endY)
+
+- 文本绘制参数
+ - 可以指定字体`Font`,文本颜色 `Color`,行间距 `lineSpace`
+
+- 绘制的文本信息
+ - 文本内容 `List`
+
+**绘制实现**
+
+- 若单行的文本超过长度上限,则需要自动换行,所以有 `batchSplitText` 方法,对原文本内容进行分割,确保不会超过边界
+
+- 不同的对齐方式,绘制的起始坐标需要计算, 所以在水平布局文字时,需要通过 `calculateX`方法获取新的x坐标;竖直布局文字时,需要通过 `calculateY`获取新的y坐标
+
+
+实际代码如下
+
+```java
+@Data
+public class TextCell implements IMergeCell {
+
+ private List texts;
+
+ private Color color = Color.black;
+
+ private Font font = FontUtil.DEFAULT_FONT;
+
+
+ private int lineSpace;
+
+ private int startX, startY;
+ private int endX, endY;
+
+
+ /**
+ * 绘制样式
+ */
+ private ImgCreateOptions.DrawStyle drawStyle = ImgCreateOptions.DrawStyle.HORIZONTAL;
+
+
+ private ImgCreateOptions.AlignStyle alignStyle = ImgCreateOptions.AlignStyle.LEFT;
+
+
+ public void addText(String text) {
+ if (texts == null) {
+ texts = new ArrayList<>();
+ }
+
+ texts.add(text);
+ }
+
+
+ @Override
+ public void draw(Graphics2D g2d) {
+ g2d.setColor(color);
+ g2d.setFont(font);
+
+ FontMetrics fontMetrics = FontUtil.getFontMetric(font);
+ int tmpHeight = fontMetrics.getHeight(), tmpW = font.getSize() >>> 1;
+ int tmpY = startY, tmpX = startX;
+ int offsetX = drawStyle == ImgCreateOptions.DrawStyle.VERTICAL_LEFT
+ ? (font.getSize() + fontMetrics.getDescent() + lineSpace)
+ : -(font.getSize() + fontMetrics.getDescent() + lineSpace);
+ // 单行文本自动换行分割
+ List splitText = batchSplitText(texts, fontMetrics);
+ for (String info : splitText) {
+ if (drawStyle == ImgCreateOptions.DrawStyle.HORIZONTAL) {
+ g2d.drawString(info, calculateX(info, fontMetrics), tmpY);
+
+ // 换行,y坐标递增一位
+ tmpY += fontMetrics.getHeight() + lineSpace;
+ } else { // 垂直绘制文本
+ char[] chars = info.toCharArray();
+
+ tmpY = calculateY(info, fontMetrics);
+ for (int i = 0; i < chars.length; i++) {
+ tmpX = PunctuationUtil.isPunctuation(chars[i]) ? tmpW : 0;
+ g2d.drawString(chars[i] + "",
+ tmpX + (PunctuationUtil.isPunctuation(chars[i]) ? tmpW : 0),
+ tmpY);
+ tmpY += tmpHeight;
+ }
+
+ // 换一列
+ tmpX += offsetX;
+ }
+ }
+ }
+
+
+ // 若单行文本超过长度限制,则自动进行换行
+ private List batchSplitText(List texts, FontMetrics fontMetrics) {
+ List ans = new ArrayList<>();
+ if (drawStyle == ImgCreateOptions.DrawStyle.HORIZONTAL) {
+ int lineLen = Math.abs(endX - startX);
+ for(String t: texts) {
+ ans.addAll(Arrays.asList(GraphicUtil.splitStr(t, lineLen, fontMetrics)));
+ }
+ } else {
+ int lineLen = Math.abs(endY - startY);
+ for(String t: texts) {
+ ans.addAll(Arrays.asList(GraphicUtil.splitVerticalStr(t, lineLen, fontMetrics)));
+ }
+ }
+ return ans;
+ }
+
+
+ private int calculateX(String text, FontMetrics fontMetrics) {
+ if (alignStyle == ImgCreateOptions.AlignStyle.LEFT) {
+ return startX;
+ } else if (alignStyle == ImgCreateOptions.AlignStyle.RIGHT) {
+ return endX - fontMetrics.stringWidth(text);
+ } else {
+ return startX + ((endX - startX - fontMetrics.stringWidth(text)) >>> 1);
+ }
+
+ }
+
+
+ private int calculateY(String text, FontMetrics fontMetrics) {
+ if (alignStyle == ImgCreateOptions.AlignStyle.TOP) {
+ return startY;
+ } else if (alignStyle == ImgCreateOptions.AlignStyle.BOTTOM) {
+ int size = fontMetrics.stringWidth(text) + fontMetrics.getDescent() * (text.length() - 1);
+ return endY - size;
+ } else {
+ int size = fontMetrics.stringWidth(text) + fontMetrics.getDescent() * (text.length() - 1);
+ return startY + ((endY - endX - size) >>> 1);
+ }
+ }
+}
+```
+
+_说明:_
+
+- 单行文本的分割,使用了博文系列中的工具方法 `GraphicUtil.splitStr`,有兴趣的关注源码进行查看
+- 水平布局时,期望 `startX < endX`, 从习惯来讲,基本上我们都是从左到右进行阅读
+- 水平or垂直布局,都希望是 `startY < endY`
+- 垂直布局时,以字符为单位进行绘制;标点符号的绘制时,x坐标有一个偏移量
+
+
+#### 3. Line直线绘制
+
+几何图形之直线绘制,给出起点和结束点坐标,绘制一条直线,比较简单;这里给出了虚线的支持
+
+
+```java
+@Data
+@Builder
+public class LineCell implements IMergeCell {
+
+ /**
+ * 起点坐标
+ */
+ private int x1, y1;
+
+ /**
+ * 终点坐标
+ */
+ private int x2, y2;
+
+ /**
+ * 颜色
+ */
+ private Color color;
+
+
+ /**
+ * 是否是虚线
+ */
+ private boolean dashed;
+
+ /**
+ * 虚线样式
+ */
+ private Stroke stroke = CellConstants.LINE_DEFAULT_STROKE;
+
+
+ @Override
+ public void draw(Graphics2D g2d) {
+ g2d.setColor(color);
+ if (!dashed) {
+ g2d.drawLine(x1, y1, x2, y2);
+ } else { // 绘制虚线时,需要保存下原有的画笔用于恢复
+ Stroke origin = g2d.getStroke();
+ g2d.setStroke(stroke);
+ g2d.drawLine(x1, y1, x2, y2);
+ g2d.setStroke(origin);
+ }
+ }
+}
+```
+
+#### 4. 矩形框绘制
+
+矩形框绘制,同直线绘制,支持圆角矩形,支持虚线框
+
+```java
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class RectCell implements IMergeCell {
+
+ /**
+ * 起始坐标
+ */
+ private int x, y;
+
+ /**
+ * 矩形宽高
+ */
+ private int w, h;
+
+
+ /**
+ * 颜色
+ */
+ private Color color;
+
+
+ /**
+ * 是否为虚线
+ */
+ private boolean dashed;
+
+
+ /**
+ * 虚线样式
+ */
+ private Stroke stroke;
+
+
+ /**
+ * 圆角弧度
+ */
+ private int radius;
+
+
+ @Override
+ public void draw(Graphics2D g2d) {
+ g2d.setColor(color);
+ if (!dashed) {
+ g2d.drawRoundRect(x, y, w, h, radius, radius);
+ } else {
+ Stroke stroke = g2d.getStroke();
+ g2d.setStroke(stroke);
+ g2d.drawRoundRect(x, y, w, h, radius, radius);
+ g2d.setStroke(stroke);
+ }
+ }
+}
+```
+
+#### 5. 矩形区域填充
+
+```java
+@Data
+@Builder
+public class RectFillCell implements IMergeCell {
+
+ private Font font;
+
+ private Color color;
+
+
+ private int x,y,w,h;
+
+ @Override
+ public void draw(Graphics2D g2d) {
+ g2d.setFont(font);
+ g2d.setColor(color);;
+ g2d.fillRect(x, y, w, h);
+ }
+}
+```
+
+### III. 封装
+
+上面实现了几个常见的基本单元绘制,接下来则是封装绘制, 这块的逻辑就比较简单了如下
+
+```java
+public class ImgMergeWrapper {
+ public static BufferedImage merge(List list, int w, int h) {
+ BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g2d = GraphicUtil.getG2d(img);
+ list.forEach(cell -> cell.draw(g2d));
+ return img;
+ }
+}
+```
+
+
+### IV. 测试
+
+写了一个模板`QrCodeCardTemplateBuilder`,用于拼装上图的样式,代码较长,不贴了,有兴趣的查看原图
+
+测试代码如下
+
+```java
+@Test
+public void testTemplate() throws IOException {
+ BufferedImage logo = ImageUtil.getImageByPath("logo.jpg");
+ BufferedImage qrCode = ImageUtil.getImageByPath("/Users/yihui/Desktop/12.jpg");
+ String name = "小灰灰blog";
+ List desc = Arrays.asList("我是一灰灰,一匹不吃羊的狼 专注码农技术分享");
+
+
+ int w = QrCodeCardTemplate.w, h = QrCodeCardTemplate.h;
+ List list = QrCodeCardTemplateBuilder.build(logo, name, desc, qrCode, "微 信 公 众 号");
+
+ BufferedImage bg = ImgMergeWrapper.merge(list, w, h);
+
+ try {
+ ImageIO.write(bg, "jpg", new File("/Users/yihui/Desktop/merge.jpg"));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+}
+```
+
+演示图如下:
+
+
+![](https://static.oschina.net/uploads/img/201710/13203703_6IVg.jpg)
+
+
+### V. 其他
+
+**项目地址:**
+
+- [https://github.com/liuyueyi/quick-media](https://github.com/liuyueyi/quick-media)
+- `QuickMedia` 目标是创建一个专注图文,音视频,二维码处理的开源项目
+
+
+**系列博文**
+
+- [spring-boot & ffmpeg 搭建一个音频转码服务](https://my.oschina.net/u/566591/blog/1359432)
+- [spring-boot & zxingy 搭建二维码服务](https://my.oschina.net/u/566591/blog/1457164)
+- [二维码服务拓展(支持logo,圆角logo,背景图,颜色配置)](https://my.oschina.net/u/566591/blog/1491697)
+- [zxing二维码生成服务之深度定制](https://my.oschina.net/u/566591/blog/1507162)
+- [Java实现长图文生成](https://my.oschina.net/u/566591/blog/1514644)
+- [Java竖排长图文生成](https://my.oschina.net/u/566591/blog/1529564)
+- [Java实现markdown 转 html](https://my.oschina.net/u/566591/blog/1535380)
+- [Java实现html 转 image](https://my.oschina.net/u/566591/blog/1536078)
+
+
+**扫描关注,java分享**
+
+![](https://static.oschina.net/uploads/img/201710/13203703_6IVg.jpg)
diff --git "a/docs/\346\217\222\344\273\266/image/\346\246\202\350\247\210.md" "b/docs/\346\217\222\344\273\266/image/\346\246\202\350\247\210.md"
new file mode 100644
index 00000000..3bf12917
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/image/\346\246\202\350\247\210.md"
@@ -0,0 +1 @@
+# 概览
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/magic/Java\345\200\237\345\212\251ImageMagic\345\256\236\347\216\260\345\233\276\347\211\207\347\274\226\350\276\221\346\234\215\345\212\241.md" "b/docs/\346\217\222\344\273\266/magic/Java\345\200\237\345\212\251ImageMagic\345\256\236\347\216\260\345\233\276\347\211\207\347\274\226\350\276\221\346\234\215\345\212\241.md"
new file mode 100644
index 00000000..ad5103ec
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/magic/Java\345\200\237\345\212\251ImageMagic\345\256\236\347\216\260\345\233\276\347\211\207\347\274\226\350\276\221\346\234\215\345\212\241.md"
@@ -0,0 +1,971 @@
+# Java 借助ImageMagic实现图片编辑服务
+
+java原生对于图片的编辑处理并没有特别友好,而且问题也有不少,那么作为一个java后端,如果要提供图片的编辑服务可以怎么办?也得想办法去支持业务需求,本片博文基于此进行展开
+
+
+
+## I. 调研
+
+首先最容易想到的就是目前是不是已经有了相关的开源库,直接用不就很high了嘛,git上搜一下
+
+### 1. thumbnailator
+
+差不多四年都没有更新了,基于awt进行图片的编辑处理,目前提供了基本的图片编辑接口,开始用了一段时间,有几个绕不够去的坑,所以最后放弃了
+
+使用姿势:
+
+```xml
+
+ net.coobird
+ thumbnailator
+ 0.4.8
+
+```
+
+一个使用case:
+
+```java
+BufferedImage originalImage = ImageIO.read(new File("original.jpg"));
+
+BufferedImage thumbnail = Thumbnails.of(originalImage)
+ .size(200, 200)
+ .rotate(90)
+ .asBufferedImage();
+```
+
+问题说明:
+
+- jpg图片编辑后,输出图片变红的问题(详情参考:[兼容ImageIO读取jpeg图片变红](https://liuyueyi.github.io/hexblog/2018/01/22/%E5%85%BC%E5%AE%B9ImageIO%E8%AF%BB%E5%8F%96jpeg%E5%9B%BE%E7%89%87%E5%8F%98%E7%BA%A2/))
+- 图片精度丢失(对于精度要求较高的场景下,直接使用Jdk的BufferedImage会丢失精度)
+
+
+上面两个问题中,第二个精度丢失在某些对图片质量有要求的场景下比较严重,如果业务场景没那么将就的话,用这个库还是可以减少很多事情的,下面基于ImageMagic的接口设计,很大程度上参考了该工程的使用规范,因为使用起来(+阅读)确实特别顺畅
+
+
+### 2. simpleimage
+
+阿里的开源库,文档极其欠缺,而且良久没有人维护,没有实际使用过,感觉属于玩票的性质(个人猜测是KPI为导向下的产物)
+
+
+如果想造轮子的话,参考它的源码,某些图片的处理方案还是不错的
+
+### 3. imagemagic + im4java
+
+ImageMagic/GraphicMagic 是c++的图象处理软件,很多服务基于此来搭建图片处理服务的
+
+- 优点:稳定、性能高、支持接口多、开箱即用、靠谱
+- 缺点:得提前配置环境,基本上改造不动,内部有问题也没辙
+
+这个方法也是下面的主要讲述重点,放弃Thumbnailator选择imagemagic的原因如下:
+
+- 支持更多的服务功能(比Thumbnailator多很多的接口)
+- 没有精度丢失问题
+- 没有图片失真问题(颜色变化,alpha值变化问题)
+
+## II. 环境准备
+
+首先得安装ImageMagic环境,有不少的第三方依赖,下面提供linux和mac的安装过程
+
+### 1. linux安装过程
+
+
+```sh
+# 依赖安装
+yum install libjpeg-devel
+yum install libpng-devel
+yum install libwebp-devel
+
+
+## 也可以使用源码方式安装
+安装jpeg 包 `wget ftp://223.202.54.10/pub/web/php/libjpeg-6b.tar.gz`
+安装webp 包 `wget http://www.imagemagick.org/download/delegates/libwebp-0.5.1.tar.gz`
+安装png 包 `wget http://www.imagemagick.org/download/delegates/libpng-1.6.24.tar.gz`
+
+
+## 下载并安装ImageMagic
+wget http://www.imagemagick.org/download/ImageMagick.tar.gz
+
+tar -zxvf ImageMagick.tar.gz
+cd ImageMagick-7.0.7-28
+./configure; sudo make; sudo make install
+```
+
+安装完毕之后,进行测试
+
+```sh
+$ convert --version
+
+Version: ImageMagick 7.0.7-28 Q16 x86_64 2018-04-17 http://www.imagemagick.org
+Copyright: © 1999-2018 ImageMagick Studio LLC
+License: http://www.imagemagick.org/script/license.php
+Features: Cipher DPC HDRI OpenMP
+Delegates (built-in): fontconfig freetype jng jpeg lzma png webp x xml zlib
+```
+
+
+### 2. mac安装过程
+
+依赖安装
+
+```sh
+sudo brew install jpeg
+sudo brew install libpng
+sudo brew install libwebp
+sudo brew install GraphicsMagick
+sudo brew install ImageMagick
+```
+
+源码安装方式与上面一致
+
+
+### 3. 问题及修复
+
+如果安装完毕之后,可能会出现下面的问题
+
+提示找不到png依赖:
+
+- 安装:一直找不到 png的依赖,查阅需要安装 http://pkgconfig.freedesktop.org/releases/pkg-config-0.28.tar.gz
+
+执行 convert 提示linux shared libraries 不包含某个库
+
+- 临时方案:`export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH`
+- 永久方案:
+
+ ```sh
+ vi /etc/ld.so.conf
+ 在这个文件里加入:/usr/local/lib 来指明共享库的搜索位置
+ 然后再执行/sbin/ldconf
+ ```
+
+### 4. 常见Convert命令
+
+imagemagic的场景使用命令如下
+
+裁图
+
+- convert test.jpg -crop 640x960+0+0 output.jpg
+
+旋转
+
+- convert test.jpg -rotate 90 output.jpg
+
+缩放
+
+- convert test.jpg -resize 200x200 output.jpg
+
+强制宽高缩放
+
+- convert test.jpg -resize 200x200! output.jpg
+
+缩略图
+
+- convert -thumbnail 200x300 test.jpg thumb.jpg
+
+上下翻转:
+
+- convert -flip foo.png bar.png
+
+左右翻转:
+
+- convert -flop foo.png bar.png
+
+水印:
+
+- composite -gravity northwest -dissolve 100 -geometry +0+0 water.png temp.jpg out.jpg
+
+添加边框 :
+
+- convert -border 6x6 -bordercolor "#ffffff" test.jpg bord.jpg
+
+去除边框 :
+
+- convert -thumbnail 200x300 test.jpg thumb.jpg
+
+## III. 接口设计与实现
+
+java调用ImageMagic的方式有两种,一个是基于命令行的,一种是基于JNI的,我们选则im4java来操作imagemagic的接口(基于命令行的操作)
+
+**目标:**
+
+对外的使用姿势尽可能如 `Thumbnailtor`,采用builder模式来设置参数,支持多种输入输出
+
+
+### 1. im4java使用姿势
+
+几个简单的case,演示下如何使用im4java实现图片的操作
+
+```java
+IMOperation op = new IMOperation();
+
+// 裁剪
+op.crop(operate.getWidth(), operate.getHeight(), operate.getX(), operate.getY());
+
+
+// 旋转
+op.rotate(rotate);
+
+
+// 压缩
+op.resize(operate.getWidth(), operate.getHeight());
+op.quality(operate.getQuality().doubleValue()); // 精度
+
+
+// 翻转
+op.flip();
+
+// 镜像
+op.flop();
+
+// 水印
+op.geometry(operate.getWidth(), operate.getHeight(), operate.getX(), operate.getY()).composite();
+
+// 边框
+op.border(operate.getWidth(), operate.getHeight()).bordercolor(operate.getColor());
+
+
+// 原始命令方式添加
+op.addRawArgs("-resize", "!100x200");
+
+
+// 添加原始图片地址
+op.addImage(sourceFilename);
+// 目标图片地址
+op.addImage(outputFilename);
+
+
+/** 传true到构造函数中,则表示使用GraphicMagic, 裁图时,图片大小会变 */
+ConvertCmd convert = new ConvertCmd();
+convert.run(op);
+```
+
+### 2. 使用姿势
+
+在具体的设计接口之前,不妨先看一下最终的使用姿势,然后逆向的再看是如何设计的
+
+```java
+private static final String localFile = "blogInfoV2.png";
+
+
+/**
+ * 复合操作
+ */
+@Test
+public void testOperate() {
+ BufferedImage img;
+ try {
+ img = ImgWrapper.of(localFile)
+ .board(10, 10, "red")
+ .flip()
+ .rotate(180)
+ .crop(0, 0, 1200, 500)
+ .asImg();
+ System.out.println("--- " + img);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+}
+```
+
+上面这个方法,演示了图片的多个操作,首先是加个红色边框,然后翻转,然后旋转180°,再裁剪输出图片
+
+
+所以这个封装,肯定是使用了Builder模式了,接下来看下配置参数
+
+
+### 3. 接口设计
+
+首先确定目前支持的几个方法:`OperateType`
+
+其次就是相关的配置参数: `Operate`
+
+```java
+@Data
+public static class Operate {
+ /**
+ * 操作类型
+ */
+ private OperateType operateType;
+
+ /**
+ * 裁剪宽; 缩放宽
+ */
+ private Integer width;
+ /**
+ * 高
+ */
+ private Integer height;
+ /**
+ * 裁剪时,起始 x
+ */
+ private Integer x;
+ /**
+ * 裁剪时,起始y
+ */
+ private Integer y;
+ /**
+ * 旋转角度
+ */
+ private Double rotate;
+
+ /**
+ * 按照整体的缩放参数, 1 表示不变, 和裁剪一起使用
+ */
+ private Double radio;
+
+ /**
+ * 图片精度, 1 - 100
+ */
+ private Integer quality;
+
+ /**
+ * 颜色 (添加边框中的颜色; 去除图片中某颜色)
+ */
+ private String color;
+
+ /**
+ * 水印图片, 可以为图片名, uri, 或者inputstream
+ */
+ private T water;
+
+ /**
+ * 水印图片的类型
+ */
+ private String waterImgType;
+
+ /**
+ * 强制按照给定的参数进行压缩
+ */
+ private boolean forceScale;
+
+
+ public boolean valid() {
+ switch (operateType) {
+ case CROP:
+ return width != null && height != null && x != null && y != null;
+ case SCALE:
+ return width != null || height != null || radio != null;
+ case ROTATE:
+ return rotate != null;
+ case WATER:
+ // 暂时不支持水印操作
+ return water != null;
+ case BOARD:
+ if (width == null) {
+ width = 3;
+ }
+ if (height == null) {
+ height = 3;
+ }
+ if (color == null) {
+ color = "#ffffff";
+ }
+ case FLIP:
+ case FLOP:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * 获取水印图片的路径
+ *
+ * @return
+ */
+ public String getWaterFilename() throws ImgOperateException {
+ try {
+ return FileWriteUtil.saveFile(water, waterImgType).getAbsFile();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+}
+
+
+public enum OperateType {
+ /**
+ * 裁剪
+ */
+ CROP,
+ /**
+ * 缩放
+ */
+ SCALE,
+ /**
+ * 旋转
+ */
+ ROTATE,
+ /**
+ * 水印
+ */
+ WATER,
+
+ /**
+ * 上下翻转
+ */
+ FLIP,
+
+ /**
+ * 水平翻转
+ */
+ FLOP,
+ /**
+ * 添加边框
+ */
+ BOARD;
+}
+```
+
+### 4. Builder实现
+
+简化使用成本,因此针对图片裁剪、旋转等接口,封装了更友好的接口方式
+
+```java
+public static class Builder {
+ private T sourceFile;
+
+ /**
+ * 图片类型 JPEG, PNG, GIF ...
+ *
+ * 默认为jpg图片
+ */
+ private String outputFormat = "jpg";
+
+ private List operates = new ArrayList<>();
+
+ public Builder(T sourceFile) {
+ this.sourceFile = sourceFile;
+ }
+
+
+ private static Builder ofString(String str) {
+ return new Builder(ImgWrapper.class.getClassLoader().getResource(str).getFile());
+ }
+
+
+ private static Builder ofUrl(URI url) {
+ return new Builder(url);
+ }
+
+ private static Builder ofStream(InputStream stream) {
+ return new Builder(stream);
+ }
+
+
+ /**
+ * 设置输出的文件格式
+ *
+ * @param format
+ * @return
+ */
+ public Builder setOutputFormat(String format) {
+ this.outputFormat = format;
+ return this;
+ }
+
+
+ private void updateOutputFormat(String originType) {
+ if (this.outputFormat != null || originType == null) {
+ return;
+ }
+
+ int index = originType.lastIndexOf(".");
+ if (index <= 0) {
+ return;
+ }
+ this.outputFormat = originType.substring(index + 1);
+ }
+
+ /**
+ * 缩放
+ *
+ * @param width
+ * @param height
+ * @return
+ */
+ public Builder scale(Integer width, Integer height, Integer quality) {
+ return scale(width, height, quality, false);
+ }
+
+
+ public Builder scale(Integer width, Integer height, Integer quality, boolean forceScale) {
+ Operate operate = new Operate();
+ operate.setOperateType(OperateType.SCALE);
+ operate.setWidth(width);
+ operate.setHeight(height);
+ operate.setQuality(quality);
+ operate.setForceScale(forceScale);
+ operates.add(operate);
+ return this;
+ }
+
+ /**
+ * 按照比例进行缩放
+ *
+ * @param radio 1.0 表示不缩放, 0.5 缩放为一半
+ * @return
+ */
+ public Builder scale(Double radio, Integer quality) {
+ Operate operate = new Operate();
+ operate.setOperateType(OperateType.SCALE);
+ operate.setRadio(radio);
+ operate.setQuality(quality);
+ operates.add(operate);
+ return this;
+ }
+
+
+ /**
+ * 裁剪
+ *
+ * @param x
+ * @param y
+ * @param width
+ * @param height
+ * @return
+ */
+ public Builder crop(int x, int y, int width, int height) {
+ Operate operate = new Operate();
+ operate.setOperateType(OperateType.CROP);
+ operate.setWidth(width);
+ operate.setHeight(height);
+ operate.setX(x);
+ operate.setY(y);
+ operates.add(operate);
+ return this;
+ }
+
+
+ /**
+ * 旋转
+ *
+ * @param rotate
+ * @return
+ */
+ public Builder rotate(double rotate) {
+ Operate operate = new Operate();
+ operate.setOperateType(OperateType.ROTATE);
+ operate.setRotate(rotate);
+ operates.add(operate);
+ return this;
+ }
+
+ /**
+ * 上下翻转
+ *
+ * @return
+ */
+ public Builder flip() {
+ Operate operate = new Operate();
+ operate.setOperateType(OperateType.FLIP);
+ operates.add(operate);
+ return this;
+ }
+
+ /**
+ * 左右翻转,即镜像
+ *
+ * @return
+ */
+ public Builder flop() {
+ Operate operate = new Operate();
+ operate.setOperateType(OperateType.FLOP);
+ operates.add(operate);
+ return this;
+ }
+
+ /**
+ * 添加边框
+ *
+ * @param width 边框的宽
+ * @param height 边框的高
+ * @param color 边框的填充色
+ * @return
+ */
+ public Builder board(Integer width, Integer height, String color) {
+ Operate args = new Operate();
+ args.setOperateType(OperateType.BOARD);
+ args.setWidth(width);
+ args.setHeight(height);
+ args.setColor(color);
+ operates.add(args);
+ return this;
+ }
+
+ /**
+ * 添加水印
+ *
+ * @param water 水印的源图片 (默认为png格式)
+ * @param x 添加到目标图片的x坐标
+ * @param y 添加到目标图片的y坐标
+ * @param
+ * @return
+ */
+ public Builder water(U water, int x, int y) {
+ return water(water, "png", x, y);
+ }
+
+ /**
+ * 添加水印
+ *
+ * @param water
+ * @param imgType 水印图片的类型; 当传入的为inputStream时, 此参数才有意义
+ * @param x
+ * @param y
+ * @param
+ * @return
+ */
+ public Builder water(U water, String imgType, int x, int y) {
+ Operate operate = new Operate<>();
+ operate.setOperateType(OperateType.WATER);
+ operate.setX(x);
+ operate.setY(y);
+ operate.setWater(water);
+ operate.setWaterImgType(imgType);
+ operates.add(operate);
+ return this;
+ }
+
+
+ /**
+ * 执行图片处理, 并保存文件为: 源文件_out.jpg (类型由输出的图片类型决定)
+ *
+ * @return 保存的文件名
+ * @throws Exception
+ */
+ public String toFile() throws Exception {
+ return toFile(null);
+ }
+
+
+ /**
+ * 执行图片处理,并将结果保存为指定文件名的file
+ *
+ * @param outputFilename 若为null, 则输出文件为 源文件_out.jpg 这种格式
+ * @return
+ * @throws Exception
+ */
+ public String toFile(String outputFilename) throws Exception {
+ if (CollectionUtils.isEmpty(operates)) {
+ throw new ImgOperateException("operates null!");
+ }
+
+ /**
+ * 获取原始的图片信息, 并构建输出文件名
+ * 1. 远程图片,则保存到临时目录下
+ * 2. stream, 保存到临时目录下
+ * 3. 本地文件
+ *
+ * 输出文件都放在临时文件夹内,和原文件同名,加一个_out进行区分
+ **/
+ FileWriteUtil.FileInfo sourceFile = createFile();
+ if (outputFilename == null) {
+ outputFilename = FileWriteUtil.getTmpPath() + "/"
+ + sourceFile.getFilename() + "_"
+ + System.currentTimeMillis() + "_out." + outputFormat;
+ }
+
+ /** 执行图片的操作 */
+ if (ImgBaseOperate.operate(operates, sourceFile.getAbsFile(), outputFilename)) {
+ return outputFilename;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * 执行图片操作,并输出字节流
+ *
+ * @return
+ * @throws Exception
+ */
+ public InputStream asStream() throws Exception {
+ if (CollectionUtils.isEmpty(operates)) {
+ throw new ImgOperateException("operate null!");
+ }
+
+ String outputFilename = this.toFile();
+ if (StringUtils.isBlank(outputFilename)) {
+ return null;
+ }
+
+ return new FileInputStream(new File(outputFilename));
+ }
+
+
+ public byte[] asBytes() throws Exception {
+ if (CollectionUtils.isEmpty(operates)) {
+ throw new ImgOperateException("operate null!");
+ }
+
+ String outputFilename = this.toFile();
+ if (StringUtils.isBlank(outputFilename)) {
+ return null;
+ }
+
+
+ return BytesTool.file2bytes(outputFilename);
+ }
+
+
+ public BufferedImage asImg() throws Exception {
+ if (CollectionUtils.isEmpty(operates)) {
+ throw new ImgOperateException("operate null!");
+ }
+
+ String outputFilename = this.toFile();
+ if (StringUtils.isBlank(outputFilename)) {
+ return null;
+ }
+
+ return ImageIO.read(new File(outputFilename));
+ }
+
+
+ private FileWriteUtil.FileInfo createFile() throws Exception {
+ if (this.sourceFile instanceof String) {
+ /** 生成的文件在源文件目录下 */
+ updateOutputFormat((String) this.sourceFile);
+ } else if (this.sourceFile instanceof URI) {
+ /** 源文件和生成的文件都保存在临时目录下 */
+ String urlPath = ((URI) this.sourceFile).getPath();
+ updateOutputFormat(urlPath);
+ }
+
+ return FileWriteUtil.saveFile(this.sourceFile, outputFormat);
+ }
+}
+```
+
+参数的设置相关的比较清晰,唯一需要注意的是输出`asFile()`,这个里面实现了一些有意思的东西
+
+- 保存原图片(将网络/二进制的原图,保存到本地)
+- 生成临时输出文件
+- 命令执行
+
+上面前两个,主要是借助辅助工具 FileWriteUtil实现,与主题的关联不大,但是内部东西还是很有意思的,推荐查看:
+
+- [https://github.com/liuyueyi/quick-media/blob/master/plugins/base-plugin/src/main/java/com/github/hui/quick/plugin/base/FileWriteUtil.java](https://github.com/liuyueyi/quick-media/blob/master/plugins/base-plugin/src/main/java/com/github/hui/quick/plugin/base/FileWriteUtil.java)
+
+
+命令执行的封装如下(就是解析Operate参数,翻译成对应的IMOperation)
+
+```java
+/**
+ * 执行图片的复合操作
+ *
+ * @param operates
+ * @param sourceFilename 原始图片名
+ * @param outputFilename 生成图片名
+ * @return
+ * @throws ImgOperateException
+ */
+public static boolean operate(List operates, String sourceFilename, String outputFilename) throws ImgOperateException {
+ try {
+ IMOperation op = new IMOperation();
+ boolean operateTag = false;
+ String waterFilename = null;
+ for (ImgWrapper.Builder.Operate operate : operates) {
+ if (!operate.valid()) {
+ continue;
+ }
+
+ if (operate.getOperateType() == ImgWrapper.Builder.OperateType.CROP) {
+ op.crop(operate.getWidth(), operate.getHeight(), operate.getX(), operate.getY());
+// if (operate.getRadio() != null && Math.abs(operate.getRadio() - 1.0) > 0.005) {
+// // 需要对图片进行缩放
+// op.resize((int) Math.ceil(operate.getWidth() * operate.getRadio()));
+// }
+ operateTag = true;
+ } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.ROTATE) {
+ // fixme 180度旋转后裁图,会出现bug, 先这么兼容
+ double rotate = operate.getRotate();
+ if (Math.abs((rotate % 360) - 180) <= 0.005) {
+ rotate += 0.01;
+ }
+ op.rotate(rotate);
+ operateTag = true;
+ } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.SCALE) {
+ if (operate.getRadio() == null) {
+ if (operate.isForceScale()) { // 强制根据给定的参数进行压缩时
+ StringBuilder builder = new StringBuilder();
+ builder.append("!").append(operate.getWidth() == null ? "" : operate.getWidth()).append("x");
+ builder.append(operate.getHeight() == null ? "" : operate.getHeight());
+ op.addRawArgs("-resize", builder.toString());
+ } else {
+ op.resize(operate.getWidth(), operate.getHeight());
+ }
+ } else if(Math.abs(operate.getRadio() - 1) > 0.005) {
+ // 对图片进行比例缩放
+ op.addRawArgs("-resize", "%" + (operate.getRadio() * 100));
+ }
+
+ if (operate.getQuality() != null && operate.getQuality() > 0) {
+ op.quality(operate.getQuality().doubleValue());
+ }
+ operateTag = true;
+ } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.FLIP) {
+ op.flip();
+ operateTag = true;
+ } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.FLOP) {
+ op.flop();
+ operateTag = true;
+ } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.WATER && waterFilename == null) {
+ // 当前只支持添加一次水印
+ op.geometry(operate.getWidth(), operate.getHeight(), operate.getX(), operate.getY())
+ .composite();
+ waterFilename = operate.getWaterFilename();
+ operateTag = true;
+ } else if (operate.getOperateType() == ImgWrapper.Builder.OperateType.BOARD) {
+ op.border(operate.getWidth(), operate.getHeight()).bordercolor(operate.getColor());
+ operateTag = true;
+ }
+ }
+
+ if (!operateTag) {
+ throw new ImgOperateException("operate illegal! operates: " + operates);
+ }
+ op.addImage(sourceFilename);
+ if (waterFilename != null) {
+ op.addImage(waterFilename);
+ }
+ op.addImage(outputFilename);
+ /** 传true到构造函数中,则表示使用GraphicMagic, 裁图时,图片大小会变 */
+ ConvertCmd convert = new ConvertCmd();
+ convert.run(op);
+ } catch (IOException e) {
+ log.error("file read error!, e: {}", e);
+ return false;
+ } catch (InterruptedException e) {
+ log.error("interrupt exception! e: {}", e);
+ return false;
+ } catch (IM4JavaException e) {
+ log.error("im4java exception! e: {}", e);
+ return false;
+ }
+ return true;
+}
+```
+
+### 5. 接口封装
+
+包装一个对外使用的方式
+
+```java
+public class ImgWrapper {
+ /**
+ * 根据本地图片进行处理
+ *
+ * @param file
+ * @return
+ */
+ public static Builder of(String file) {
+ checkForNull(file, "Cannot specify null for input file.");
+ if (file.startsWith("http")) {
+ throw new IllegalArgumentException("file should not be URI resources! file: " + file);
+ }
+ return Builder.ofString(file);
+ }
+
+ public static Builder of(URI uri) {
+ checkForNull(uri, "Cannot specify null for input uri.");
+ return Builder.ofUrl(uri);
+ }
+
+ public static Builder of(InputStream inputStream) {
+ checkForNull(inputStream, "Cannot specify null for InputStream.");
+ return Builder.ofStream(inputStream);
+ }
+
+
+ private static void checkForNull(Object o, String message) {
+ if (o == null) {
+ throw new NullPointerException(message);
+ }
+ }
+}
+```
+
+## IV. 测试
+
+上面基本上完成了整个接口的设计与实现,接下来就是接口测试了
+
+
+给出几个使用姿势演示,更多可以查看:[ImgWrapperTest](https://github.com/liuyueyi/quick-media/blob/master/plugins/imagic-plugin/src/test/java/com/github/hui/quick/plugin/test/ImgWrapperTest.java)
+
+```java
+private static final String url = "http://a.hiphotos.baidu.com/image/pic/item/14ce36d3d539b6006a6cc5d0e550352ac65cb733.jpg";
+private static final String localFile = "blogInfoV2.png";
+
+@Test
+public void testCutImg() {
+
+ try {
+ // 保存到本地
+ ImgWrapper.of(URI.create(url))
+ .crop(10, 20, 500, 500)
+ .toFile();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+}
+
+
+@Test
+public void testRotateImg() {
+ try {
+ InputStream stream = FileReadUtil.getStreamByFileName(localFile);
+ BufferedImage img = ImgWrapper.of(stream).rotate(90).asImg();
+ System.out.println("----" + img);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+}
+
+
+@Test
+public void testWater() {
+ BufferedImage img;
+ try {
+ img = ImgWrapper.of(URI.create(url))
+ .board(10, 10, "red")
+ .water(localFile, 100, 100)
+ .asImg();
+ System.out.println("--- " + img);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+}
+```
+
+## V. 其他
+
+### 项目:
+
+**GitHub:**
+
+- 项目:[Quick-Media](https://github.com/liuyueyi/quick-media)
+- 源码:[imagic-plugin](https://github.com/liuyueyi/quick-media/tree/master/plugins/imagic-plugin)
+
+**Gitee:**
+
+- 项目:[Quick-Media](https://gitee.com/liuyueyi/quick-media)
+- 源码:[imagic-plugin](https://gitee.com/liuyueyi/quick-media/tree/master/plugins/imagic-plugin)
+
+
+### 个人博客: [一灰灰Blog](https://liuyueyi.github.io/hexblog)
+
+基于hexo + github pages搭建的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
+
+
+### 声明
+
+尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如发现bug或者有更好的建议,随时欢迎批评指正
+
+- 微博地址: [小灰灰Blog](https://weibo.com/p/1005052169825577/home)
+- QQ: 一灰灰/3302797840
+
+### 扫描关注
+
+![QrCode](https://raw.githubusercontent.com/liuyueyi/Source/master/img/info/blogInfoV2.png)
diff --git "a/docs/\346\217\222\344\273\266/magic/\346\246\202\350\247\210.md" "b/docs/\346\217\222\344\273\266/magic/\346\246\202\350\247\210.md"
new file mode 100644
index 00000000..3bf12917
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/magic/\346\246\202\350\247\210.md"
@@ -0,0 +1 @@
+# 概览
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/markdown/markdown\350\275\254html.md" "b/docs/\346\217\222\344\273\266/markdown/markdown\350\275\254html.md"
new file mode 100644
index 00000000..663b2e39
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/markdown/markdown\350\275\254html.md"
@@ -0,0 +1,339 @@
+# markdown to html
+## 背景
+将markdown文档转换为html,主要是web应用中有些场景会用到,如博客系统,支持markdown语法的评论功能等
+
+要自己去实现这个功能,并没有那么简单,当然面向GitHub编程,就简单很多了
+
+## 设计
+
+### 1. markdown 转 html
+
+在github上相关的开源包还是比较多的,选择了一个之前看 Solo (一个开源的java博客系统)源码时,接触到的辅助包 `flexmark`
+
+因为`flexmark` 工程比较庞大,我们这里只依赖其中的markdown转html的工具类,所以只需要添加下面的依赖即可
+
+```java
+
+
+ com.vladsch.flexmark
+ flexmark
+ 0.26.4
+
+
+ com.vladsch.flexmark
+ flexmark-util
+ 0.26.4
+
+
+
+ com.vladsch.flexmark
+ flexmark-ext-tables
+ 0.26.4
+
+```
+
+
+使用姿势也比较简单,从demo中查看,下面给出一个从文件中读取内容并转换的过程
+
+```java
+// 从文件中读取markdown内容
+InputStream stream = this.getClass().getClassLoader().getResourceAsStream("test.md");
+BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "utf-8"));
+
+List list = reader.lines().collect(Collectors.toList());
+String content = Joiner.on("\n").join(list);
+
+
+
+// markdown to image
+MutableDataSet options = new MutableDataSet();
+options.setFrom(ParserEmulationProfile.MARKDOWN);
+options.set(Parser.EXTENSIONS, Arrays.asList(new Extension[] { TablesExtension.create()}));
+Parser parser = Parser.builder(options).build();
+HtmlRenderer renderer = HtmlRenderer.builder(options).build();
+
+Node document = parser.parse(content);
+String html = renderer.render(document);
+```
+
+## 实现
+
+> 上面给出了设计思路,主要是利用开源包进行转换,在此基础上进行封装,使得调用方式更加友好
+
+
+### 0. 依赖
+
+pom直接依赖即可
+
+```java
+
+
+ com.vladsch.flexmark
+ flexmark
+ ${flexmark.version}
+
+
+ com.vladsch.flexmark
+ flexmark-util
+ ${flexmark.version}
+
+
+ com.vladsch.flexmark
+ flexmark-ext-tables
+ ${flexmark.version}
+
+
+```
+
+### 1. `MarkdownEntity`
+
+这个entity类除了markdown转换后的html内容之外,还增加了`css` 和 `divStyle` 属性
+
+- `css` 属性,主要是用于美化输出html的展示样式
+- `divStyle` 同样也是为了定义一些通用的属性,会在html内容外层加一个``标签,可以在其中进行统一的宽高设置,字体....
+
+```java
+@Data
+public class MarkdownEntity {
+
+ public static String TAG_WIDTH = "";
+
+ // css 样式
+ private String css;
+
+ // 最外网的div标签, 可以用来设置样式,宽高,字体等
+ private Map
divStyle = new ConcurrentHashMap<>();
+
+ // 转换后的html文档
+ private String html;
+
+ public MarkdownEntity() {
+ }
+
+ public MarkdownEntity(String html) {
+ this.html = html;
+ }
+
+ @Override
+ public String toString() {
+ return css + "\n\n" + html + "\n
";
+ }
+
+
+ private String parseDiv() {
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry entry : divStyle.entrySet()) {
+ builder.append(entry.getKey()).append("=\"")
+ .append(entry.getValue()).append("\" ");
+ }
+ return builder.toString();
+ }
+
+
+ public void addDivStyle(String attrKey, String value) {
+ if (divStyle.containsKey(attrKey)) {
+ divStyle.put(attrKey, divStyle.get(attrKey) + " " + value);
+ } else {
+ divStyle.put(attrKey, value);
+ }
+ }
+
+
+ public void addWidthCss(String tag) {
+ String wcss = String.format(TAG_WIDTH, tag);
+ css += wcss;
+ }
+}
+```
+
+### 2. `MarkDown2HtmlWrapper`
+> 操作封装类
+
+- 从git上找了一个简单`markdown.css`样式, 为了避免每次都去文件中读,这里定义一个静态变量 `MD_CSS`
+- 为了利用css样式,需要给 `MarkdownEntity` 的 divStyle 新增一个 `class: markdown-body `样式
+- markdown to html 的主要逻辑在 `parse` 方法中,注意下为了支持table,加载了对应的table插件
+
+```java
+public class MarkDown2HtmlWrapper {
+
+ private static String MD_CSS = null;
+
+ static {
+ try {
+ MD_CSS = FileReadUtil.readAll("md/huimarkdown.css");
+ MD_CSS = "\n";
+ } catch (Exception e) {
+ MD_CSS = "";
+ }
+ }
+
+
+ /**
+ * 将本地的markdown文件,转为html文档输出
+ *
+ * @param path 相对地址or绝对地址 ("/" 开头)
+ * @return
+ * @throws IOException
+ */
+ public static MarkdownEntity ofFile(String path) throws IOException {
+ return ofStream(FileReadUtil.getStreamByFileName(path));
+ }
+
+
+ /**
+ * 将网络的markdown文件,转为html文档输出
+ *
+ * @param url http开头的url格式
+ * @return
+ * @throws IOException
+ */
+ public static MarkdownEntity ofUrl(String url) throws IOException {
+ return ofStream(FileReadUtil.getStreamByFileName(url));
+ }
+
+
+ /**
+ * 将流转为html文档输出
+ *
+ * @param stream
+ * @return
+ */
+ public static MarkdownEntity ofStream(InputStream stream) {
+ BufferedReader bufferedReader = new BufferedReader(
+ new InputStreamReader(stream, Charset.forName("UTF-8")));
+ List lines = bufferedReader.lines().collect(Collectors.toList());
+ String content = Joiner.on("\n").join(lines);
+ return ofContent(content);
+ }
+
+
+ /**
+ * 直接将markdown语义的文本转为html格式输出
+ *
+ * @param content markdown语义文本
+ * @return
+ */
+ public static MarkdownEntity ofContent(String content) {
+ String html = parse(content);
+ MarkdownEntity entity = new MarkdownEntity();
+ entity.setCss(MD_CSS);
+ entity.setHtml(html);
+ entity.addDivStyle("class", "markdown-body ");
+ return entity;
+ }
+
+
+ /**
+ * markdown to image
+ *
+ * @param content markdown contents
+ * @return parse html contents
+ */
+ public static String parse(String content) {
+ MutableDataSet options = new MutableDataSet();
+ options.setFrom(ParserEmulationProfile.MARKDOWN);
+
+ // enable table parse!
+ options.set(Parser.EXTENSIONS, Arrays.asList(TablesExtension.create()));
+
+
+ Parser parser = Parser.builder(options).build();
+ HtmlRenderer renderer = HtmlRenderer.builder(options).build();
+
+ Node document = parser.parse(content);
+ return renderer.render(document);
+ }
+
+}
+```
+
+
+## 测试
+
+测试代码比较简单,下面三行即可
+
+```java
+@Test
+public void markdown2html() throws IOException {
+ String file = "md/tutorial.md";
+ MarkdownEntity html = MarkDown2HtmlWrapper.ofFile(file);
+ System.out.println(html.toString());
+}
+```
+
+
+markdown 文件如下
+
+```
+Markdown cells support standard Markdown syntax as well as GitHub Flavored Markdown (GFM). Open the preview to see these rendered.
+
+### Basics
+
+# H1
+## H2
+### H3
+#### H4
+##### H5
+###### H6
+
+---
+
+*italic*, **bold**, ~~Scratch this.~~
+
+`inline code`
+
+### Lists
+
+1. First ordered list item
+2. Another item
+ * Unordered sub-list.
+1. Actual numbers don't matter, just that it's a number
+ 1. Ordered sub-list
+4. And another item.
+
+### Quote
+
+> Peace cannot be kept by force; it can only be achieved by understanding.
+
+### Links
+
+[I'm an inline-style link](https://www.google.com)
+http://example.com
+
+You can also create a link to another note: (Note menu -> Copy Note Link -> Paste)
+[01 - Getting Started](quiver-note-url/D2A1CC36-CC97-4701-A895-EFC98EF47026)
+
+### Tables
+
+| Tables | Are | Cool |
+| ------------- |:-------------:| -----:|
+| col 3 is | right-aligned | $1600 |
+| col 2 is | centered | $12 |
+| zebra stripes | are neat | $1 |
+
+### GFM Task Lists
+
+- [ ] a task list item
+- [ ] list syntax required
+- [ ] normal **formatting**, @mentions, #1234 refs
+- [ ] incomplete
+- [x] completed
+
+### Inline LaTeX
+
+You can use inline LaTeX inside Markdown cells as well, for example, $x^2$.
+```
+
+测试示意图
+
+![testCase](https://static.oschina.net/uploads/img/201709/11200153_lCgP.gif)
+
+## 其他
+
+项目地址:[https://github.com/liuyueyi/quick-media](https://github.com/liuyueyi/quick-media)
+
+个人博客:[一灰的个人博客](http://blog.zbang.online:8080)
+
+公众号获取更多:
+
+![个人信息](https://static.oschina.net/uploads/img/201709/05212311_hPmi.png "个人信息")
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/markdown/markdown\350\275\254image.md" "b/docs/\346\217\222\344\273\266/markdown/markdown\350\275\254image.md"
new file mode 100644
index 00000000..767814cf
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/markdown/markdown\350\275\254image.md"
@@ -0,0 +1,392 @@
+# markdown 转 image
+
+> 前段时间实现了长图文生成的基本功能,然后想了下能否有个进阶版,直接将markdown生成渲染后的图片呢?
+
+## 思路
+> 有不少的库可以将 markdown 转为 html,那么这个需求就可以转为 html转Image了
+
+### 1. markdown 转 html
+
+可以参看之前的博文[《Java 实现 markdown转Html》](https://my.oschina.net/u/566591/blog/1535380)
+
+### 2. html 转 图片
+
+主要的核心问题就在这里了,如何实现html转图片?
+
+- 直接实现html转图片的包没怎么见,看到一个 `html2image`, 还不太好用
+- 在 AWT or Swing 的Panel上显示网页,在把Panel输出为 image 文件
+- 使用js相关技术实现转换
+
+本篇博文具体实现以 `html2image` 的实现逻辑作为参考,然后定制实现一把(后面有机会写一篇利用js来实现html转图片的博文)
+
+#### html2image 的实现原理
+
+`html2image` 基本上没啥维护了,内部主要是利用了 `xhtmlrender` 实现html渲染为图片
+
+```java
+Graphics2DRenderer renderer = new Graphics2DRenderer();
+// 设置渲染内容
+renderer.setDocument(document, document.getDocumentURI());
+
+// 获取Graphics2D
+graphics2D = bufferedImage.createGraphics();
+renderer.layout(graphics2D, dimension);
+
+// 内容渲染
+renderer.render(graphics2D);
+```
+
+#### 说明
+
+1. 为什么并不直接使用 `java-html2image` ?
+
+ - 因为有些定制的场景支持得不太友好,加上源码也比较简单,所以干脆站在前人的基础上进行拓展
+
+2. 设计目标(这里指html转图片的功能)
+
+ - 生成图片的宽可指定
+ - 支持对线上网页进行转图片
+ - 支持对html中指定的区域进行转换
+ - css样式渲染支持
+
+
+## 实现
+
+> 本篇先会先实现一个基本的功能,即读去`markdown`文档, 并转为一张图片
+
+
+### 1. markdown 转 html 封装
+> 利用之前封装的 `MarkDown2HtmlWrapper` 工具类
+
+具体实现逻辑参考项目工程,和markdown转html博文
+
+### 2. html 转 image
+
+#### 参数配置项 `HtmlRenderOptions`
+
+**注意**
+
+- html 为 Document 属性
+- autoW, autoH 用于控制是否自适应html实际的长宽
+
+```java
+@Data
+public class HtmlRenderOptions {
+
+ /**
+ * 输出图片的宽
+ */
+ private Integer w;
+
+
+ /**
+ * 输出图片的高
+ */
+ private Integer h;
+
+
+ /**
+ * 是否自适应宽
+ */
+ private boolean autoW;
+
+
+ /**
+ * 是否自适应高
+ */
+ private boolean autoH;
+
+
+ /**
+ * 输出图片的格式
+ */
+ private String outType;
+
+ /**
+ * html相关内容
+ */
+ private Document document;
+}
+```
+
+### 封装处理类
+> 同样采用Builder模式来进行配置项设置
+
+```java
+public class Html2ImageWrapper {
+
+ private static DOMParser domParser;
+
+ static {
+ domParser = new DOMParser(new HTMLConfiguration());
+ try {
+ domParser.setProperty("http://cyberneko.org/html/properties/names/elems", "lower");
+ } catch (Exception e) {
+ throw new RuntimeException("Can't create HtmlParserImpl", e);
+ }
+ }
+
+
+ private HtmlRenderOptions options;
+
+
+ private Html2ImageWrapper(HtmlRenderOptions options) {
+ this.options = options;
+ }
+
+
+ private static Document parseDocument(String content) throws Exception {
+ domParser.parse(new InputSource(new StringReader(content)));
+ return domParser.getDocument();
+ }
+
+
+ public static Builder of(String html) {
+ return new Builder().setHtml(html);
+ }
+
+
+ public static Builder ofMd(MarkdownEntity entity) {
+ return new Builder().setHtml(entity);
+ }
+
+
+ public BufferedImage asImage() {
+ BufferedImage bf = HtmlRender.parseImage(options);
+ return bf;
+ }
+
+
+ public boolean asFile(String absFileName) throws IOException {
+ File file = new File(absFileName);
+ FileUtil.mkDir(file);
+
+ BufferedImage bufferedImage = asImage();
+ if (!ImageIO.write(bufferedImage, options.getOutType(), file)) {
+ throw new IOException("save image error!");
+ }
+
+ return true;
+ }
+
+
+ public String asString() throws IOException {
+ BufferedImage img = asImage();
+ return Base64Util.encode(img, options.getOutType());
+ }
+
+
+ @Getter
+ public static class Builder {
+ /**
+ * 输出图片的宽
+ */
+ private Integer w = 600;
+
+ /**
+ * 输出图片的高度
+ */
+ private Integer h;
+
+ /**
+ * true,根据网页的实际宽渲染;
+ * false, 则根据指定的宽进行渲染
+ */
+ private boolean autoW = true;
+
+ /**
+ * true,根据网页的实际高渲染;
+ * false, 则根据指定的高进行渲染
+ */
+ private boolean autoH = false;
+
+
+ /**
+ * 输出图片的格式
+ */
+ private String outType = "jpg";
+
+
+ /**
+ * 待转换的html内容
+ */
+ private MarkdownEntity html;
+
+
+ public Builder setW(Integer w) {
+ this.w = w;
+ return this;
+ }
+
+ public Builder setH(Integer h) {
+ this.h = h;
+ return this;
+ }
+
+ public Builder setAutoW(boolean autoW) {
+ this.autoW = autoW;
+ return this;
+ }
+
+ public Builder setAutoH(boolean autoH) {
+ this.autoH = autoH;
+ return this;
+ }
+
+ public Builder setOutType(String outType) {
+ this.outType = outType;
+ return this;
+ }
+
+
+ public Builder setHtml(String html) {
+ this.html = new MarkdownEntity();
+ return this;
+ }
+
+
+ public Builder setHtml(MarkdownEntity html) {
+ this.html = html;
+ return this;
+ }
+
+ public Html2ImageWrapper build() throws Exception {
+ HtmlRenderOptions options = new HtmlRenderOptions();
+ options.setW(w);
+ options.setH(h);
+ options.setAutoW(autoW);
+ options.setAutoH(autoH);
+ options.setOutType(outType);
+
+
+ if (fontColor != null) {
+ html.addDivStyle("style", "color:" + options.getFontColor());
+ }
+ html.addDivStyle("style", "width:" + w + ";");
+ html.addWidthCss("img");
+ html.addWidthCss("code");
+
+ options.setDocument(parseDocument(html.toString()));
+
+ return new Html2ImageWrapper(options);
+ }
+
+ }
+}
+```
+
+上面的实现,有个需要注意的地方
+
+**如何将html格式的字符串,转为 Document 对象**
+
+利用了开源工具 `nekohtml`, 可以较好的实现html标签解析,看一下`DOMParse` 的初始化过程
+
+
+```java
+private static DOMParser domParser;
+
+static {
+ domParser = new DOMParser(new HTMLConfiguration());
+ try {
+ domParser.setProperty("http://cyberneko.org/html/properties/names/elems",
+ "lower");
+ } catch (Exception e) {
+ throw new RuntimeException("Can't create HtmlParserImpl", e);
+ }
+}
+```
+
+try语句块中的内容并不能缺少,否则最终的样式会错乱,关于 `nekohtml` 的使用说明,可以查阅相关教程
+
+
+上面的封装,主要是`HtmlRenderOptions`的构建,主要的渲染逻辑则在下面
+
+### 渲染
+
+利用 `xhtmlrenderer` 实现html的渲染
+
+- 宽高的自适应
+- 图片的布局,内容渲染
+
+```java
+public class HtmlRender {
+
+ /**
+ * 输出图片
+ *
+ * @param options
+ * @return
+ */
+ public static BufferedImage parseImage(HtmlRenderOptions options) {
+ int width = options.getW();
+ int height = options.getH() == null ? 1024 : options.getH();
+ Graphics2DRenderer renderer = new Graphics2DRenderer();
+ renderer.setDocument(options.getDocument(), options.getDocument().getDocumentURI());
+
+
+ Dimension dimension = new Dimension(width, height);
+ BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+ Graphics2D graphics2D = GraphicUtil.getG2d(bufferedImage);
+
+ // 自适应修改生成图片的宽高
+ if (options.isAutoH() || options.getH() == null) {
+ // do layout with temp buffer
+ renderer.layout(graphics2D, new Dimension(width, height));
+ graphics2D.dispose();
+
+ Rectangle size = renderer.getMinimumSize();
+ final int autoWidth = options.isAutoW() ? (int) size.getWidth() : width;
+ final int autoHeight = (int) size.getHeight();
+ bufferedImage = new BufferedImage(autoWidth, autoHeight, BufferedImage.TYPE_INT_RGB);
+ dimension = new Dimension(autoWidth, autoHeight);
+
+ graphics2D = GraphicUtil.getG2d(bufferedImage);
+ }
+
+
+ renderer.layout(graphics2D, dimension);
+ renderer.render(graphics2D);
+ graphics2D.dispose();
+ return bufferedImage;
+ }
+}
+```
+
+## 测试
+
+```java
+@Test
+public void testParse() throws Exception {
+ String file = "md/tutorial.md";
+
+ MarkdownEntity html = MarkDown2HtmlWrapper.ofFile(file);
+
+ BufferedImage img = Html2ImageWrapper.ofMd(html)
+ .setW(600)
+ .setAutoW(false)
+ .setAutoH(true)
+ .setOutType("jpg")
+ .build()
+ .asImage();
+
+ ImageIO.write(img, "jpg", new File("/Users/yihui/Desktop/md.jpg"));
+}
+```
+
+输出图片
+
+![out.jpg](https://camo.githubusercontent.com/90dc0e400056650e581439945f82a193d4ae9f17/687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f313430353933362d336164353064323937386533643465302e6a70673f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970253743696d61676556696577322f322f772f31323430)
+
+然后演示一个对项目中实际的教程文档输出图片的动态示意图, 因为生成的图片特别特别长,所以就不贴输出的图片了,有兴趣的同学可以下载工程,实际跑一下看看
+
+源markdown文件地址:
+
+[https://github.com/liuyueyi/quick-media/blob/master/doc/images/imgGenV2.md](https://github.com/liuyueyi/quick-media/blob/master/doc/images/imgGenV2.md)
+
+
+![show.gif](https://camo.githubusercontent.com/113c4e98f0dc2d149d74d23fca154368bfb7f7aa/687474703a2f2f75706c6f61642d696d616765732e6a69616e7368752e696f2f75706c6f61645f696d616765732f313430353933362d613734393739653762303736623335322e6769663f696d6167654d6f6772322f6175746f2d6f7269656e742f7374726970)
+
+
+## 参考博文
+
+- [Java 实现HTML 页面转成image 图片](https://www.2cto.com/kf/201303/196946.html)
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/markdown/\346\246\202\350\247\210.md" "b/docs/\346\217\222\344\273\266/markdown/\346\246\202\350\247\210.md"
new file mode 100644
index 00000000..3bf12917
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/markdown/\346\246\202\350\247\210.md"
@@ -0,0 +1 @@
+# 概览
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/phantom/Java&PhantomJs\345\256\236\347\216\260html\350\276\223\345\207\272\345\233\276\347\211\207.md" "b/docs/\346\217\222\344\273\266/phantom/Java&PhantomJs\345\256\236\347\216\260html\350\276\223\345\207\272\345\233\276\347\211\207.md"
new file mode 100644
index 00000000..5504ec48
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/phantom/Java&PhantomJs\345\256\236\347\216\260html\350\276\223\345\207\272\345\233\276\347\211\207.md"
@@ -0,0 +1,125 @@
+## Java & PhantomJs 实现html输出图片
+> 借助phantomJs来实现将html网页输出为图片
+
+
+## 前提准备
+
+### 1. phantom.js 安装
+
+```sh
+# 1. 下载
+
+## mac 系统
+wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-macosx.zip
+
+
+## linux 系统
+wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
+
+## windows 系统
+## 就不要玩了,没啥意思
+
+
+# 2. 解压
+
+sudo su
+tar -jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
+
+# 如果解压报错,则安装下面的
+# yum -y install bzip2
+
+# 3. 安装
+
+## 简单点,移动到bin目录下
+
+cp phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin
+
+# 4. 验证是否ok
+phantomjs --version
+
+# 输出版本号,则表示ok
+```
+
+### 2. java依赖配置
+
+maven 配置添加依赖
+
+```xml
+
+
+ org.seleniumhq.selenium
+ selenium-java
+ 2.53.1
+
+
+ com.github.detro
+ ghostdriver
+ 2.1.0
+
+
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+```
+
+
+## 开动
+
+主要调用phantomjs来实现html渲染图片的逻辑如下
+
+```java
+public class Html2ImageByJsWrapper {
+
+ private static PhantomJSDriver webDriver = getPhantomJs();
+
+ private static PhantomJSDriver getPhantomJs() {
+ //设置必要参数
+ DesiredCapabilities dcaps = new DesiredCapabilities();
+ //ssl证书支持
+ dcaps.setCapability("acceptSslCerts", true);
+ //截屏支持
+ dcaps.setCapability("takesScreenshot", true);
+ //css搜索支持
+ dcaps.setCapability("cssSelectorsEnabled", true);
+ //js支持
+ dcaps.setJavascriptEnabled(true);
+ //驱动支持(第二参数表明的是你的phantomjs引擎所在的路径,which/whereis phantomjs可以查看)
+ // fixme 这里写了执行, 可以考虑判断系统是否有安装,并获取对应的路径 or 开放出来指定路径
+ dcaps.setCapability(PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY, "/usr/local/bin/phantomjs");
+ //创建无界面浏览器对象
+ return new PhantomJSDriver(dcaps);
+ }
+
+
+ public static BufferedImage renderHtml2Image(String url) throws IOException {
+ webDriver.get(url);
+ File file = webDriver.getScreenshotAs(OutputType.FILE);
+ return ImageIO.read(file);
+ }
+
+}
+```
+
+## 测试case
+
+```java
+@Test
+public void testRender() throws IOException {
+ BufferedImage img = null;
+ for (int i = 0; i < 20; ++i) {
+ String url = "https://my.oschina.net/u/566591/blog/1580020";
+ long start = System.currentTimeMillis();
+ img = Html2ImageByJsWrapper.renderHtml2Image(url);
+ long end = System.currentTimeMillis();
+ System.out.println("cost: " + (end - start));
+ }
+
+ System.out.println(DomUtil.toDomSrc(Base64Util.encode(img, "png"), MediaType.ImagePng));
+
+}
+```
diff --git "a/docs/\346\217\222\344\273\266/phantom/\346\246\202\350\247\210.md" "b/docs/\346\217\222\344\273\266/phantom/\346\246\202\350\247\210.md"
new file mode 100644
index 00000000..3bf12917
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/phantom/\346\246\202\350\247\210.md"
@@ -0,0 +1 @@
+# 概览
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/svg/\346\246\202\350\247\210.md" "b/docs/\346\217\222\344\273\266/svg/\346\246\202\350\247\210.md"
new file mode 100644
index 00000000..2150da5b
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/svg/\346\246\202\350\247\210.md"
@@ -0,0 +1,2 @@
+# 概览
+
diff --git "a/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\272\214\347\273\264\347\240\201\346\217\222\344\273\266\346\246\202\350\247\210.md" "b/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\272\214\347\273\264\347\240\201\346\217\222\344\273\266\346\246\202\350\247\210.md"
new file mode 100644
index 00000000..ac21d6b3
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\272\214\347\273\264\347\240\201\346\217\222\344\273\266\346\246\202\350\247\210.md"
@@ -0,0 +1,6 @@
+# 二维码插件概览
+> qrcode-plugin 在开源二维码处理库zxing的基础上,重写了二维码渲染的逻辑,以支持更灵活、更个性化的二维码生成规则
+
+## 1. 使用尝鲜
+
+## 2. 功能介绍
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\272\214\347\273\264\347\240\201\347\224\237\346\210\220\346\234\215\345\212\241\344\271\213\346\267\261\345\272\246\345\256\232\345\210\266.md" "b/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\272\214\347\273\264\347\240\201\347\224\237\346\210\220\346\234\215\345\212\241\344\271\213\346\267\261\345\272\246\345\256\232\345\210\266.md"
new file mode 100644
index 00000000..f11c274d
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\272\214\347\273\264\347\240\201\347\224\237\346\210\220\346\234\215\345\212\241\344\271\213\346\267\261\345\272\246\345\256\232\345\210\266.md"
@@ -0,0 +1,760 @@
+# 二维码生成服务之深度定制
+> 之前写了一篇二维码服务定制的博文,现在则在之前的基础上,再进一步,花样的实现深度定制的需求,我们的目标是二维码上的一切都是可以由用户来随意指定
+
+## 设计
+
+### 1. 技术相关
+
+- zxing 开源包用于生成二维码
+- springboot 搭建基本web服务,提供http接口
+- awt 用于图片的编辑
+- httpclient 用于从网络下载图片
+- lombok 简化编码
+
+
+### 2. 目的
+> 既然是对二维码服务的深度定制,那我们的目的基本上就是二维码上面出现的东西,都可以按照我们的需求进行改造
+
+这里,我们设计两个目的,一个基础版,一个进阶版
+
+- 基础版
+ - 二维码大小
+ - 边距留白指定
+ - 添加logo
+ - 加背景
+
+- 进阶版
+ - 二维码中前置色和背景色可自由指定颜色
+ - 二维码中前置色(黑白二维码中的黑色区域)可换成圆点,三角形等其他图形
+ - 前置色可用图片替换
+ - 探测点(三个矩形框就是探测点,也叫做定位点)颜色可配置
+ - 探测点可用图片替换
+ - 二维码样式(圆角矩形,添加边框,边框颜色可指定)
+ - 背景支持填充(填充在背景图片的某个区域)和覆盖方式(全覆盖背景图,二维码设置透明度)
+
+上面是我们希望达到的目的,下面给几个实际生成的二维码瞅瞅最终的效果
+
+![生成demom](https://static.oschina.net/uploads/img/201708/12175500_AYTi.png "demo")
+
+(小灰灰blog公众号,实际测试时,请用微信扫一扫)
+
+### 3. 前提准备
+
+#### 1.相关博文
+
+在直接进入上面花样的二维码生成之前,有必要安利一把zxing的基本使用方式,本篇将不会对如何使用zxing进行说明,有需求了解的可以参考下面几篇相关博文,此篇博文是 `《spring-boot & zxing 搭建二维码服务》` 的衍生
+
+- [java 实现二维码生成工具类](https://my.oschina.net/u/566591/blog/872728)
+- [zxing 二维码大白边一步一步修复指南](https://my.oschina.net/u/566591/blog/872770)
+- [spring-boot & zxing 搭建二维码服务](https://my.oschina.net/u/566591/blog/1457164)
+- [二维码服务拓展(支持logo,圆角logo,背景图,颜色配置)](https://my.oschina.net/u/566591/blog/1491697)
+
+#### 2. 源码介绍
+
+此外下面直接贴代码,可能有些地方不太容易理解,下面将简单对一些辅助类进行必要的功能说明
+
+源码直通车:[quick-media](https://github.com/liuyueyi/quick-media)
+
+涉及到的工具类:
+
+- `QrCodeUtil` : 二维码生成工具类
+ - 生成二维码矩阵
+ - 根据二维码矩阵渲染二维码图片
+- `ImageUtil` : 图片处理工具类
+ - 加载图片(支持从本地,网络获取图片)
+ - 绘制二维码logo
+ - 图片圆角化
+ - 图片添加纯色边框
+ - 背景绘制
+ - 二维码绘制
+- `QrCodeOptions`: 二维码配置类
+- `BitMatrixEx`: 二维码矩阵信息扩展类
+- `QrCodeGenWrapper`: 二维码生成服务包装类,与用户进行交互的主要接口,设置配置信息,生成二维码,选择输出方式,都是通过它来设定
+
+### 4. 实现说明
+
+#### 第一步,生成矩阵
+我们直接利用zxing来生成二维码矩阵信息,并用来实例我们的矩阵拓展类 `BitMatrixEx`
+
+在我们的工程中,相关的代码为
+
+```java
+com.hust.hui.quickmedia.common.util.QrCodeUtil#encode
+```
+
+在这里,只关心下面几个参数的生成,其他的基本上就是zxing库的调用了
+
+```java
+/**
+ * 实际生成二维码的宽
+ */
+private int width;
+
+
+/**
+ * 实际生成二维码的高
+ */
+private int height;
+
+
+/**
+ * 左白边大小
+ */
+private int leftPadding;
+
+/**
+ * 上白边大小
+ */
+private int topPadding;
+
+/**
+ * 矩阵信息缩放比例
+ */
+private int multiple;
+
+private ByteMatrix byteMatrix;
+```
+
+在理解为什么有上面的几个参数之前,有必要看一下`byteMatrix`到底是个什么东西?(自问自答:二维码矩阵)
+
+下面截出前面二维码中对应的矩阵信息,在生成一张二维码时,下面的1表示一个小黑块,0表示一个小白块;
+
+```
+ 1 1 1 1 1 1 1 0 0 0 0 1 0 0 0 1 1 1 0 0 0 0 0 1 0 1 1 0 0 0 1 1 1 1 1 1 1
+ 1 0 0 0 0 0 1 0 1 0 1 1 1 1 0 1 1 0 0 1 0 0 1 0 1 0 0 0 1 0 1 0 0 0 0 0 1
+ 1 0 1 1 1 0 1 0 1 1 0 0 0 0 0 1 1 0 1 0 1 0 1 0 0 1 0 0 1 0 1 0 1 1 1 0 1
+ 1 0 1 1 1 0 1 0 1 1 0 1 0 1 0 1 0 1 0 0 0 1 0 1 0 0 1 0 1 0 1 0 1 1 1 0 1
+ 1 0 1 1 1 0 1 0 1 0 0 1 0 1 1 1 0 0 0 0 1 1 0 0 1 1 0 0 0 0 1 0 1 1 1 0 1
+ 1 0 0 0 0 0 1 0 1 0 0 1 0 1 0 0 0 1 1 0 1 1 1 0 0 1 0 0 1 0 1 0 0 0 0 0 1
+ 1 1 1 1 1 1 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 1 1 1 1 1 1
+ 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 1 0 0 1 0 1 1 1 0 0 0 0 0 0 0 0
+ 0 0 1 0 0 1 1 1 1 1 0 1 1 0 0 1 1 1 1 0 1 1 0 0 1 0 0 0 0 1 0 1 1 1 1 1 0
+ 0 1 0 0 0 1 0 1 0 1 1 1 0 1 1 0 1 1 0 0 1 0 1 0 1 0 0 1 0 0 1 1 0 1 0 0 1
+ 1 1 1 1 0 0 1 0 1 0 1 1 1 0 0 1 1 1 1 0 1 1 1 0 1 0 0 1 0 1 1 0 1 0 0 1 1
+ 1 0 1 1 0 0 0 0 0 1 0 0 0 1 0 1 0 0 1 1 1 1 1 1 1 0 1 0 0 0 0 1 0 0 0 0 1
+ 1 0 1 1 0 0 1 0 1 0 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 1 1 1 0 0 0 0 1 1
+ 1 0 1 0 0 0 0 0 0 1 1 0 0 1 1 1 1 0 0 1 0 1 1 1 0 1 0 0 1 1 1 0 0 0 1 0 1
+ 1 1 1 1 0 0 1 0 0 0 0 0 1 1 1 0 1 1 1 0 0 0 1 1 1 0 0 1 0 1 1 0 0 1 1 0 1
+ 1 1 1 0 0 0 0 1 0 1 0 1 1 1 1 1 1 0 0 0 1 0 1 0 1 0 1 0 0 1 0 0 0 1 0 0 0
+ 0 1 1 0 0 0 1 0 0 1 0 1 0 0 1 0 0 0 0 0 0 0 1 0 1 0 0 1 1 0 1 0 0 0 0 1 0
+ 0 0 1 1 1 0 0 0 0 1 0 1 1 1 0 0 1 0 1 1 1 0 0 0 1 0 0 1 1 1 1 1 0 0 1 0 1
+ 0 0 1 1 1 1 1 1 0 0 1 0 1 0 0 1 0 0 0 1 1 1 1 1 0 1 1 1 1 1 0 1 0 0 1 0 1
+ 0 0 1 0 1 1 0 1 1 0 1 0 0 0 0 0 1 0 0 0 1 1 1 0 1 0 0 1 1 1 0 1 1 1 0 1 1
+ 0 0 1 0 1 0 1 0 0 1 0 1 1 0 0 1 1 0 0 0 1 1 1 0 0 1 0 0 1 0 1 0 0 1 0 1 0
+ 1 1 0 1 0 0 0 0 1 0 0 0 0 1 1 0 0 1 1 1 0 0 0 1 1 1 0 1 0 1 1 1 0 1 1 1 1
+ 1 0 0 0 1 0 1 1 0 1 1 0 1 1 0 0 0 0 1 1 0 0 1 1 1 0 0 1 0 1 1 1 1 0 0 1 1
+ 0 1 1 0 1 1 0 0 1 1 0 1 1 0 1 0 1 0 1 1 1 1 0 0 0 0 1 1 1 1 1 1 0 1 0 1 0
+ 1 0 0 1 0 1 1 0 1 1 0 0 0 1 1 0 1 0 0 0 0 0 1 0 1 1 0 0 0 0 1 0 1 1 1 1 1
+ 0 0 1 0 1 1 0 1 0 0 1 0 1 1 1 0 0 1 1 0 0 0 0 0 1 0 0 1 0 0 0 1 0 0 1 1 1
+ 1 1 0 1 1 0 1 1 0 1 0 0 1 0 0 1 1 0 0 0 0 1 1 0 1 1 0 1 1 1 0 1 0 1 1 0 1
+ 0 0 1 0 1 1 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 1 0 1 1 0 1 0 1 0 1 0 0 1 0 1 1
+ 1 1 0 1 0 0 1 0 0 0 0 0 1 0 0 1 0 0 1 0 1 1 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0
+ 0 0 0 0 0 0 0 0 1 1 1 0 1 0 1 1 1 0 1 1 1 0 1 1 0 1 0 0 1 0 0 0 1 1 0 1 1
+ 1 1 1 1 1 1 1 0 1 0 0 1 0 0 0 0 0 0 1 0 0 1 0 1 0 1 0 0 1 0 1 0 1 1 1 0 1
+ 1 0 0 0 0 0 1 0 1 0 0 0 1 0 0 1 0 1 0 0 0 0 0 1 1 0 1 1 1 0 0 0 1 1 0 1 0
+ 1 0 1 1 1 0 1 0 0 0 0 1 0 0 1 1 1 0 1 0 0 1 0 1 1 0 1 1 1 1 1 1 1 0 0 1 0
+ 1 0 1 1 1 0 1 0 0 1 0 0 1 1 0 1 0 0 0 1 1 1 0 0 0 0 0 1 1 0 0 1 0 1 1 0 1
+ 1 0 1 1 1 0 1 0 1 1 0 0 1 1 1 1 0 1 0 0 0 1 1 1 1 1 0 0 1 1 0 0 1 1 0 1 1
+ 1 0 0 0 0 0 1 0 0 0 1 1 1 0 1 1 1 1 1 0 0 1 1 0 0 0 1 0 1 0 0 0 1 0 0 0 0
+ 1 1 1 1 1 1 1 0 0 0 1 1 1 0 1 0 0 0 0 0 0 1 1 0 1 0 0 1 1 0 1 0 1 1 0 0 1
+```
+
+当生成了上面的人矩阵之后,最终的二维码绘制都是根据上面的矩阵来的,将1的地方用我们希望绘制的样式(如圆点,三角形,图形等)来替换;
+
+上面的矩阵表示的基本的二维码信息,最终渲染二维码图片时,我们还需要知道最终的图片大小,四周的留白空间,每个二维码信息在放射到最终二维码图片时放大的倍数,有这些参数之后才能唯一指定最终的输出结果,所以就有了上面的几个参数
+
+
+#### 第二步, 二维码信息的绘制
+
+根据上面的二维码矩阵来渲染二维码图片,先考虑最简单的,没有任何配置时,可以怎么玩?
+
+下面用到的参数来自`BitMatirxEx`
+
+1. 绘制整个背景(直接根据给定的宽高绘制矩形背景即可)
+
+ ```java
+ g2.setColor(Color.WHITE);
+ g2.fillRect(0, 0, qrCodeWidth, qrCodeHeight);
+ ```
+2. 二维码矩阵中(x,y) == 1的地方绘制小方块
+
+ ```java
+ g2.setColor(Color.BLACK);
+ g2.fillRect(x+leftPadding, y+topPadding, multiple, multiple);
+ ```
+3. 根据2可知,整个渲染就是矩阵(二维数组)的遍历而已
+
+----
+
+根据上面的生成逻辑,我们可以很清晰的发现,有几个目标是可以很简单实现的
+
+- 二维码背景色&前置色的指定(就是在1,2步骤中的setColor用指定的颜色替换即可)
+- 替换二维码黑色小方块为其他图形
+
+这里是一个小关键点了,在具体的实现中,我提供了:
+ - 三角形,
+ - 矩形(即二维码默认格式),
+ - 五边形(钻石),
+ - 六边形,
+ - 八边形,
+ - 圆形,
+ - 图片
+
+**比较遗憾的是五角星没有支持,没想到合适的绘制方式**
+
+不同的样式,对应的绘制不同,我们定义了一个枚举,来定义不同的样式对应的绘制规则,优势就是扩展自定义样式方便,下面给出具体的绘制代码
+
+```java
+/**
+ * 绘制二维码信息的样式
+ */
+public enum DrawStyle {
+ RECT { // 矩形
+
+ @Override
+ public void draw(Graphics2D g2d, int x, int y, int w, int h, BufferedImage img) {
+ g2d.fillRect(x, y, w, h);
+ }
+
+ @Override
+ public boolean expand(ExpandType expandType) {
+ return true;
+ }
+ },
+ CIRCLE {
+ // 圆点
+ @Override
+ public void draw(Graphics2D g2d, int x, int y, int w, int h, BufferedImage img) {
+ g2d.fill(new Ellipse2D.Float(x, y, w, h));
+ }
+
+ @Override
+ public boolean expand(ExpandType expandType) {
+ return expandType == ExpandType.SIZE4;
+ }
+ },
+ TRIANGLE {
+ // 三角形
+ @Override
+ public void draw(Graphics2D g2d, int x, int y, int w, int h, BufferedImage img) {
+ int px[] = {x, x + (w >> 1), x + w};
+ int py[] = {y + w, y, y + w};
+ g2d.fillPolygon(px, py, 3);
+ }
+
+ @Override
+ public boolean expand(ExpandType expandType) {
+ return false;
+ }
+ },
+ DIAMOND {
+ // 五边形-钻石
+ @Override
+ public void draw(Graphics2D g2d, int x, int y, int size, int h, BufferedImage img) {
+ int cell4 = size >> 2;
+ int cell2 = size >> 1;
+ int px[] = {x + cell4, x + size - cell4, x + size, x + cell2, x};
+ int py[] = {y, y, y + cell2, y + size, y + cell2};
+ g2d.fillPolygon(px, py, 5);
+ }
+
+ @Override
+ public boolean expand(ExpandType expandType) {
+ return expandType == ExpandType.SIZE4;
+ }
+ },
+ SEXANGLE {
+ // 六边形
+ @Override
+ public void draw(Graphics2D g2d, int x, int y, int size, int h, BufferedImage img) {
+ int add = size >> 2;
+ int px[] = {x + add, x + size - add, x + size, x + size - add, x + add, x};
+ int py[] = {y, y, y + add + add, y + size, y + size, y + add + add};
+ g2d.fillPolygon(px, py, 6);
+ }
+
+ @Override
+ public boolean expand(ExpandType expandType) {
+ return expandType == ExpandType.SIZE4;
+ }
+ },
+ OCTAGON {
+ // 八边形
+ @Override
+ public void draw(Graphics2D g2d, int x, int y, int size, int h, BufferedImage img) {
+ int add = size / 3;
+ int px[] = {x + add, x + size - add, x + size, x + size, x + size - add, x + add, x, x};
+ int py[] = {y, y, y + add, y + size - add, y + size, y + size, y + size - add, y + add};
+ g2d.fillPolygon(px, py, 8);
+ }
+
+ @Override
+ public boolean expand(ExpandType expandType) {
+ return expandType == ExpandType.SIZE4;
+ }
+ },
+ IMAGE {
+ // 自定义图片
+ @Override
+ public void draw(Graphics2D g2d, int x, int y, int w, int h, BufferedImage img) {
+ g2d.drawImage(img, x, y, w, h, null);
+ }
+
+ @Override
+ public boolean expand(ExpandType expandType) {
+ return true;
+ }
+ },;
+
+ private static Map map;
+
+ static {
+ map = new HashMap<>(7);
+ for (DrawStyle style : DrawStyle.values()) {
+ map.put(style.name(), style);
+ }
+ }
+
+ public static DrawStyle getDrawStyle(String name) {
+ if (StringUtils.isBlank(name)) { // 默认返回矩形
+ return RECT;
+ }
+
+
+ DrawStyle style = map.get(name.toUpperCase());
+ return style == null ? RECT : style;
+ }
+
+
+ public abstract void draw(Graphics2D g2d, int x, int y, int w, int h, BufferedImage img);
+
+
+ /**
+ * 返回是否支持绘制图形的扩展
+ *
+ * @param expandType
+ * @return
+ */
+ public abstract boolean expand(ExpandType expandType);
+}
+```
+
+---
+
+
+上面完成了二维码样式的定制,还有一个探测点(或者叫做定位点)的定制,也得在这一步中进行;
+
+普通的二维码结构如下
+
+![二维码结构](https://static.oschina.net/uploads/img/201708/12175554_0vL7.png "二维码结构")
+
+探测点就是二维码中的三个方块,再看上面的二维码矩阵,下图中的两个红框内的其实就是上面的两个探测图形,外面的那层全0是分割符
+
+![探测图形](https://static.oschina.net/uploads/img/201708/12175609_SD3H.png "在这里输入图片标题")
+
+两者一结合,很容易就可以搞定探测图形的位置,第一行有多少个连续的1就表示探测图形的size是多大
+
+所以探测图形的私人定制就比较简单了,下面是具体的绘制代码(下面实现图片绘制,内外框采用不同颜色的实现)
+
+```java
+// 设置三个位置探测图形
+if (x < detectCornerSize && y < detectCornerSize // 左上角
+ || (x < detectCornerSize && y >= byteH - detectCornerSize) // 左下脚
+ || (x >= byteW - detectCornerSize && y < detectCornerSize)) { // 右上角
+
+ if (qrCodeConfig.getDetectOptions().getDetectImg() != null) {
+ // 绘制图片
+ g2.drawImage(qrCodeConfig.getDetectOptions().getDetectImg(),
+ leftPadding + x * infoSize, topPadding + y * infoSize,
+ infoSize * detectCornerSize, infoSize * detectCornerSize, null);
+
+ for (int addX = 0; addX < detectCornerSize; addX++) {
+ for (int addY = 0; addY < detectCornerSize; addY++) {
+ bitMatrix.getByteMatrix().set(x + addX, y + addY, 0);
+ }
+ }
+ continue;
+ }
+
+
+ if (x == 0 || x == detectCornerSize - 1 || x == byteW - 1 || x == byteW - detectCornerSize
+ || y == 0 || y == detectCornerSize - 1 || y == byteH - 1 || y == byteH - detectCornerSize) {
+ // 外层的框
+ g2.setColor(detectOutColor);
+ } else {
+ // 内层的框
+ g2.setColor(detectInnerColor);
+ }
+
+ g2.fillRect(leftPadding + x * infoSize, topPadding + y * infoSize, infoSize, infoSize);
+}
+```
+
+---
+
+到此,二维码主体的定制基本上over了,就最终的实现来看,我们的目标中除了logo和背景外,其他的基本上都是ok的,这里稍稍拓展了一点,如果连续两个为1,或一个小矩形全是1,则将这相同的几个串在一起,因此才有了上面的部分图形较大的情况(当然这个是可选的配置)
+
+
+下面贴出整个绘制代码
+
+```java
+public static BufferedImage drawQrInfo(QrCodeOptions qrCodeConfig, BitMatrixEx bitMatrix) {
+ int qrCodeWidth = bitMatrix.getWidth();
+ int qrCodeHeight = bitMatrix.getHeight();
+ int infoSize = bitMatrix.getMultiple();
+ BufferedImage qrCode = new BufferedImage(qrCodeWidth, qrCodeHeight, BufferedImage.TYPE_INT_RGB);
+
+
+ // 绘制的背景色
+ Color bgColor = qrCodeConfig.getDrawOptions().getBgColor();
+ // 绘制前置色
+ Color preColor = qrCodeConfig.getDrawOptions().getPreColor();
+
+ // 探测图形外圈的颜色
+ Color detectOutColor = qrCodeConfig.getDetectOptions().getOutColor();
+ // 探测图形内圈的颜色
+ Color detectInnerColor = qrCodeConfig.getDetectOptions().getInColor();
+
+
+ int leftPadding = bitMatrix.getLeftPadding();
+ int topPadding = bitMatrix.getTopPadding();
+
+ Graphics2D g2 = qrCode.createGraphics();
+ g2.setComposite(AlphaComposite.Src);
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+
+ // 直接背景铺满整个图
+ g2.setColor(bgColor);
+ g2.fillRect(0, 0, qrCodeWidth, qrCodeHeight);
+
+ // 探测图形的大小
+ int detectCornerSize = bitMatrix.getByteMatrix().get(0, 5) == 1 ? 7 : 5;
+
+ int byteW = bitMatrix.getByteMatrix().getWidth();
+ int byteH = bitMatrix.getByteMatrix().getHeight();
+
+ boolean row2 = false;
+ boolean col2 = false;
+ QrCodeOptions.DrawStyle drawStyle = qrCodeConfig.getDrawOptions().getDrawStyle();
+ for (int x = 0; x < byteW; x++) {
+ for (int y = 0; y < byteH; y++) {
+ if (bitMatrix.getByteMatrix().get(x, y) == 0) {
+ continue;
+ }
+
+ // 设置三个位置探测图形
+ if (x < detectCornerSize && y < detectCornerSize // 左上角
+ || (x < detectCornerSize && y >= byteH - detectCornerSize) // 左下脚
+ || (x >= byteW - detectCornerSize && y < detectCornerSize)) { // 右上角
+
+ if (qrCodeConfig.getDetectOptions().getDetectImg() != null) {
+ g2.drawImage(qrCodeConfig.getDetectOptions().getDetectImg(),
+ leftPadding + x * infoSize, topPadding + y * infoSize,
+ infoSize * detectCornerSize, infoSize * detectCornerSize, null);
+
+ for (int addX = 0; addX < detectCornerSize; addX++) {
+ for (int addY = 0; addY < detectCornerSize; addY++) {
+ bitMatrix.getByteMatrix().set(x + addX, y + addY, 0);
+ }
+ }
+ continue;
+ }
+
+
+ if (x == 0 || x == detectCornerSize - 1 || x == byteW - 1 || x == byteW - detectCornerSize
+ || y == 0 || y == detectCornerSize - 1 || y == byteH - 1 || y == byteH - detectCornerSize) {
+ // 外层的框
+ g2.setColor(detectOutColor);
+ } else {
+ // 内层的框
+ g2.setColor(detectInnerColor);
+ }
+
+ g2.fillRect(leftPadding + x * infoSize, topPadding + y * infoSize, infoSize, infoSize);
+ } else { // 着色二维码主题
+ g2.setColor(preColor);
+
+ if (!qrCodeConfig.getDrawOptions().isEnableScale()) {
+ drawStyle.draw(g2,
+ leftPadding + x * infoSize,
+ topPadding + y * infoSize,
+ infoSize,
+ infoSize,
+ qrCodeConfig.getDrawOptions().getImg());
+ continue;
+ }
+
+
+ // 支持拓展时
+ row2 = rightTrue(bitMatrix.getByteMatrix(), x, y);
+ col2 = belowTrue(bitMatrix.getByteMatrix(), x, y);
+
+ if (row2 && col2 && diagonalTrue(bitMatrix.getByteMatrix(), x, y) &&
+ qrCodeConfig.getDrawOptions().enableScale(QrCodeOptions.ExpandType.SIZE4)) {
+ // 四个相等
+ bitMatrix.getByteMatrix().set(x + 1, y, 0);
+ bitMatrix.getByteMatrix().set(x + 1, y + 1, 0);
+ bitMatrix.getByteMatrix().set(x, y + 1, 0);
+ drawStyle.draw(g2,
+ leftPadding + x * infoSize,
+ topPadding + y * infoSize,
+ infoSize << 1,
+ infoSize << 1,
+ qrCodeConfig.getDrawOptions().getSize4Img());
+ } else if (row2 && qrCodeConfig.getDrawOptions().enableScale(QrCodeOptions.ExpandType.ROW2)) { // 横向相同
+ bitMatrix.getByteMatrix().set(x + 1, y, 0);
+ drawStyle.draw(g2,
+ leftPadding + x * infoSize,
+ topPadding + y * infoSize,
+ infoSize << 1,
+ infoSize,
+ qrCodeConfig.getDrawOptions().getRow2Img());
+ } else if (col2 && qrCodeConfig.getDrawOptions().enableScale(QrCodeOptions.ExpandType.COL2)) { // 列的两个
+ bitMatrix.getByteMatrix().set(x, y + 1, 0);
+ drawStyle.draw(g2,
+ leftPadding + x * infoSize,
+ topPadding + y * infoSize,
+ infoSize,
+ infoSize << 1,
+ qrCodeConfig.getDrawOptions().getCol2img());
+ } else {
+ drawStyle.draw(g2,
+ leftPadding + x * infoSize,
+ topPadding + y * infoSize,
+ infoSize,
+ infoSize,
+ qrCodeConfig.getDrawOptions().getImg());
+ }
+ }
+ }
+ }
+ g2.dispose();
+ return qrCode;
+}
+
+
+private static boolean rightTrue(ByteMatrix byteMatrix, int x, int y) {
+ return x + 1 < byteMatrix.getWidth() && byteMatrix.get(x + 1, y) == 1;
+}
+
+private static boolean belowTrue(ByteMatrix byteMatrix, int x, int y) {
+ return y + 1 < byteMatrix.getHeight() && byteMatrix.get(x, y + 1) == 1;
+}
+
+// 对角是否相等
+private static boolean diagonalTrue(ByteMatrix byteMatrix, int x, int y) {
+ return byteMatrix.get(x + 1, y + 1) == 1;
+}
+```
+
+#### 第三步. logo&背景的绘制
+> 到第二步,其实二维码就已经绘制完成了,二维码和背景都是在二维码这种图片上做文章,一个是往二维码上加图片,一个是将二维码绘制在另一张图片上
+
+一个图片在另一个图片上绘制没啥技术含量,稍微特别点的就是logo的圆角和边框了
+
+[《二维码服务拓展(支持logo,圆角logo,背景图,颜色配置)》](https://my.oschina.net/u/566591/blog/1491697) 较清晰的说了如何绘制圆角图片,圆角边框
+
+
+不想看上面博文的没啥关系,下面直接贴出代码,算是比较通用的方法了,与二维码项目本身没什么黏合
+
+```java
+/**
+ * 生成边框
+ *
+ * @param image 原图
+ * @param cornerRadius 角度 0表示直角
+ * @param color 边框颜色
+ * @return
+ */
+public static BufferedImage makeRoundBorder(BufferedImage image,
+ int cornerRadius,
+ Color color) {
+ int size = image.getWidth() / 15;
+ int w = image.getWidth() + size;
+ int h = image.getHeight() + size;
+ BufferedImage output = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g2 = output.createGraphics();
+ g2.setComposite(AlphaComposite.Src);
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2.setColor(color == null ? Color.WHITE : color);
+ g2.fill(new RoundRectangle2D.Float(0, 0, w, h, cornerRadius, cornerRadius));
+
+ g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
+ g2.drawImage(image, size >> 1, size >> 1, null);
+ g2.dispose();
+
+ return output;
+}
+
+
+/**
+ * 生成圆角图片
+ *
+ * @param image 原始图片
+ * @param cornerRadius 圆角的弧度大小(根据实测效果,一般建议为图片宽度的1/4), 0表示直角
+ * @return 返回圆角图
+ */
+public static BufferedImage makeRoundedCorner(BufferedImage image,
+ int cornerRadius) {
+ int w = image.getWidth();
+ int h = image.getHeight();
+ BufferedImage output = new BufferedImage(w, h,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g2 = output.createGraphics();
+ g2.setComposite(AlphaComposite.Src);
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ g2.setColor(Color.WHITE);
+ g2.fill(new RoundRectangle2D.Float(0, 0, w, h, cornerRadius,
+ cornerRadius));
+
+
+ g2.setComposite(AlphaComposite.SrcAtop);
+ g2.drawImage(image, 0, 0, null);
+
+ g2.dispose();
+
+ return output;
+}
+```
+
+与上一篇定制博文有一点区别的是,对背景图的支持进行了扩展,除了支持之前的设置二维码透明度,全覆盖背景图之外,又支持了在背景图的指定位置处进行绘制二维码,因为这一块确实没什么好讲的,干脆贴下代码好了
+
+```java
+/**
+ * 绘制背景图
+ *
+ * @param source 二维码图
+ * @param bgImgOptions 背景图信息
+ * @return
+ */
+public static BufferedImage drawBackground(BufferedImage source, QrCodeOptions.BgImgOptions bgImgOptions) {
+ int sW = source.getWidth();
+ int sH = source.getHeight();
+
+ // 背景的图宽高不应该小于原图
+ int bgW = bgImgOptions.getBgW() < sW ? sW : bgImgOptions.getBgW();
+ int bgH = bgImgOptions.getBgH() < sH ? sH : bgImgOptions.getBgH();
+
+
+ // 背景图缩放
+ BufferedImage bg = bgImgOptions.getBgImg();
+ if (bg.getWidth() != bgW || bg.getHeight() != bgH) {
+ BufferedImage temp = new BufferedImage(bgW, bgH, BufferedImage.TYPE_INT_ARGB);
+ temp.getGraphics().drawImage(bg.getScaledInstance(bgW, bgH, Image.SCALE_SMOOTH)
+ , 0, 0, null);
+ bg = temp;
+ }
+
+ Graphics2D g2d = bg.createGraphics();
+ if (bgImgOptions.getBgImgStyle() == QrCodeOptions.BgImgStyle.FILL) {
+ // 选择一块区域进行填充
+ g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2d.drawImage(source, bgImgOptions.getStartX(), bgImgOptions.getStartY(), sW, sH, null);
+ } else {
+ // 覆盖方式
+ int x = (bgW - sW) >> 1;
+ int y = (bgH - sH) >> 1;
+ g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, bgImgOptions.getOpacity())); // 透明度, 避免看不到背景
+ g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g2d.drawImage(source, x, y, sW, sH, null);
+ g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
+ }
+ g2d.dispose();
+ bg.flush();
+ return bg;
+}
+```
+
+## 测试
+
+开发完了之后,就要开始愉快的进行测试了,测试一个全乎的
+
+```java
+@Test
+public void testGenStyleCodeV2() {
+ String msg = "http://weixin.qq.com/r/FS9waAPEg178rUcL93oH";
+
+ try {
+ String logo = "logo.jpg";
+ String bg = "qrbg.jpg";
+ BufferedImage img = QrCodeGenWrapper.of(msg)
+ .setW(550)
+ .setDrawPreColor(0xff002fa7) // 宝石蓝
+ .setDetectOutColor(0xff0000ff)
+ .setDetectInColor(Color.RED)
+ .setDetectImg("detect.png")
+ .setPadding(1)
+ .setErrorCorrection(ErrorCorrectionLevel.H)
+ .setLogo(logo)
+ .setLogoStyle(QrCodeOptions.LogoStyle.ROUND)
+ .setLogoBgColor(0xff00cc00)
+ .setLogoRate(15)
+ .setDrawStyle(QrCodeOptions.DrawStyle.IMAGE.name())
+ .setDrawEnableScale(true)
+ .setDrawImg("xhrBase.jpg")
+ .setDrawRow2Img("xhrr2.jpeg")
+ .setDrawCol2Img("xhrc2.jpeg")
+ .setDrawSize4Img("xhrSize4.jpg")
+ .setBgStyle(QrCodeOptions.BgImgStyle.FILL)
+ .setBgImg(bg)
+ .setBgStartX(230)
+ .setBgStartY(330)
+ .asBufferedImage();
+
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ ImageIO.write(img, "png", outputStream);
+ String img64 = Base64Util.encode(outputStream);
+ System.out.println("");
+ } catch (Exception e) {
+ System.out.println("create qrcode error! e: " + e);
+ Assert.assertTrue(false);
+ }
+}
+```
+
+演示case:
+
+![测试示例]( http://s3.mogucdn.com/mlcdn/c45406/170812_3b0jhcgedi4abe7kcjbja5l0276bd_1224x708.gif)
+
+一个最终定格的二维码
+
+![定格图](https://static.oschina.net/uploads/img/201708/12175633_sOfz.png "生成二维码图")
+
+
+### 说明
+
+上面的改造,在实际使用时,建议多测试测试是否可以扫描出来,腾讯系列产品的二维码扫描特别给力,一般都能很迅速的识别,其他的就不好说了
+
+
+## 其他
+
+相关博文
+
+- [java 实现二维码生成工具类](https://my.oschina.net/u/566591/blog/872728)
+- [zxing 二维码大白边一步一步修复指南](https://my.oschina.net/u/566591/blog/872770)
+- [spring-boot & zxing 搭建二维码服务](https://my.oschina.net/u/566591/blog/1457164)
+- [二维码服务拓展(支持logo,圆角logo,背景图,颜色配置)](https://my.oschina.net/u/566591/blog/1491697)
+
+
+项目地址: [https://github.com/liuyueyi/quick-media](https://github.com/liuyueyi/quick-media)
+
+
+个人博客:[一灰的个人博客](http://blog.zbang.online:8080)
+
+公众号获取更多:
+
+![个人信息](https://static.oschina.net/uploads/img/201708/12175649_wn2r.png "个人信息")
+
+
+## 参考
+
+- [二维码基础原理](http://cli.im/news/help/10601)
+
+
+
diff --git "a/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\272\214\347\273\264\347\240\201\347\232\204\345\237\272\347\241\200\346\234\215\345\212\241\346\213\223\345\261\225.md" "b/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\272\214\347\273\264\347\240\201\347\232\204\345\237\272\347\241\200\346\234\215\345\212\241\346\213\223\345\261\225.md"
new file mode 100644
index 00000000..109e2107
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\272\214\347\273\264\347\240\201\347\232\204\345\237\272\347\241\200\346\234\215\345\212\241\346\213\223\345\261\225.md"
@@ -0,0 +1,577 @@
+# 二维码的基础服务拓展
+> zxing 提供了二维码一些列的功能,在日常生活中,可以发现很多二维码并不仅仅是简单的黑白矩形块,有的添加了文字,加了logo,定制颜色,背景等,本片博文则着手于此,进行基础服务的拓展
+
+本片博文拓展的功能点:
+
+- 支持在二维码中间添加logo
+- logo样式选择:支持圆角/直角logo,支持logo的边框选择
+- 二维码颜色选择(可自由将原来的黑白色进行替换)
+- 支持背景图片
+- 支持探测图形的前置色选择
+
+一个包含上面所有功能点的二维码如下图
+
+![http://s2.mogucdn.com/mlcdn/c45406/170728_45a54147f26eh3lf1aiek04c1620h_300x300.png](http://s2.mogucdn.com/mlcdn/c45406/170728_45a54147f26eh3lf1aiek04c1620h_300x300.png)
+
+## 准备
+> 由于之前有一篇博文[《spring-boot & zxing 搭建二维码服务》](https://my.oschina.net/u/566591/blog/1457164) 较为消息的介绍了设计一个二维码服务的过程,因此这篇则不再整体设计上多做说明,主要的功能点将集中在以上几个功能点设计与实现上
+
+源码地址: [https://github.com/liuyueyi/quick-media](https://github.com/liuyueyi/quick-media)
+
+这篇博文,将不对二维码生成的细节进行说明,某些地方如有疑惑(如二维码生成时的一些参数,渲染逻辑等)请直接查看代码,or百度谷歌,或者私聊也可。
+
+下面简单说明一下这个工程中与二维码相关的几个类的作用
+
+### 1. `QrCodeOptions.java`
+
+二维码的各种配置参数
+
+### 2. `QrCodeGenWrapper.java`
+
+封装了二维码的参数设置和处理方法,通常来讲对于使用者而言,只需要使用这个类中的方法即可实现二维码的生成,如生成上面的二维码测试代码如下
+
+```java
+@Test
+public void testGenColorCode() {
+ String msg = "https://my.oschina.net/u/566591/blog/1359432";
+ // 根据本地文件生成待logo的二维码, 重新着色位置探测图像
+ try {
+ String logo = "logo.jpg";
+ String bg = "bg.png";
+ BufferedImage img = QrCodeGenWrapper.of(msg)
+ .setW(300)
+ .setPreColor(0xff0000ff)
+ .setBgColor(0xffFFFF00)
+ .setDetectCornerPreColor(0xffff0000)
+ .setPadding(2)
+ .setLogo(logo)
+ .setLogoStyle(QrCodeOptions.LogoStyle.ROUND)
+ .setLogoBgColor(0xff00cc00)
+ .setBackground(bg)
+ .asBufferedImage();
+
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ ImageIO.write(img, "png", outputStream);
+ System.out.println(Base64Util.encode(outputStream));
+ } catch (Exception e) {
+ System.out.println("create qrcode error! e: " + e);
+ Assert.assertTrue(false);
+ }
+}
+```
+
+### 3.`QrCodeUtil.java`
+
+二维码工具类,包括生成二维码矩阵信息,二维码图片渲染,输出`BufferedIamge`对象等
+
+
+### 4. `ImageUtil.java`
+
+图片处理辅助类,实现图片圆角化,添加边框,插入logo,绘制背景图等
+
+---
+
+## 设计与实现
+
+### 1. 二维码颜色可配置
+> 二维码颜色的选择,主要在将二维码矩阵转换成图的时候,选择不同的颜色进行渲染即可,我们主要的代码将放在 `com.hust.hui.quickmedia.common.util.QrCodeUtil#toBufferedImage` 方法中
+
+先看一下实现逻辑
+
+```java
+/**
+ * 根据二维码配置 & 二维码矩阵生成二维码图片
+ *
+ * @param qrCodeConfig
+ * @param bitMatrix
+ * @return
+ * @throws IOException
+ */
+public static BufferedImage toBufferedImage(QrCodeOptions qrCodeConfig, BitMatrixEx bitMatrix) throws IOException {
+ int qrCodeWidth = bitMatrix.getWidth();
+ int qrCodeHeight = bitMatrix.getHeight();
+ BufferedImage qrCode = new BufferedImage(qrCodeWidth, qrCodeHeight, BufferedImage.TYPE_INT_RGB);
+
+ for (int x = 0; x < qrCodeWidth; x++) {
+ for (int y = 0; y < qrCodeHeight; y++) {
+ qrCode.setRGB(x, y,
+ bitMatrix.get(x, y) ?
+ qrCodeConfig.getMatrixToImageConfig().getPixelOnColor() :
+ qrCodeConfig.getMatrixToImageConfig().getPixelOffColor());
+ }
+ }
+
+ ...
+}
+```
+
+**注意**
+
+`BitMatrixEx` 是 `com.google.zxing.common.BitMatrix` 的拓展,后面说明为什么这么做,
+
+此处知晓 `com.hust.hui.quickmedia.common.qrcode.BitMatrixEx#get` 等同于 `com.google.zxing.common.BitMatrix#get`即可
+
+**说明**
+
+- 上面的逻辑比较清晰,先创建一个置顶大小的图像,然后遍历 `bitMatrix`,对图像进行着色
+
+- `bitMatrix.get(x, y) == true` 表示该处为二维码的有效信息(这个是在二维码生成时决定,zxing的二维码生成逻辑负责生成BitMatrix对象,原理此处省略,因为我也没仔细研究),然后涂上配置的前置色;否则表示空白背景,涂上背景色即可
+
+
+### 2. 位置探测图行可配置
+> 位置探测图形就是二维码的左上角,右上角,左下角的三个矩形框(前面途中的三个红框),用于定位二维码使用,这里的实现确保它的颜色可以与二维码的前置色不同
+
+经过上面的二维码颜色渲染,很容易就可以想到,在二维码的最终渲染时,对位置探测图形采用不同的颜色进行渲染即可,所以渲染代码如下
+
+
+```java
+/**
+ * 根据二维码配置 & 二维码矩阵生成二维码图片
+ *
+ * @param qrCodeConfig
+ * @param bitMatrix
+ * @return
+ * @throws IOException
+ */
+public static BufferedImage toBufferedImage(QrCodeOptions qrCodeConfig, BitMatrixEx bitMatrix) throws IOException {
+ int qrCodeWidth = bitMatrix.getWidth();
+ int qrCodeHeight = bitMatrix.getHeight();
+ BufferedImage qrCode = new BufferedImage(qrCodeWidth, qrCodeHeight, BufferedImage.TYPE_INT_RGB);
+
+ for (int x = 0; x < qrCodeWidth; x++) {
+ for (int y = 0; y < qrCodeHeight; y++) {
+ if (bitMatrix.isDetectCorner(x, y)) { // 着色位置探测图形
+ qrCode.setRGB(x, y,
+ bitMatrix.get(x, y) ?
+ qrCodeConfig.getDetectCornerColor().getPixelOnColor() :
+ qrCodeConfig.getDetectCornerColor().getPixelOffColor());
+ } else { // 着色二维码主题
+ qrCode.setRGB(x, y,
+ bitMatrix.get(x, y) ?
+ qrCodeConfig.getMatrixToImageConfig().getPixelOnColor() :
+ qrCodeConfig.getMatrixToImageConfig().getPixelOffColor());
+ }
+ }
+ }
+
+ ....
+}
+```
+
+相比较与之前,在遍历逻辑中,多了一个是否为位置探测图形的分支判断
+
+```java
+if (bitMatrix.isDetectCorner(x, y)) { // 着色位置探测图形
+ qrCode.setRGB(x, y,
+ bitMatrix.get(x, y) ?
+ qrCodeConfig.getDetectCornerColor().getPixelOnColor() :
+ qrCodeConfig.getDetectCornerColor().getPixelOffColor());
+}
+```
+
+所以我们的问题就是如何判断(x,y)坐标对应的位置是否为位置探测图形?
+
+#### 位置探测图形判定
+
+这个判定的逻辑,就需要深入到二维码矩阵的生成逻辑中,直接给出对应代码位置
+
+```java
+// Embed basic patterns
+// The basic patterns are:
+// - Position detection patterns
+// - Timing patterns
+// - Dark dot at the left bottom corner
+// - Position adjustment patterns, if need be
+com.google.zxing.qrcode.encoder.MatrixUtil#embedBasicPatterns
+
+
+// 确定位置探测图形的方法
+com.google.zxing.qrcode.encoder.MatrixUtil#embedPositionDetectionPatternsAndSeparators
+
+// 自适应调整矩阵的方法
+com.google.zxing.qrcode.encoder.MatrixUtil#maybeEmbedPositionAdjustmentPatterns
+```
+
+直接看代码,会发现位置探测图形的二维数组如下
+
+```java
+private static final int[][] POSITION_DETECTION_PATTERN = {
+ {1, 1, 1, 1, 1, 1, 1},
+ {1, 0, 0, 0, 0, 0, 1},
+ {1, 0, 1, 1, 1, 0, 1},
+ {1, 0, 1, 1, 1, 0, 1},
+ {1, 0, 1, 1, 1, 0, 1},
+ {1, 0, 0, 0, 0, 0, 1},
+ {1, 1, 1, 1, 1, 1, 1},
+};
+
+private static final int[][] POSITION_ADJUSTMENT_PATTERN = {
+ {1, 1, 1, 1, 1},
+ {1, 0, 0, 0, 1},
+ {1, 0, 1, 0, 1},
+ {1, 0, 0, 0, 1},
+ {1, 1, 1, 1, 1},
+};
+```
+
+到这里,我们的判断就比较清晰了,位置探测图形有两种规格,5 or 7
+
+在看具体的判定逻辑之前,先看 `BitMatrixEx`增强类,可以判定(x,y)坐标处是否为位置探测图形,内部判定逻辑和 `BitMatrix`中是否为二维码有效信息的判定一致
+
+```java
+@Getter
+@Setter
+public class BitMatrixEx {
+ private final int width;
+ private final int height;
+ private final int rowSize;
+ private final int[] bits;
+
+
+ private BitMatrix bitMatrix;
+
+ public BitMatrixEx(BitMatrix bitMatrix) {
+ this(bitMatrix.getWidth(), bitMatrix.getHeight());
+ this.bitMatrix = bitMatrix;
+
+ }
+
+ private BitMatrixEx(int width, int height) {
+ if (width < 1 || height < 1) {
+ throw new IllegalArgumentException("Both dimensions must be greater than 0");
+ }
+
+ this.width = width;
+ this.height = height;
+ this.rowSize = (width + 31) / 32;
+ bits = new int[rowSize * height];
+ }
+
+
+
+ public void setRegion(int left, int top, int width, int height) {
+ int right = left + width;
+ int bottom = top + height;
+
+ for (int y = top; y < bottom; y++) {
+ int offset = y * rowSize;
+ for (int x = left; x < right; x++) {
+ bits[offset + (x / 32)] |= 1 << (x & 0x1f);
+ }
+ }
+ }
+
+
+ public boolean get(int x, int y) {
+ return bitMatrix.get(x, y);
+ }
+
+
+ public boolean isDetectCorner(int x, int y) {
+ int offset = y * rowSize + (x / 32);
+ return ((bits[offset] >>> (x & 0x1f)) & 1) != 0;
+ }
+}
+```
+
+
+**位置判定逻辑**
+
+位置判定逻辑在 `com.hust.hui.quickmedia.common.util.QrCodeUtil#renderResult` 方法中,简单说一下这个方法的作用
+
+- 根据 `com.google.zxing.qrcode.encoder.QRCode` 生成 `BitMatrixEx` 对象
+- 内部实现二维码白边的修复(详情参考博文:[《zxing 二维码大白边一步一步修复指南》](https://my.oschina.net/u/566591/blog/872770))
+- 内部实现位置探测图形的判定逻辑
+
+直接看判定逻辑
+
+```java
+// 获取位置探测图形的size,根据源码分析,有两种size的可能
+// {@link com.google.zxing.qrcode.encoder.MatrixUtil.embedPositionDetectionPatternsAndSeparators}
+ByteMatrix input = qrCode.getMatrix();
+// 因为位置探测图形的下一位必然是0,所以下面的一行可以判定选择的是哪种规格的位置判定
+int detectCornerSize = input.get(0, 5) == 1 ? 7 : 5;
+
+for (int inputY = 0, outputY = topPadding; inputY < inputHeight; inputY++, outputY += multiple) {
+ // Write the contents of this row of the barcode
+ for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) {
+ if (input.get(inputX, inputY) == 1) {
+ // 二维码的有效信息设置(即传统二维码中黑色局域的确定)
+ output.setRegion(outputX, outputY, multiple, multiple);
+ }
+
+
+ // 设置三个位置探测图形
+ if (inputX < detectCornerSize && inputY < detectCornerSize // 左上角
+ || (inputX < detectCornerSize && inputY >= inputHeight - detectCornerSize) // 左下脚
+ || (inputX >= inputWidth - detectCornerSize && inputY < detectCornerSize)) { // 右上角
+ res.setRegion(outputX, outputY, multiple, multiple);
+ }
+ }
+}
+
+```
+
+### 3. 背景图支持
+> 前面两个涉及到二维码本身的修改,接下来的背景 & logo则基本上无二维码无关,只是图片的操作而已,背景图支持,即将背景图作为图层,将二维码渲染在正中间即可
+
+对于图片的覆盖,直接借用 java.awt 包下的工具类即可实现
+
+```java
+/**
+ * 绘制背景图
+ *
+ * @param source 原图
+ * @param background 背景图
+ * @param bgW 背景图宽
+ * @param bgH 背景图高
+ * @return
+ * @throws IOException
+ */
+public static BufferedImage drawBackground(BufferedImage source, String background, int bgW, int bgH) throws IOException {
+ int sW = source.getWidth();
+ int sH = source.getHeight();
+
+
+ // 背景的图宽高不应该小于原图
+ if (bgW < sW) {
+ bgW = sW;
+ }
+
+ if (bgH < sH) {
+ bgH = sH;
+ }
+
+
+ // 获取背景图
+ BufferedImage bg = getImageByPath(background);
+ if (bg.getWidth() != bgW || bg.getHeight() != bgH) { // 需要缩放
+ BufferedImage temp = new BufferedImage(bgW, bgH, BufferedImage.TYPE_INT_ARGB);
+ temp.getGraphics().drawImage(bg.getScaledInstance(bgW, bgH, Image.SCALE_SMOOTH)
+ , 0, 0, null);
+ bg = temp;
+ }
+
+
+ // 绘制背景图
+ int x = (bgW - sW) >> 1;
+ int y = (bgH - sH) >> 1;
+ Graphics2D g2d = bg.createGraphics();
+ g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.8f)); // 透明度, 避免看不到背景
+ g2d.drawImage(source, x, y, sW, sH, null);
+ g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
+ g2d.dispose();
+ bg.flush();
+ return bg;
+}
+
+```
+
+简单说一下上面的实现逻辑
+
+- 获取背景图
+- 根据置顶的背景图大小,对原背景图进行缩放
+- 将目标图片(二维码)绘制在背景图正中间
+
+其中,我们对二维码的覆盖设置了透明度为0.8,确保不会完全覆盖背景图,导致完全看不到背景是什么,此处如有其他的需求场景可以进行可配置化处理
+
+
+### 4. logo支持
+> 其实logo的支持和背景的支持逻辑基本没什么差别,都是将一个图绘制在另一个图上
+
+具体的实现如下, 先无视logo样式的选择问题
+
+```java
+/**
+ * 在图片中间,插入圆角的logo
+ *
+ * @param qrCode 原图
+ * @param logo logo地址
+ * @param logoStyle logo 的样式 (圆角, 直角)
+ * @param logoBgColor logo的背景色
+ * @throws IOException
+ */
+public static void insertLogo(BufferedImage qrCode,
+ String logo,
+ QrCodeOptions.LogoStyle logoStyle,
+ Color logoBgColor) throws IOException {
+ int QRCODE_WIDTH = qrCode.getWidth();
+ int QRCODE_HEIGHT = qrCode.getHeight();
+
+ // 获取logo图片
+ BufferedImage bf = getImageByPath(logo);
+ int boderSize = bf.getWidth() / 15;
+ // 生成圆角边框logo
+ bf = makeRoundBorder(bf, logoStyle, boderSize, logoBgColor); // 边距为二维码图片的1/15
+
+ // logo的宽高
+ int w = bf.getWidth() > QRCODE_WIDTH * 2 / 10 ? QRCODE_WIDTH * 2 / 10 : bf.getWidth();
+ int h = bf.getHeight() > QRCODE_HEIGHT * 2 / 10 ? QRCODE_HEIGHT * 2 / 10 : bf.getHeight();
+
+ // 插入LOGO
+ Graphics2D graph = qrCode.createGraphics();
+
+ int x = (QRCODE_WIDTH - w) >> 1 ;
+ int y = (QRCODE_HEIGHT - h) >> 1;
+
+ graph.drawImage(bf, x, y, w, h, null);
+ graph.dispose();
+ bf.flush();
+}
+```
+
+上面的主要逻辑,其实没啥区别,接下来主要关心的则是圆角图形生成以及边框的支持
+
+### 5. 圆角图形
+> 生成圆角图片是一个非常常见的需求
+
+先借用`new RoundRectangle2D.Float(0, 0, w, h, cornerRadius,
+ cornerRadius)`绘制一个圆角的画布出来
+
+将原图绘制在画布上即可
+
+```java
+/**
+ * 生成圆角图片
+ *
+ * @param image 原始图片
+ * @param cornerRadius 圆角的弧度
+ * @return 返回圆角图
+ */
+public static BufferedImage makeRoundedCorner(BufferedImage image,
+ int cornerRadius) {
+ int w = image.getWidth();
+ int h = image.getHeight();
+ BufferedImage output = new BufferedImage(w, h,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g2 = output.createGraphics();
+
+ // This is what we want, but it only does hard-clipping, i.e. aliasing
+ // g2.setClip(new RoundRectangle2D ...)
+
+ // so instead fake soft-clipping by first drawing the desired clip shape
+ // in fully opaque white with antialiasing enabled...
+ g2.setComposite(AlphaComposite.Src);
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ g2.setColor(Color.WHITE);
+ g2.fill(new RoundRectangle2D.Float(0, 0, w, h, cornerRadius,
+ cornerRadius));
+
+ // ... then compositing the image on top,
+ // using the white shape from above as alpha source
+ g2.setComposite(AlphaComposite.SrcAtop);
+ g2.drawImage(image, 0, 0, null);
+
+ g2.dispose();
+
+ return output;
+}
+```
+
+### 6. 圆角边框的图片
+> 上面实现圆角图片之后,再考虑生成一个带圆角边框的图片就很简单了,直接绘制一个大一号的存色边框,然后将圆角图片绘制上去即可
+
+```java
+/**
+ *
+ * 生成圆角图片 & 圆角边框
+ *
+ * @param image 原图
+ * @param logoStyle 圆角的角度
+ * @param size 边框的边距
+ * @param color 边框的颜色
+ * @return 返回带边框的圆角图
+ */
+public static BufferedImage makeRoundBorder(BufferedImage image,
+ QrCodeOptions.LogoStyle logoStyle,
+ int size, Color color) {
+ // 将图片变成圆角
+ int cornerRadius = 0;
+ if (logoStyle == QrCodeOptions.LogoStyle.ROUND) {
+ cornerRadius = image.getWidth() / 4;
+ image = makeRoundedCorner(image, cornerRadius);
+ }
+
+ int w = image.getWidth() + size;
+ int h = image.getHeight() + size;
+ BufferedImage output = new BufferedImage(w, h,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g2 = output.createGraphics();
+ g2.setComposite(AlphaComposite.Src);
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ g2.setColor(color == null ? Color.WHITE : color);
+ g2.fill(new RoundRectangle2D.Float(0, 0, w, h, cornerRadius,
+ cornerRadius));
+
+ // ... then compositing the image on top,
+ // using the white shape from above as alpha source
+// g2.setComposite(AlphaComposite.SrcAtop);
+ g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 1.0f));
+ g2.drawImage(image, size / 2, size / 2, null);
+ g2.dispose();
+
+ return output;
+}
+```
+
+
+## 测试
+
+上面分别对每一个点进行了实现并加以简单说明,最后就是需要将上面的都串起来进行测试了,因为我们的工程是在前面已经搭建好的二维码服务上进行的,所以测试代码也比较简单,如下
+
+```java
+@Test
+public void testGenColorCode() {
+ String msg = "https://my.oschina.net/u/566591/blog/1359432";
+ // 根据本地文件生成待logo的二维码, 重新着色位置探测图像
+ try {
+ String logo = "logo.jpg";
+ String bg = "bg.png";
+ BufferedImage img = QrCodeGenWrapper.of(msg)
+ .setW(300)
+ .setPreColor(0xff0000ff)
+ .setBgColor(0xffFFFF00)
+ .setDetectCornerPreColor(0xffff0000)
+ .setPadding(2)
+ .setLogo(logo)
+ .setLogoStyle(QrCodeOptions.LogoStyle.ROUND)
+ .setLogoBgColor(0xff00cc00)
+ .setBackground(bg)
+ .asBufferedImage();
+
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ ImageIO.write(img, "png", outputStream);
+ System.out.println(Base64Util.encode(outputStream));
+ } catch (Exception e) {
+ System.out.println("create qrcode error! e: " + e);
+ Assert.assertTrue(false);
+ }
+}
+```
+
+测试执行示意图
+
+![http://s2.mogucdn.com/mlcdn/c45406/170728_2lebbba9b47037cc0g03hd42hf6ga_1224x639.gif](http://s2.mogucdn.com/mlcdn/c45406/170728_2lebbba9b47037cc0g03hd42hf6ga_1224x639.gif)
+
+---
+## 其他
+
+项目源码: [https://github.com/liuyueyi/quick-media](https://github.com/liuyueyi/quick-media)
+
+相关博文:
+
+- [zxing 二维码大白边一步一步修复指南](https://my.oschina.net/u/566591/blog/872770)
+- [spring-boot & zxing 搭建二维码服务](https://my.oschina.net/u/566591/blog/1457164)
+
+
+个人博客:[一灰的个人博客](http://zbang.online:8080/articles/2017/07/18/1500369136069.html)
+
+公众号获取更多:
+
+![https://static.oschina.net/uploads/img/201707/09205944_0PzS.jpg](https://static.oschina.net/uploads/img/201707/09205944_0PzS.jpg)
\ No newline at end of file
diff --git "a/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\275\277\347\224\250zxing\346\217\220\344\276\233\344\272\214\347\273\264\347\240\201\347\224\237\346\210\220\350\247\243\346\236\220\346\234\215\345\212\241.md" "b/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\275\277\347\224\250zxing\346\217\220\344\276\233\344\272\214\347\273\264\347\240\201\347\224\237\346\210\220\350\247\243\346\236\220\346\234\215\345\212\241.md"
new file mode 100644
index 00000000..af7e0692
--- /dev/null
+++ "b/docs/\346\217\222\344\273\266/\344\272\214\347\273\264\347\240\201/\344\275\277\347\224\250zxing\346\217\220\344\276\233\344\272\214\347\273\264\347\240\201\347\224\237\346\210\220\350\247\243\346\236\220\346\234\215\345\212\241.md"
@@ -0,0 +1,846 @@
+# 使用zxing提供二维码生成解析服务
+
+> 搭建一个二维码的生成 & 解析服务, 使用java web对外提供http调用,返回base64格式的二维码图片
+
+## 1. 背景&准备
+> 二维码生成场景实在是太多了,背景都没啥好说的...
+
+### 采用的技术
+
+- zxing : 实现二维码的生成 & 解析
+- spring-boot: 提供http服务接口
+- jdk base64 : 对图片进行base64编码返回
+- awt : 插入logo
+
+### 测试case
+
+二维码生成除了传入基本的内容之外,有很多可以配置的参数,比如背景色,前置色,大小,logo,边框...,显然这种多参数配置的情况,我们会采用Builder设计模式来处理,可以看下最终的测试代码如下
+
+```java
+/**
+ * 测试二维码
+ */
+@Test
+public void testGenQrCode() {
+ String msg = "https://my.oschina.net/u/566591/blog/1359432";
+
+ try {
+
+ boolean ans = QrCodeGenWrapper.of(msg).asFile("src/test/qrcode/gen.png");
+ System.out.println(ans);
+ } catch (Exception e) {
+ System.out.println("create qrcode error! e: " + e);
+ Assert.assertTrue(false);
+ }
+
+
+ //生成红色的二维码 300x300, 无边框
+ try {
+ boolean ans = QrCodeGenWrapper.of(msg)
+ .setW(300)
+ .setPreColor(0xffff0000)
+ .setBgColor(0xffffffff)
+ .setPadding(0)
+ .asFile("src/test/qrcode/gen_300x300.png");
+ System.out.println(ans);
+ } catch (Exception e) {
+ System.out.println("create qrcode error! e: " + e);
+ Assert.assertTrue(false);
+ }
+
+
+ // 生成带logo的二维码
+ try {
+ String logo = "https://static.oschina.net/uploads/user/283/566591_100.jpeg";
+ boolean ans = QrCodeGenWrapper.of(msg)
+ .setW(300)
+ .setPreColor(0xffff0000)
+ .setBgColor(0xffffffff)
+ .setPadding(0)
+ .setLogo(logo)
+ .setLogoStyle(QrCodeOptions.LogoStyle.ROUND)
+ .asFile("src/test/qrcode/gen_300x300_logo.png");
+ System.out.println(ans);
+ } catch (Exception e) {
+ System.out.println("create qrcode error! e: " + e);
+ Assert.assertTrue(false);
+ }
+
+
+ // 根据本地文件生成待logo的二维码
+ try {
+ String logo = "logo.jpg";
+ boolean ans = QrCodeGenWrapper.of(msg)
+ .setW(300)
+ .setPreColor(0xffff0000)
+ .setBgColor(0xffffffff)
+ .setPadding(0)
+ .setLogo(logo)
+ .asFile("src/test/qrcode/gen_300x300_logo_v2.png");
+ System.out.println(ans);
+ } catch (Exception e) {
+ System.out.println("create qrcode error! e: " + e);
+ Assert.assertTrue(false);
+ }
+}
+```
+
+## 2. 设计与实现
+
+### 1. 配置参数: `QrCodeOptions`
+
+根据最常用的规则,目前提供以下可选的配置项
+
+- 输入内容
+- logo
+- logo的样式
+- 宽高
+- 前置色,背景色
+- 输出图片格式
+- 内容编码
+
+```java
+@Data
+public class QrCodeOptions {
+ /**
+ * 塞入二维码的信息
+ */
+ private String msg;
+
+
+ /**
+ * 二维码中间的logo
+ */
+ private String logo;
+
+
+ /**
+ * logo的样式, 目前支持圆角+普通
+ */
+ private LogoStyle logoStyle;
+
+
+ /**
+ * 生成二维码的宽
+ */
+ private Integer w;
+
+
+ /**
+ * 生成二维码的高
+ */
+ private Integer h;
+
+
+ /**
+ * 生成二维码的颜色
+ */
+ private MatrixToImageConfig matrixToImageConfig;
+
+
+ private Map hints;
+
+
+ /**
+ * 生成二维码图片的格式 png, jpg
+ */
+ private String picType;
+
+
+ public enum LogoStyle {
+ ROUND,
+ NORMAL;
+ }
+}
+```
+
+从上面的配置来看,有较多其实是与zxing进行打交道的,直接对使用者而言,有点不太友好,下面可以看下我们的包装类
+
+### 2. 包装类: `QrCodeGenWrapper`
+> 对外提供二维码生成的主要入口,从我们的设计来看,通过`of(content)` 来创建一个builder对象,并设置二维码的内容,然后可以设置builder中的参数,来选择最终的二维码配置规则
+
+
+提供三中输出方式:
+
+- BufferImage 对象 : 适用于对二维码进行再次处理的场景
+- 二维码图片文件 : 适用于本地生成
+- base64编码的二维码字符串 : 适用于网络接口调用
+
+
+下面的实现比较简单,唯一需要注意的就是组装 `QrCodeOptions` 参数的默认值问题
+
+```java
+public class QrCodeGenWrapper {
+ public static Builder of(String content) {
+ return new Builder().setMsg(content);
+ }
+
+
+ private static BufferedImage asBufferedImage(QrCodeOptions qrCodeConfig) throws WriterException, IOException {
+ BitMatrix bitMatrix = QrCodeUtil.encode(qrCodeConfig);
+ return QrCodeUtil.toBufferedImage(qrCodeConfig, bitMatrix);
+ }
+
+ private static String asString(QrCodeOptions qrCodeOptions) throws WriterException, IOException {
+ BufferedImage bufferedImage = asBufferedImage(qrCodeOptions);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ ImageIO.write(bufferedImage, qrCodeOptions.getPicType(), outputStream);
+ return Base64Util.encode(outputStream);
+ }
+
+ private static boolean asFile(QrCodeOptions qrCodeConfig, String absFileName) throws WriterException, IOException {
+ File file = new File(absFileName);
+ FileUtil.mkDir(file);
+
+ BufferedImage bufferedImage = asBufferedImage(qrCodeConfig);
+ if (!ImageIO.write(bufferedImage, qrCodeConfig.getPicType(), file)) {
+ throw new IOException("save qrcode image error!");
+ }
+
+ return true;
+ }
+
+
+ @ToString
+ public static class Builder {
+ private static final MatrixToImageConfig DEFAULT_CONFIG = new MatrixToImageConfig();
+
+ /**
+ * The message to put into QrCode
+ */
+ private String msg;
+
+
+ /**
+ * qrcode center logo
+ */
+ private String logo;
+
+
+ /**
+ * logo的样式
+ */
+ private QrCodeOptions.LogoStyle logoStyle = QrCodeOptions.LogoStyle.NORMAL;
+
+
+ /**
+ * qrcode image width
+ */
+ private Integer w;
+
+
+ /**
+ * qrcode image height
+ */
+ private Integer h;
+
+
+ /**
+ * qrcode bgcolor, default white
+ */
+ private Integer bgColor;
+
+
+ /**
+ * qrcode msg color, default black
+ */
+ private Integer preColor;
+
+
+ /**
+ * qrcode message's code, default UTF-8
+ */
+ private String code = "utf-8";
+
+
+ /**
+ * 0 - 4
+ */
+ private Integer padding;
+
+
+ /**
+ * error level, default H
+ */
+ private ErrorCorrectionLevel errorCorrection = ErrorCorrectionLevel.H;
+
+
+ /**
+ * output qrcode image type, default png
+ */
+ private String picType = "png";
+
+
+ public String getMsg() {
+ return msg;
+ }
+
+ public Builder setMsg(String msg) {
+ this.msg = msg;
+ return this;
+ }
+
+ public Builder setLogo(String logo) {
+ this.logo = logo;
+ return this;
+ }
+
+
+ public Builder setLogoStyle(QrCodeOptions.LogoStyle logoStyle) {
+ this.logoStyle = logoStyle;
+ return this;
+ }
+
+
+ public Integer getW() {
+ return w == null ? (h == null ? 200 : h) : w;
+ }
+
+ public Builder setW(Integer w) {
+ if (w != null && w <= 0) {
+ throw new IllegalArgumentException("生成二维码的宽必须大于0");
+ }
+ this.w = w;
+ return this;
+ }
+
+ public Integer getH() {
+ return h == null ? (w == null ? 200 : w) : h;
+ }
+
+ public Builder setH(Integer h) {
+ if (h != null && h <= 0) {
+ throw new IllegalArgumentException("生成功能二维码的搞必须大于0");
+ }
+ this.h = h;
+ return this;
+ }
+
+ public Integer getBgColor() {
+ return bgColor == null ? MatrixToImageConfig.WHITE : bgColor;
+ }
+
+ public Builder setBgColor(Integer bgColor) {
+ this.bgColor = bgColor;
+ return this;
+ }
+
+ public Integer getPreColor() {
+ return preColor == null ? MatrixToImageConfig.BLACK : preColor;
+ }
+
+ public Builder setPreColor(Integer preColor) {
+ this.preColor = preColor;
+ return this;
+ }
+
+ public Builder setCode(String code) {
+ this.code = code;
+ return this;
+ }
+
+ public Integer getPadding() {
+ if (padding == null) {
+ return 1;
+ }
+
+ if (padding < 0) {
+ return 0;
+ }
+
+ if (padding > 4) {
+ return 4;
+ }
+
+ return padding;
+ }
+
+ public Builder setPadding(Integer padding) {
+ this.padding = padding;
+ return this;
+ }
+
+ public Builder setPicType(String picType) {
+ this.picType = picType;
+ return this;
+ }
+
+ public void setErrorCorrection(ErrorCorrectionLevel errorCorrection) {
+ this.errorCorrection = errorCorrection;
+ }
+
+ private void validate() {
+ if (msg == null || msg.length() == 0) {
+ throw new IllegalArgumentException("生成二维码的内容不能为空!");
+ }
+ }
+
+
+ private QrCodeOptions build() {
+ this.validate();
+
+ QrCodeOptions qrCodeConfig = new QrCodeOptions();
+ qrCodeConfig.setMsg(getMsg());
+ qrCodeConfig.setH(getH());
+ qrCodeConfig.setW(getW());
+ qrCodeConfig.setLogo(logo);
+ qrCodeConfig.setLogoStyle(logoStyle);
+ qrCodeConfig.setPicType(picType);
+
+ Map hints = new HashMap<>(3);
+ hints.put(EncodeHintType.ERROR_CORRECTION, errorCorrection);
+ hints.put(EncodeHintType.CHARACTER_SET, code);
+ hints.put(EncodeHintType.MARGIN, this.getPadding());
+ qrCodeConfig.setHints(hints);
+
+
+ MatrixToImageConfig config;
+ if (getPreColor() == MatrixToImageConfig.BLACK
+ && getBgColor() == MatrixToImageConfig.WHITE) {
+ config = DEFAULT_CONFIG;
+ } else {
+ config = new MatrixToImageConfig(getPreColor(), getBgColor());
+ }
+ qrCodeConfig.setMatrixToImageConfig(config);
+
+
+ return qrCodeConfig;
+ }
+
+
+ public String asString() throws IOException, WriterException {
+ return QrCodeGenWrapper.asString(build());
+ }
+
+
+ public BufferedImage asBufferedImage() throws IOException, WriterException {
+ return QrCodeGenWrapper.asBufferedImage(build());
+ }
+
+
+ public boolean asFile(String absFileName) throws IOException, WriterException {
+ return QrCodeGenWrapper.asFile(build(), absFileName);
+ }
+ }
+}
+```
+
+### 二维码生成工具类 : `QrCodeUtil`
+
+下面这个工具类看着比较复杂,其实大部分代码是从 `com.google.zxing.qrcode.QRCodeWriter#encode(String, BarcodeFormat, int, int, Map)` 抠出来的
+
+主要是为了解决二维码的白边问题,关于这个大白边问题,可以参看我之前的一篇博文 [《zxing 二维码大白边一步一步修复指南》](https://my.oschina.net/u/566591/blog/872770)
+
+
+```java
+@Slf4j
+public class QrCodeUtil {
+
+ private static final int QUIET_ZONE_SIZE = 4;
+
+
+ /**
+ * 对 zxing 的 QRCodeWriter 进行扩展, 解决白边过多的问题
+ *
+ * 源码参考 {@link com.google.zxing.qrcode.QRCodeWriter#encode(String, BarcodeFormat, int, int, Map)}
+ */
+ public static BitMatrix encode(QrCodeOptions qrCodeConfig) throws WriterException {
+ ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.L;
+ int quietZone = 1;
+ if (qrCodeConfig.getHints() != null) {
+ if (qrCodeConfig.getHints().containsKey(EncodeHintType.ERROR_CORRECTION)) {
+ errorCorrectionLevel = ErrorCorrectionLevel.valueOf(qrCodeConfig.getHints().get(EncodeHintType.ERROR_CORRECTION).toString());
+ }
+ if (qrCodeConfig.getHints().containsKey(EncodeHintType.MARGIN)) {
+ quietZone = Integer.parseInt(qrCodeConfig.getHints().get(EncodeHintType.MARGIN).toString());
+ }
+
+ if (quietZone > QUIET_ZONE_SIZE) {
+ quietZone = QUIET_ZONE_SIZE;
+ } else if (quietZone < 0) {
+ quietZone = 0;
+ }
+ }
+
+ QRCode code = Encoder.encode(qrCodeConfig.getMsg(), errorCorrectionLevel, qrCodeConfig.getHints());
+ return renderResult(code, qrCodeConfig.getW(), qrCodeConfig.getH(), quietZone);
+ }
+
+
+ /**
+ * 对 zxing 的 QRCodeWriter 进行扩展, 解决白边过多的问题
+ *
+ * 源码参考 {@link com.google.zxing.qrcode.QRCodeWriter#renderResult(QRCode, int, int, int)}
+ *
+ * @param code
+ * @param width
+ * @param height
+ * @param quietZone 取值 [0, 4]
+ * @return
+ */
+ private static BitMatrix renderResult(QRCode code, int width, int height, int quietZone) {
+ ByteMatrix input = code.getMatrix();
+ if (input == null) {
+ throw new IllegalStateException();
+ }
+
+ // xxx 二维码宽高相等, 即 qrWidth == qrHeight
+ int inputWidth = input.getWidth();
+ int inputHeight = input.getHeight();
+ int qrWidth = inputWidth + (quietZone * 2);
+ int qrHeight = inputHeight + (quietZone * 2);
+
+
+ // 白边过多时, 缩放
+ int minSize = Math.min(width, height);
+ int scale = calculateScale(qrWidth, minSize);
+ if (scale > 0) {
+ if (log.isDebugEnabled()) {
+ log.debug("qrCode scale enable! scale: {}, qrSize:{}, expectSize:{}x{}", scale, qrWidth, width, height);
+ }
+
+ int padding, tmpValue;
+ // 计算边框留白
+ padding = (minSize - qrWidth * scale) / QUIET_ZONE_SIZE * quietZone;
+ tmpValue = qrWidth * scale + padding;
+ if (width == height) {
+ width = tmpValue;
+ height = tmpValue;
+ } else if (width > height) {
+ width = width * tmpValue / height;
+ height = tmpValue;
+ } else {
+ height = height * tmpValue / width;
+ width = tmpValue;
+ }
+ }
+
+ int outputWidth = Math.max(width, qrWidth);
+ int outputHeight = Math.max(height, qrHeight);
+
+ int multiple = Math.min(outputWidth / qrWidth, outputHeight / qrHeight);
+ int leftPadding = (outputWidth - (inputWidth * multiple)) / 2;
+ int topPadding = (outputHeight - (inputHeight * multiple)) / 2;
+
+ BitMatrix output = new BitMatrix(outputWidth, outputHeight);
+
+ for (int inputY = 0, outputY = topPadding; inputY < inputHeight; inputY++, outputY += multiple) {
+ // Write the contents of this row of the barcode
+ for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) {
+ if (input.get(inputX, inputY) == 1) {
+ output.setRegion(outputX, outputY, multiple, multiple);
+ }
+ }
+ }
+
+ return output;
+ }
+
+
+ /**
+ * 如果留白超过15% , 则需要缩放
+ * (15% 可以根据实际需要进行修改)
+ *
+ * @param qrCodeSize 二维码大小
+ * @param expectSize 期望输出大小
+ * @return 返回缩放比例, <= 0 则表示不缩放, 否则指定缩放参数
+ */
+ private static int calculateScale(int qrCodeSize, int expectSize) {
+ if (qrCodeSize >= expectSize) {
+ return 0;
+ }
+
+ int scale = expectSize / qrCodeSize;
+ int abs = expectSize - scale * qrCodeSize;
+ if (abs < expectSize * 0.15) {
+ return 0;
+ }
+
+ return scale;
+ }
+
+
+
+ /**
+ * 根据二维码配置 & 二维码矩阵生成二维码图片
+ *
+ * @param qrCodeConfig
+ * @param bitMatrix
+ * @return
+ * @throws IOException
+ */
+ public static BufferedImage toBufferedImage(QrCodeOptions qrCodeConfig, BitMatrix bitMatrix) throws IOException {
+ int qrCodeWidth = bitMatrix.getWidth();
+ int qrCodeHeight = bitMatrix.getHeight();
+ BufferedImage qrCode = new BufferedImage(qrCodeWidth, qrCodeHeight, BufferedImage.TYPE_INT_RGB);
+
+ for (int x = 0; x < qrCodeWidth; x++) {
+ for (int y = 0; y < qrCodeHeight; y++) {
+ qrCode.setRGB(x, y,
+ bitMatrix.get(x, y) ?
+ qrCodeConfig.getMatrixToImageConfig().getPixelOnColor() :
+ qrCodeConfig.getMatrixToImageConfig().getPixelOffColor());
+ }
+ }
+
+ // 插入logo
+ if (!(qrCodeConfig.getLogo() == null || "".equals(qrCodeConfig.getLogo()))) {
+ ImageUtil.insertLogo(qrCode, qrCodeConfig.getLogo(), qrCodeConfig.getLogoStyle());
+ }
+
+ // 若二维码的实际宽高和预期的宽高不一致, 则缩放
+ int realQrCodeWidth = qrCodeConfig.getW();
+ int realQrCodeHeight = qrCodeConfig.getH();
+ if (qrCodeWidth != realQrCodeWidth || qrCodeHeight != realQrCodeHeight) {
+ BufferedImage tmp = new BufferedImage(realQrCodeWidth, realQrCodeHeight, BufferedImage.TYPE_INT_RGB);
+ tmp.getGraphics().drawImage(
+ qrCode.getScaledInstance(realQrCodeWidth, realQrCodeHeight,
+ Image.SCALE_SMOOTH), 0, 0, null);
+ qrCode = tmp;
+ }
+
+ return qrCode;
+ }
+
+}
+```
+
+
+### 4. logo的插入辅助类: `ImageUtil`
+
+zxing本身是不支持生成待logo的二维码的,这里我们借用awt对将logo绘制在生成的二维码图片上
+
+这里提供了圆角图片生成,边框生成,插入logo三个功能
+
+涉及到绘图的逻辑,也没啥可说的,基本上的套路都一样
+
+```java
+public class ImageUtil {
+
+ /**
+ * 在图片中间,插入圆角的logo
+ *
+ * @param qrCode 原图
+ * @param logo logo地址
+ * @throws IOException
+ */
+ public static void insertLogo(BufferedImage qrCode, String logo, QrCodeOptions.LogoStyle logoStyle) throws IOException {
+ int QRCODE_WIDTH = qrCode.getWidth();
+ int QRCODE_HEIGHT = qrCode.getHeight();
+
+ // 获取logo图片
+ BufferedImage bf = getImageByPath(logo);
+ int size = bf.getWidth() > QRCODE_WIDTH * 2 / 10 ? QRCODE_WIDTH * 2 / 50 : bf.getWidth() / 5;
+ bf = ImageUtil.makeRoundBorder(bf, logoStyle, size, Color.BLUE); // 边距为二维码图片的1/10
+
+ // logo的宽高
+ int w = bf.getWidth() > QRCODE_WIDTH * 2 / 10 ? QRCODE_WIDTH * 2 / 10 : bf.getWidth();
+ int h = bf.getHeight() > QRCODE_HEIGHT * 2 / 10 ? QRCODE_HEIGHT * 2 / 10 : bf.getHeight();
+
+ // 插入LOGO
+ Graphics2D graph = qrCode.createGraphics();
+
+ int x = (QRCODE_WIDTH - w) / 2;
+ int y = (QRCODE_HEIGHT - h) / 2;
+
+ graph.drawImage(bf, x, y, w, h, null);
+ graph.dispose();
+ bf.flush();
+ }
+
+
+ /**
+ * 根据路径获取图片
+ *
+ * @param path 本地路径 or 网络地址
+ * @return 图片
+ * @throws IOException
+ */
+ public static BufferedImage getImageByPath(String path) throws IOException {
+ if (path.startsWith("http")) { // 从网络获取logo
+// return ImageIO.read(new URL(path));
+ return ImageIO.read(HttpUtil.downFile(path));
+ } else if (path.startsWith("/")) { // 绝对地址获取logo
+ return ImageIO.read(new File(path));
+ } else { // 从资源目录下获取logo
+ return ImageIO.read(ImageUtil.class.getClassLoader().getResourceAsStream(path));
+ }
+ }
+
+
+ /**
+ * fixme 边框的计算需要根据最终生成logo图片的大小来定义,这样才不会出现不同的logo原图,导致边框不一致的问题
+ *
+ * 生成圆角图片 & 圆角边框
+ *
+ * @param image 原图
+ * @param logoStyle 圆角的角度
+ * @param size 边框的边距
+ * @param color 边框的颜色
+ * @return 返回带边框的圆角图
+ */
+ public static BufferedImage makeRoundBorder(BufferedImage image, QrCodeOptions.LogoStyle logoStyle, int size, Color color) {
+ // 将图片变成圆角
+ int cornerRadius = 0;
+ if (logoStyle == QrCodeOptions.LogoStyle.ROUND) {
+ cornerRadius = 30;
+ image = makeRoundedCorner(image, cornerRadius);
+ }
+
+ int borderSize = size;
+ int w = image.getWidth() + borderSize;
+ int h = image.getHeight() + borderSize;
+ BufferedImage output = new BufferedImage(w, h,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g2 = output.createGraphics();
+ g2.setComposite(AlphaComposite.Src);
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ g2.setColor(color == null ? Color.WHITE : color);
+ g2.fill(new RoundRectangle2D.Float(0, 0, w, h, cornerRadius,
+ cornerRadius));
+
+ // ... then compositing the image on top,
+ // using the white shape from above as alpha source
+ g2.setComposite(AlphaComposite.SrcAtop);
+ g2.drawImage(image, size, size, null);
+ g2.dispose();
+
+ return output;
+ }
+
+
+ /**
+ * 生成圆角图片
+ *
+ * @param image 原始图片
+ * @param cornerRadius 圆角的弧度
+ * @return 返回圆角图
+ */
+ public static BufferedImage makeRoundedCorner(BufferedImage image,
+ int cornerRadius) {
+ int w = image.getWidth();
+ int h = image.getHeight();
+ BufferedImage output = new BufferedImage(w, h,
+ BufferedImage.TYPE_INT_ARGB);
+
+ Graphics2D g2 = output.createGraphics();
+
+ // This is what we want, but it only does hard-clipping, i.e. aliasing
+ // g2.setClip(new RoundRectangle2D ...)
+
+ // so instead fake soft-clipping by first drawing the desired clip shape
+ // in fully opaque white with antialiasing enabled...
+ g2.setComposite(AlphaComposite.Src);
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ g2.setColor(Color.WHITE);
+ g2.fill(new RoundRectangle2D.Float(0, 0, w, h, cornerRadius,
+ cornerRadius));
+
+ // ... then compositing the image on top,
+ // using the white shape from above as alpha source
+ g2.setComposite(AlphaComposite.SrcAtop);
+ g2.drawImage(image, 0, 0, null);
+
+ g2.dispose();
+
+ return output;
+ }
+}
+```
+
+
+### 5. base64编码工具: `Base64Util`
+
+```java
+public class Base64Util {
+ public static String encode(ByteArrayOutputStream outputStream) {
+ return Base64.getEncoder().encodeToString(outputStream.toByteArray());
+ }
+}
+```
+
+### 6. 二维码解析工具: `QrCodeDeWrapper`
+
+```java
+public class QrCodeDeWrapper {
+
+
+ /**
+ * 读取二维码中的内容, 并返回
+ *
+ * @param qrcodeImg 二维码图片的地址
+ * @return 返回二维码的内容
+ * @throws IOException 读取二维码失败
+ * @throws FormatException 二维码解析失败
+ * @throws ChecksumException
+ * @throws NotFoundException
+ */
+ public static String decode(String qrcodeImg) throws IOException, FormatException, ChecksumException, NotFoundException {
+ BufferedImage image = ImageUtil.getImageByPath(qrcodeImg);
+ return decode(image);
+ }
+
+
+ public static String decode(BufferedImage image) throws FormatException, ChecksumException, NotFoundException {
+ if (image == null) {
+ throw new IllegalStateException("can not load qrCode!");
+ }
+
+
+ LuminanceSource source = new BufferedImageLuminanceSource(image);
+ BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
+ QRCodeReader qrCodeReader = new QRCodeReader();
+ Result result = qrCodeReader.decode(bitmap);
+ return result.getText();
+ }
+
+}
+```
+
+## 3. 填坑
+
+### 1. 生成二维码边框过大的问题
+
+即便指定了生成二维码图片的边距为0,但是最终生成的二维码图片边框依然可能很大
+
+如下图
+
+![http://git.oschina.net/uploads/images/2017/0403/120101_e6d40bcb_2334.jpeg](http://git.oschina.net/uploads/images/2017/0403/120101_e6d40bcb_2334.jpeg)
+
+这个问题上面已经修复,产生的原因和修复过程可以查看 [zxing 二维码大白边一步一步修复指南](https://my.oschina.net/u/566591/blog/872770)
+
+修复之后如下图
+
+![http://git.oschina.net/uploads/images/2017/0403/120811_9014928b_2334.jpeg](http://git.oschina.net/uploads/images/2017/0403/120811_9014928b_2334.jpeg)
+
+
+### 2. 插入logo
+
+上面虽然实现了插入logo的逻辑,但是生成的边框处有点问题,坑还没填
+
+希望是指定边框大小时,不管logo图片有多大,最终的边框一样大小,而上面却有点问题...
+
+
+此外就是生成的logo样式不美观,不能忍啊
+
+
+## 4. 演示
+
+启动spring-boot, 然后开启测试
+
+![演示图片](https://raw.githubusercontent.com/liuyueyi/quick-media/master/doc/img/qrcode/qr.gif)
+
+## 5. 其他
+
+
+项目源码: [https://github.com/liuyueyi/quick-media](https://github.com/liuyueyi/quick-media)
+
+
+个人博客:[一灰的个人博客](http://zbang.online:8080/articles/2017/07/18/1500369136069.html)
+
+公众号获取更多:
+
+![https://static.oschina.net/uploads/img/201707/09205944_0PzS.jpg](https://static.oschina.net/uploads/img/201707/09205944_0PzS.jpg)
\ No newline at end of file
diff --git "a/docs/\351\207\207\345\235\221/Batik\346\270\262\346\237\223png\345\233\276\347\211\207\345\274\202\345\270\270\347\232\204bug\344\277\256\345\244\215.md" "b/docs/\351\207\207\345\235\221/Batik\346\270\262\346\237\223png\345\233\276\347\211\207\345\274\202\345\270\270\347\232\204bug\344\277\256\345\244\215.md"
new file mode 100644
index 00000000..93bf98b7
--- /dev/null
+++ "b/docs/\351\207\207\345\235\221/Batik\346\270\262\346\237\223png\345\233\276\347\211\207\345\274\202\345\270\270\347\232\204bug\344\277\256\345\244\215.md"
@@ -0,0 +1,240 @@
+# Batik渲染png图片异常的bug修复
+
+batik是apache的一个开源项目,可以实现svg的渲染,后端借助它可以比较简单的实现图片渲染,当然和java一贯处理图片不太方便一样,使用起来也有不少坑
+
+下面记录一个bug的修复过程
+
+
+
+## 3.1. 问题重现
+
+svg文件:
+
+```xml
+
+```
+
+依次测试了三个图片,两个png,一个jpg,很不幸第一个png会抛异常
+
+
+输出的堆栈信息如
+
+```sh
+The URI "http://image.uc.cn/o/wemedia/s/upload/2017/39c53604fe3587a4876396cf3785b801x200x200x13.png"
+on element can't be opened because:
+PNG URL is corrupt or unsupported variant
+ at org.apache.batik.bridge.UserAgentAdapter.getBrokenLinkDocument(UserAgentAdapter.java:448)
+ at org.apache.batik.bridge.SVGImageElementBridge.createRasterImageNode(SVGImageElementBridge.java:642)
+ at org.apache.batik.bridge.SVGImageElementBridge.createImageGraphicsNode(SVGImageElementBridge.java:340)
+ at org.apache.batik.bridge.SVGImageElementBridge.buildImageGraphicsNode(SVGImageElementBridge.java:180)
+ at org.apache.batik.bridge.SVGImageElementBridge.createGraphicsNode(SVGImageElementBridge.java:122)
+ at org.apache.batik.bridge.GVTBuilder.buildGraphicsNode(GVTBuilder.java:213)
+ at org.apache.batik.bridge.GVTBuilder.buildComposite(GVTBuilder.java:171)
+ at org.apache.batik.bridge.GVTBuilder.build(GVTBuilder.java:82)
+ at org.apache.batik.transcoder.SVGAbstractTranscoder.transcode(SVGAbstractTranscoder.java:208)
+ at org.apache.batik.transcoder.image.ImageTranscoder.transcode(ImageTranscoder.java:92)
+ at org.apache.batik.transcoder.XMLAbstractTranscoder.transcode(XMLAbstractTranscoder.java:142)
+ at org.apache.batik.transcoder.SVGAbstractTranscoder.transcode(SVGAbstractTranscoder.java:156)
+ ...
+```
+
+---
+
+## 3.2. 问题定位及分析
+
+既然出现了这个问题,那么就要去修复解决了,当然遇到这么鬼畜的问题,最常见的几个步骤:
+
+1. 其他人遇到过么 (问百度) -- 结果度娘没有给出任何有效的建议,也没有搜到任何有用的信息
+2. 然后问谷歌,靠谱了一点,至少有些相关的主题了,但建设性的意见也没收到
+3. 外援实在找不到,只能debug查问题了
+
+
+### 1. DEBUG的一路
+
+通过上面的堆栈信息,可以想见,debug的几个地方也和明确了,首先定位到下面这一行
+
+```sh
+at org.apache.batik.bridge.UserAgentAdapter.getBrokenLinkDocument(UserAgentAdapter.java:448)
+```
+
+为什么这么干?因为首先得确认下这个异常是怎么抛出来的,逆向推,直接看源码,发现直接抛出异常
+
+![2A02AB38-25ED-4B71-8CE7-76460623FE08.png](https://s17.mogucdn.com/mlcdn/c45406/180119_67ik6b6l9kb199gjaachbkdce89dk_1274x284.jpg)
+
+再往上走
+
+```sh
+at org.apache.batik.bridge.SVGImageElementBridge.createRasterImageNode(SVGImageElementBridge.java:642)
+```
+
+![D1CECFCF-D940-4A17-87F9-7E12B00517D9.png](https://s17.mogucdn.com/mlcdn/c45406/180119_61el9dg4f32iafif6i57g0hac8e7i_1328x832.jpg)
+
+所以说因为这个if条件判断成立,导致进入了这个异常逻辑,判断的逻辑也没啥好说的,现在的关键是这个参数对象img是怎么来的
+
+```sh
+at org.apache.batik.bridge.SVGImageElementBridge.createImageGraphicsNode(SVGImageElementBridge.java:340)
+```
+
+![IMAGE](https://s17.mogucdn.com/mlcdn/c45406/180119_645jcjh7hae6g6e7k3bakhje593a7_1320x508.jpg)
+
+然后就稍微清晰一点了,直接将火力放在下面的方法中
+
+```java
+org.apache.batik.ext.awt.image.spi.ImageTagRegistry#readURL(java.io.InputStream,
+ org.apache.batik.util.ParsedURL,
+ org.apache.xmlgraphics.java2d.color.ICCColorSpaceWithIntent,
+ boolean,
+ boolean)
+```
+
+在这个方法内部,也没什么好说的,单步多调几次,就能发现异常的case是怎么来的了,省略掉中间各种单步debug的过程,下面直接进入关键链路
+
+### 2. 火力全开,问题定位
+
+```java
+org.apache.batik.ext.awt.image.codec.imageio.AbstractImageIORegistryEntry
+```
+
+通过上面的一路之后,发现最终的关键就是上面这个抽象类,顺带也可以看下这个抽象类的几个子类,有JPEGxxx, PNGxxx, TIFFxxx,然后问题来了,都已经有相关实现了,所以png讲道理应该是会支持的才对吧,但和实际的表现太不一样了吧,所以有必要撸一把源码了
+
+
+```java
+public Filter handleStream(InputStream inIS,
+ ParsedURL origURL,
+ boolean needRawData) {
+ final DeferRable dr = new DeferRable();
+ final InputStream is = inIS;
+ final String errCode;
+ final Object [] errParam;
+ if (origURL != null) {
+ errCode = ERR_URL_FORMAT_UNREADABLE;
+ errParam = new Object[] {getFormatName(), origURL};
+ } else {
+ errCode = ERR_STREAM_FORMAT_UNREADABLE;
+ errParam = new Object[] {getFormatName()};
+ }
+
+ Thread t = new Thread() {
+ @Override
+ public void run() {
+ Filter filt;
+ try{
+ Iterator iter = ImageIO.getImageReadersByMIMEType(
+ getMimeTypes().get(0).toString());
+ if (!iter.hasNext()) {
+ throw new UnsupportedOperationException(
+ "No image reader for "
+ + getFormatName() + " available!");
+ }
+ ImageReader reader = iter.next();
+ ImageInputStream imageIn = ImageIO.createImageInputStream(is);
+ reader.setInput(imageIn, true);
+
+ int imageIndex = 0;
+ dr.setBounds(new Rectangle2D.Double
+ (0, 0,
+ reader.getWidth(imageIndex),
+ reader.getHeight(imageIndex)));
+ CachableRed cr;
+ //Naive approach possibly wasting lots of memory
+ //and ignoring the gamma correction done by PNGRed :-(
+ //Matches the code used by the former JPEGRegistryEntry, though.
+ BufferedImage bi = reader.read(imageIndex);
+ cr = GraphicsUtil.wrap(bi);
+ cr = new Any2sRGBRed(cr);
+ cr = new FormatRed(cr, GraphicsUtil.sRGB_Unpre);
+ WritableRaster wr = (WritableRaster)cr.getData();
+ ColorModel cm = cr.getColorModel();
+ BufferedImage image = new BufferedImage
+ (cm, wr, cm.isAlphaPremultiplied(), null);
+ cr = GraphicsUtil.wrap(image);
+ filt = new RedRable(cr);
+ } catch (IOException ioe) {
+ // Something bad happened here...
+ filt = ImageTagRegistry.getBrokenLinkImage
+ (AbstractImageIORegistryEntry.this,
+ errCode, errParam);
+ } catch (ThreadDeath td) {
+ filt = ImageTagRegistry.getBrokenLinkImage
+ (AbstractImageIORegistryEntry.this,
+ errCode, errParam);
+ dr.setSource(filt);
+ throw td;
+ } catch (Throwable t) {
+ filt = ImageTagRegistry.getBrokenLinkImage
+ (AbstractImageIORegistryEntry.this,
+ errCode, errParam);
+ }
+
+ dr.setSource(filt);
+ }
+ };
+ t.start();
+ return dr;
+}
+```
+
+看上面的实现是一个非常有意思的事情, 开了一个线程做事情,而且直接就返回了,相当于给了别人一个储物箱的钥匙,虽然现在储物箱是空的,但是回头我会填满的
+
+言归正传,主要的业务逻辑就在这个线程里了,核心的几行代码就是
+
+```java
+// 加载图片,转为BufferedImage对象
+BufferedImage bi = reader.read(imageIndex);
+cr = GraphicsUtil.wrap(bi);
+// 下面实现对图片的ARGB进行修改
+cr = new Any2sRGBRed(cr);
+cr = new FormatRed(cr, GraphicsUtil.sRGB_Unpre);
+WritableRaster wr = (WritableRaster)cr.getData();
+ColorModel cm = cr.getColorModel();
+BufferedImage image = new BufferedImage
+ (cm, wr, cm.isAlphaPremultiplied(), null);
+cr = GraphicsUtil.wrap(image);
+filt = new RedRable(cr);
+```
+
+debug上面的几行代码,发现问题比较明显了,就是这个图片的转换跪了,至于为啥? java的图片各种蛋疼至极,这里面的逻辑,真心搞不进去,so深挖到此为止
+
+---
+
+## 3.3. 兼容逻辑
+
+问题定位到了,当然就是想办法来修复了,简单来说,需要兼容的就是图片的类型转换上了,直接用原来的可能会抛异常,所以做了一个简单的兼容逻辑
+
+```java
+if(bi.getType() == BufferedImage.TYPE_BYTE_INDEXED) {
+ BufferedImage image = new BufferedImage(bi.getWidth(), bi.getHeight(), BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g2d = image.createGraphics();
+ g2d.drawImage(bi, 0, 0, null);
+ g2d.dispose();
+ cr = GraphicsUtil.wrap(image);
+} else {
+ cr = GraphicsUtil.wrap(bi);
+ cr = new Any2sRGBRed(cr);
+ cr = new FormatRed(cr, GraphicsUtil.sRGB_Unpre);
+ WritableRaster wr = (WritableRaster)cr.getData();
+ ColorModel cm = cr.getColorModel();
+ BufferedImage image = new BufferedImage
+ (cm, wr, cm.isAlphaPremultiplied(), null);
+ cr = GraphicsUtil.wrap(image);
+}
+```
+
+
+再次验证,ok
+
+**注意:**
+
+一个问题来了,上面的兼容是需要修改源码的,我们可以怎么办?有几种解决方法
+
+- 猥琐方法一:down下源码,修改版本,然后传到自己的私服,使用自己的vip包
+- 猥琐方法二:把 batik-codec 工程原样拷贝到自己的项目中,就可以随意的使用改了
+- 猥琐方法三:写一个完全相同的类(包路径完全相同),然后构造一个自定义类加载器,加载这个自己的这个兼容版本的,替换原来的(未测试,不确定是否能行)
+
+
+至于我的选择,就是使用了猥琐方法二
diff --git "a/docs/\351\207\207\345\235\221/markdown\350\275\254\345\233\276\347\211\207\344\270\255\346\226\207\344\271\261\347\240\201.md" "b/docs/\351\207\207\345\235\221/markdown\350\275\254\345\233\276\347\211\207\344\270\255\346\226\207\344\271\261\347\240\201.md"
new file mode 100644
index 00000000..12c6f3e3
--- /dev/null
+++ "b/docs/\351\207\207\345\235\221/markdown\350\275\254\345\233\276\347\211\207\344\270\255\346\226\207\344\271\261\347\240\201.md"
@@ -0,0 +1,56 @@
+# markdown转图片中文乱码
+> 在本机(Mac系统)上测试,markdown转图片,妥妥的,没啥问题;可丢在阿里云服务器上就诡异了
+
+## 2.1. 问题表征
+
+在阿里云上部署的服务,提供markdonw转图片,本来一切都妥妥的,结果发现传中文过来时,中文全部被方框框给替换掉了,即出现中文乱码了!!!
+
+
+关注几点:
+
+1. 本机测试ok(mac系统)
+2. 在windows虚拟机测试也ok
+3. linux系统部署失败
+4. 服务接收的中文非乱码,utf8格式
+5. 生成图片的中文签名正常(采用自定义的娃娃体)
+
+
+## 2.2. 问题分析和解决
+
+1. 当出现这个问题时,第一感觉就是中文传给服务的时候就乱码了,这个很容易定位否定
+ - 常见手段1 : 打日志输出参数
+ - 常见手段2 : 远程debug
+2. 中文签名正常,markdonw里面的中文乱码
+ - 到这里,刚开始也是怀疑编码格式不对导致,因此设置中文的编码格式,打点输出接受参数的实际编码
+3. 上面两个都排除之外,依然没有啥头绪,这个问题就搁置了一段时间,直到再次在一个项目中遇到了这个问题,才怀疑到可能是系统的字体支持问题
+
+4. 百度/google上搜索,发现jre默认不支持中文字体,采用awt进行绘图时,中文乱码
+
+
+尝试修复问题
+
+- 下载宋体 : [simsun.ttf](https://github.com/liuyueyi/quick-media/tree/master/common/src/main/resources/font/simsun.ttf)
+- 安装到jre的字体目录
+
+ ```
+ // 登录到服务器
+
+ // 下载字体
+ wget https://github.com/liuyueyi/quick-media/tree/master/common/src/main/resources/font/simsun.ttf
+
+ // 安装字体, jdk的目录与实际相关
+ cp simsun.ttf /usr/local/jdk/jre/lib/fonts/
+ ```
+- 重启应用
+- 再次测试
+
+
+到此解决问题
+
+## 2.3. 小结说明
+
+事后想想,这个其实是一个很明显的问题,自定义的中文签名没有问题,但是其他的中文乱码,这里就可以说明不少的问题了
+
+其次就是,刚开始遇到时,排除几个问题之后,没啥头绪,就放掉了,没有继续深究这个问题,导致搁置了较长的时间,有点坑
+
+最后也是最重要的一点,个人维护的项目,总感觉激情不是特别的足,有鸵鸟心态,这个问题还是在实际的工作项目中,再次遇到后,并没有花多久就定位并解决掉,原因何在?因为工作的这个需求,必须得完成,遇到问题不管怎么样,必须得想办法去fix掉,不然这个工资就没法愉快的领取,所以还是有压力才有动力
\ No newline at end of file
diff --git "a/docs/\351\207\207\345\235\221/\345\205\274\345\256\271ImageIO\350\257\273\345\217\226jpeg\345\233\276\347\211\207\345\217\230\347\272\242.md" "b/docs/\351\207\207\345\235\221/\345\205\274\345\256\271ImageIO\350\257\273\345\217\226jpeg\345\233\276\347\211\207\345\217\230\347\272\242.md"
new file mode 100644
index 00000000..e2c9a304
--- /dev/null
+++ "b/docs/\351\207\207\345\235\221/\345\205\274\345\256\271ImageIO\350\257\273\345\217\226jpeg\345\233\276\347\211\207\345\217\230\347\272\242.md"
@@ -0,0 +1,95 @@
+# 兼容ImageIO读取jpeg图片变红
+
+> 使用ImageIO.read()方法,加载图片为BufferedImage对象时,对于某些图片,会出现变红的case
+
+
+
+## 4.1 问题重现
+
+有问题的图片: ![img](http://s17.mogucdn.com/mlcdn/c45406/170418_68lkjddg3bll08h9c9bk0d8ihkffi_800x1200.jpg)
+
+测试验证代码
+
+```java
+/**
+ * 图片读取之后,颜色变红的测试
+ */
+@Test
+public void testLoadRedImg() throws IOException {
+ String url = "http://s17.mogucdn.com/mlcdn/c45406/170418_68lkjddg3bll08h9c9bk0d8ihkffi_800x1200.jpg";
+ URL u = new URL(url);
+ BufferedImage bf = ImageIO.read(u);
+ ImageIO.write
+ System.out.println("--over--");
+
+}
+```
+
+debug截图如下:
+
+![FFC31217-6457-46BD-9DB4-3C0632C2AE07.png](https://s3.mogucdn.com/mlcdn/c45406/180122_0j91i74djjka8bj959db144eecaaj_1882x1100.jpg)
+
+
+## 4.2 问题兼容
+
+不实用ImageIO来加载图片,改用Toolkit来实现图片读取,然后再将读取到的图片绘制到BufferedImage对象上
+
+```java
+@Test
+public void testLoadRedImg2() throws MalformedURLException {
+ String url = "http://s17.mogucdn.com/mlcdn/c45406/170418_68lkjddg3bll08h9c9bk0d8ihkffi_800x1200.jpg";
+ URL u = new URL(url);
+ Image img = Toolkit.getDefaultToolkit().getImage(u);
+ BufferedImage bf = toBufferedImage(img);
+ System.out.println("eeee");
+}
+
+
+static BufferedImage toBufferedImage(Image image) {
+ if (image instanceof BufferedImage) {
+ return (BufferedImage) image;
+ }
+ // This code ensures that all the pixels in the image are loaded
+ image = new ImageIcon(image).getImage();
+ BufferedImage bimage = null;
+ GraphicsEnvironment ge = GraphicsEnvironment
+ .getLocalGraphicsEnvironment();
+ try {
+ int transparency = Transparency.OPAQUE;
+ GraphicsDevice gs = ge.getDefaultScreenDevice();
+ GraphicsConfiguration gc = gs.getDefaultConfiguration();
+ bimage = gc.createCompatibleImage(image.getWidth(null),
+ image.getHeight(null), transparency);
+ } catch (HeadlessException e) {
+ // The system does not have a screen
+ }
+ if (bimage == null) {
+ // Create a buffered image using the default color model
+ int type = BufferedImage.TYPE_INT_RGB;
+ bimage = new BufferedImage(image.getWidth(null),
+ image.getHeight(null), type);
+ }
+ // Copy image to buffered image
+ Graphics g = bimage.createGraphics();
+ // Paint the image onto the buffered image
+ g.drawImage(image, 0, 0, null);
+ g.dispose();
+ return bimage;
+}
+```
+
+
+实测验证
+
+![4871A1EA-A9FC-46A8-9C9D-954B93978435.png](https://s3.mogucdn.com/mlcdn/c45406/180122_1aih840f4b60c15hdd51kabc0ee5g_2058x1034.jpg)
+
+为什么会出现这个问题:
+
+ImageIO.read()方法读取图片时可能存在不正确处理图片ICC信息的问题,ICC为JPEG图片格式中的一种头部信息,导致渲染图片前景色时蒙上一层红色。
+
+
+## 4.3 其他
+
+#### 参考文档
+
+- [Java处理某些图片红色问题](http://blog.csdn.net/amorym/article/details/52936470)
diff --git "a/docs/\351\207\207\345\235\221/\345\233\276\347\211\207\346\227\213\350\275\254\351\227\256\351\242\230\344\277\256\345\244\215.md" "b/docs/\351\207\207\345\235\221/\345\233\276\347\211\207\346\227\213\350\275\254\351\227\256\351\242\230\344\277\256\345\244\215.md"
new file mode 100644
index 00000000..130a19a8
--- /dev/null
+++ "b/docs/\351\207\207\345\235\221/\345\233\276\347\211\207\346\227\213\350\275\254\351\227\256\351\242\230\344\277\256\345\244\215.md"
@@ -0,0 +1,41 @@
+# 图片旋转问题修复
+> 图片旋转作为一个常见功能,实际使用中用处挺多,但是这次实现却遇到了个小问题,记录一二
+
+使用的几个类
+
+- `Graphics2d`
+- `AffineTransform`
+- BufferedImage
+
+
+## 1.1. Graphics2d 方式
+
+利用Graphics2d的rotate方法来实现图片旋转,奇怪的是一直不生效,实现代码如下
+
+
+```java
+BufferedImage bufferedImage = ImageUtil.getImageByPath("bg.png");
+Graphics2D g2d = bufferedImage.createGraphics();
+g2d.rotate(Math.toRadians(90), bufferedImage.getWidth() >> 1, bufferedImage.getHeight() >> 1);
+g2d.dispose();
+```
+
+
+
+## 1.2. `AffineTransform` 方式
+
+```java
+BufferedImage bufferedImage = ImageUtil.getImageByPath("bg.png");
+
+AffineTransform tx = new AffineTransform();
+tx.rotate(0.5, bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2);
+
+AffineTransformOp op = new AffineTransformOp(tx,
+ AffineTransformOp.TYPE_BILINEAR);
+bufferedImage = op.filter(bufferedImage, null);
+```
+
+
+## 1.3 参考
+
+- [Rotating a Buffered Image : Image « Advanced Graphics « Java](http://www.java2s.com/Code/Java/Advanced-Graphics/RotatingaBufferedImage.htm)
\ No newline at end of file
diff --git a/plugins/phantom-plugin/pom.xml b/plugins/phantom-plugin/pom.xml
index bba410cf..503787b3 100644
--- a/plugins/phantom-plugin/pom.xml
+++ b/plugins/phantom-plugin/pom.xml
@@ -27,6 +27,12 @@
3.12.0
+
+ org.jsoup
+ jsoup
+ 1.11.3
+
+
com.github.detro
ghostdriver
diff --git a/plugins/phantom-plugin/src/test/java/com/github/hui/quick/plugin/test/Html2ImgTest.java b/plugins/phantom-plugin/src/test/java/com/github/hui/quick/plugin/test/Html2ImgTest.java
index f2e0c68d..8791bd1e 100644
--- a/plugins/phantom-plugin/src/test/java/com/github/hui/quick/plugin/test/Html2ImgTest.java
+++ b/plugins/phantom-plugin/src/test/java/com/github/hui/quick/plugin/test/Html2ImgTest.java
@@ -1,6 +1,8 @@
package com.github.hui.quick.plugin.test;
import com.github.hui.quick.plugin.phantom.Html2ImageByJsWrapper;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
import org.junit.Test;
import java.awt.image.BufferedImage;