2010年11月26日

html生成图片

根据html来生成图片很多时候都会被用到,最常见的就是网页截图。通过html模板技术,可以让用户自定义一段html,然后为用户实时生成图片。

以下就是使用java 2D实现的html生成图片的方法,需要用到两个开源的jar包。core-renderer.jarjtidy-r938.jar,这两个jar包是用来渲染html页面的,但是不支持执行javasrcipt。另外还需要xmlgraphics-commons-1.4.jar,这是用来生成PNG图片的。

为了支持中文显示自动换行,需要修改org.xhtmlrenderer.layout.Breaker类。

因为xhtmlrenderer是外国人写的,默认是根据空格来决定是否可以换行的,但是中文语句都是没有空格的,修改后的行为就像样式word-break:break-all。core-renderer-repack.jar是我重新打的包,包含了Breaker.java的源代码。

以下是代码:

public class ImageGenerator {  
   private float jpgQuality = 0.9F;  
   private boolean useHighScaleQuality = true;  
   private ImageFormat imageFormat = ImageFormat.JPG;  
   private ChineseFontResolver fontResolver;  
   private static final Logger logger = Logger.getLogger(ImageGenerator.class);  
   public void init() {  
    XRLog.init("init log...");  
    initTidy();  
   }  
   private Tidy tidy = null;  
   private static HashMap RENDER_HINT = new HashMap();  
   static{  
    RENDER_HINT.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);  
    RENDER_HINT.put(RenderingHints.KEY_ANTIALIASING,RenderingHints. VALUE_ANTIALIAS_ON);  
    RENDER_HINT.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);  
    RENDER_HINT.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);  
    RENDER_HINT.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);  
    RENDER_HINT.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);  
    RENDER_HINT.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);  
   }  
   private static HashMap RENDER_SCALE_HINT = new HashMap();  
   static{  
    RENDER_SCALE_HINT.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);  
   }  
   private void initTidy() {  
    tidy = new Tidy();  
    tidy.setQuiet(true);  
    tidy.setXHTML(true);  
    tidy.setHideComments(true);  
    tidy.setInputEncoding("UTF-8");  
    tidy.setOutputEncoding("UTF-8");  
    tidy.setShowErrors(0);  
    tidy.setShowWarnings(false);  
   }  
   /**  
   * 按默认配置生成图片  
   * @param input  
   * @param width  
   * @param height  
   * @return  
   */  
   public ByteArrayOutputStream html2Image(InputStream input, int width, int height) {  
    ByteArrayOutputStream output = null;  
    try {  
      BufferedImage image = generateImage(input, width, height, RENDER_HINT);  
      output = generateOutput(image);  
    } catch (Exception e) {  
      logger.error("generate image fail.");  
      e.printStackTrace();  
    }  
    return output;  
   }  
   /**  
   * 生成图片并缩放  
   * @param input  
   * @param width  
   * @param height  
   * @param scaledWidth  
   * @param scaledHeight  
   * @return  
   */  
   public ByteArrayOutputStream html2Image(InputStream input, int width, int height, int scaledWidth, int scaledHeight) {  
    ByteArrayOutputStream output = null;  
    try {  
      HashMap hints = scaledWidth < width ? RENDER_SCALE_HINT : RENDER_HINT;  
      BufferedImage image = generateImage(input, width, height, hints);  
      BufferedImage scaled = scale(image, width, height, scaledWidth, scaledHeight, hints);  
      output = generateOutput(scaled);  
    } catch (Exception e) {  
      logger.error("generate image fail:[width=" + width + ",height=" + height + ",scaledWidth=" + scaledWidth + ",scaledHeight=" + scaledHeight + "]");  
      e.printStackTrace();  
    }  
    return output;  
   }  
   private BufferedImage scale(BufferedImage image, int width, int height, int scaledWidth, int scaledHeight, HashMap hints) {  
    if (scaledWidth < width && isUseHighScaleQuality()) {  
      do {  
         if (width > scaledWidth) {  
          width /= 2;  
          if (width < scaledWidth) {  
            width = scaledWidth;  
          }  
         }  
         if (height > scaledHeight) {  
          height /= 2;  
          if (height < scaledHeight) {  
            height = scaledHeight;  
          }  
         }  
         BufferedImage tmp = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);  
         Graphics2D g = tmp.createGraphics();  
         g.setRenderingHints(hints);  
         g.drawImage(image, 0, 0, width, height, null);  
         g.dispose();  
         image = tmp;  
       } while (width != scaledWidth || height != scaledHeight);  
    } else {  
      BufferedImage tmp = new BufferedImage(scaledWidth,scaledHeight, BufferedImage.TYPE_INT_RGB);  
      Graphics2D g = tmp.createGraphics();  
      g.setRenderingHints(hints);  
      g.drawImage(image, 0, 0, scaledWidth, scaledHeight, null);  
      g.dispose();  
      image = tmp;  
    }  
    return image;  
   }  
   private BufferedImage generateImage(InputStream input, int width, int height, HashMap hints) {  
    Document doc = tidy.parseDOM(input, null);  
    if (doc == null) {  
      logger.error("parse html file fail, invalid html file.");  
      return null;  
    }  
    BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);  
    Graphics2D graphics = (Graphics2D) image.getGraphics();  
    graphics.setRenderingHints(RENDER_HINT);  
    Graphics2DRenderer renderer = new Graphics2DRenderer();  
    SharedContext context = renderer.getSharedContext();  
    context.setFontResolver(getFontResolver());  
    context.setNamespaceHandler(new XhtmlNamespaceHandler());  
    renderer.setDocument(doc, "");  
    renderer.layout(graphics, new Dimension(width, height));  
    renderer.render(graphics);  
    graphics.dispose();  
    return image;  
   }  
   private ByteArrayOutputStream generateOutput(BufferedImage image) {  
    ByteArrayOutputStream output = new ByteArrayOutputStream();  
    try {  
      switch (getImageFormat()) {  
      case JPG:  
       encodeJpg(image, output);  
       break;  
      case PNG:  
       default:  
       encodePng(image, output);  
       break;  
      }  
    } catch (ImageFormatException e) {  
      logger.error("generate image fail.");  
      e.printStackTrace();  
      output = null;  
    } catch (IOException e) {  
      logger.error("generate image fail.");  
      e.printStackTrace();  
      output = null;  
    }  
    return output;  
   }  
   private void encodeJpg(BufferedImage image, ByteArrayOutputStream output, float quality) throws ImageFormatException, IOException {  
    JPEGEncodeParam param = JPEGCodec.getDefaultJPEGEncodeParam(image);  
    param.setQuality(quality, true);  
    JPEGImageEncoder imageEncoder = JPEGCodec.createJPEGEncoder(output, param);  
    imageEncoder.encode(image);  
   }  
   private void encodeJpg(BufferedImage image, ByteArrayOutputStream output) throws ImageFormatException, IOException {  
    encodeJpg(image, output, getJpgQuality());  
   }  
   private void encodePng(BufferedImage image, ByteArrayOutputStream output) throws IOException {  
    PNGEncodeParam param = PNGEncodeParam.RGB.getDefaultEncodeParam(image);  
    PNGImageEncoder imageEncoder = new PNGImageEncoder(output, param);  
    imageEncoder.encode(image);  
   }  
   public void setJpgQuality(float jpgQuality) {  
    this.jpgQuality = jpgQuality;  
   }  
   public float getJpgQuality() {  
    return jpgQuality;  
   }  
   public void setImageFormat(ImageFormat imageFormat) {  
    this.imageFormat = imageFormat;  
   }  
   public ImageFormat getImageFormat() {  
    return imageFormat;  
   }  
   public void setFontResolver(ChineseFontResolver fontResolver) {  
    this.fontResolver = fontResolver;  
   }  
   public ChineseFontResolver getFontResolver() {  
    return fontResolver;  
   }  
   public void setUseHighScaleQuality(boolean useHighScaleQuality) {  
    this.useHighScaleQuality = useHighScaleQuality;  
   }  
   public boolean isUseHighScaleQuality() {  
    return useHighScaleQuality;  
   }  
 }  
 enum ImageFormat {  
   JPG, PNG  
 }

为了使用中文字体,需要手工加载中文字体,这里都是用truetype字体:

/**
 * 中文字体支持,默认使用微软雅黑
 *
 */
public class ChineseFontResolver extends AWTFontResolver {

    private static Font DEFAULT_FONT;

    private String fontPath;

    public ChineseFontResolver() {
        super();
    }

    public void init() {
     try {
            InputStream input =  new FileInputStream(new File(getFontPath() + "msyh.ttf"));
            Font font = Font.createFont(Font.TRUETYPE_FONT, input);
            font = font.deriveFont(12.0F);
            setFontMapping("微软雅黑", font);
            DEFAULT_FONT = font;
            setFontMapping("arial", font);
            setFontMapping("serif", font);
            setFontMapping("SansSerif", font);
            setFontMapping("Monospaced", font);

        } catch (FontFormatException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    protected Font resolveFont(SharedContext ctx, String font, float size, IdentValue weight, IdentValue style, IdentValue variant) {
        Font result = super.resolveFont(ctx, "微软雅黑", size, weight, style, variant);
        if (result == null) {
            result = DEFAULT_FONT;
        }
        return result;
    }

    public void setFontPath(String fontPath) {
        this.fontPath = fontPath;
    }

    public String getFontPath() {
        return fontPath;
    }

}

truetype的微软雅黑结合ImageGenerator中的RENDER_HINT,渲染出来的中文字体质量还是相当不错的。
使用方法就像这样,为了不出现乱码,最好全部都是用UTF-8编码:

ByteArrayOutputStream output = null;  
 try {  
    Map data = new HashMap();  
    String html = getTemplateParser().parse("template.ftl", data);//使用freemarker填充数据  
    InputStream input = new ByteArrayInputStream(html.getBytes(Charset.forName("UTF-8")));  
    output = getImageGenerator().html2Image(input, 1260, 64);  
   //for test  
   // FileOutputStream file = new FileOutputStream(new File("d://images//result.jpg"));  
   // file.write(output.toByteArray());  
   // file.flush();  
   // file.close();  
 } catch (TemplateException e) {  
   e.printStackTrace();  
 } catch (IOException e) {  
   e.printStackTrace();  
 }  

以上代码是使用模板来生成图片,网站截图只要稍微修改一下代码即可,以下是个简单的例子:

public class Main {  
   public static void main(String[] args) {  
     try {  
       Tidy tidy = new Tidy();  
       tidy.setQuiet(true);  
       tidy.setXHTML(true);  
       tidy.setHideComments(true);  
       tidy.setInputEncoding("UTF-8");  
       tidy.setOutputEncoding("UTF-8");  
       tidy.setShowErrors(0);  
       tidy.setShowWarnings(false);  
       //for test start  
       FileOutputStream output = new FileOutputStream(new File("d:/1234.png"));  
       URL url=new URL("http://www.baidu.com");  
       URLConnection conn=url.openConnection();  
       conn.connect();  
       InputStream is=conn.getInputStream();  
       BufferedReader br=new BufferedReader(new InputStreamReader(is,"gbk"));  
       String s = "";   
       StringBuffer sb = new StringBuffer("");   
       while ((s = br.readLine()) != null) {    
           sb.append(s + "\r\n");   
       }  
       ByteArrayInputStream bis = new ByteArrayInputStream(sb.toString().getBytes("UTF-8"));  
       //for test end  
       Document doc = tidy.parseDOM(bis, null);  
       Graphics2DRenderer g2r = new Graphics2DRenderer();  
       g2r.setDocument(doc,"");  
       //这里其实还是使用固定的width、height去渲染网页,但是可以设置一个较大的值  
       Dimension dim = new Dimension(1024, 2000);  
       BufferedImage buff = new BufferedImage((int)dim.getWidth(), (int)dim.getHeight(), BufferedImage.TYPE_INT_RGB);  
       Graphics2D g = (Graphics2D)buff.getGraphics();  
       g2r.layout(g, new Dimension(1024, 2000));  
       g.dispose();  
       //这里获取真正的网页大小,如果页面有背景图设了repeat,会导致无法获取真实大小  
       Rectangle rect = g2r.getMinimumSize();  
       buff = new BufferedImage((int)rect.getWidth(), (int)rect.getHeight(), BufferedImage.TYPE_INT_RGB);  
       g = (Graphics2D)buff.getGraphics();  
       g2r.render(g);  
       g.dispose();  
       PNGEncodeParam param = PNGEncodeParam.RGB.getDefaultEncodeParam(buff);  
       PNGImageEncoder imageEncoder = new PNGImageEncoder(output, param);  
       imageEncoder.encode(buff);  
       output.flush();  
       output.close();  
     } catch (FileNotFoundException e) {  
       e.printStackTrace();  
     } catch (IOException e) {  
       e.printStackTrace();  
     }  
   }  
 }