/** * Copyright 2015-2017 Knowm Inc. (http://knowm.org) and contributors. * Copyright 2011-2015 Xeiam LLC (http://xeiam.com) and contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.knowm.xchart; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import javax.imageio.IIOImage; import javax.imageio.ImageIO; import javax.imageio.ImageTypeSpecifier; import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriter; import javax.imageio.metadata.IIOInvalidTreeException; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.stream.FileImageOutputStream; import org.knowm.xchart.internal.chartpart.Chart; /** * A helper class with static methods for saving Charts as bitmaps * * @author timmolter */ public final class BitmapEncoder { /** * Constructor - Private constructor to prevent instantiation */ private BitmapEncoder() { } public enum BitmapFormat { PNG, JPG, BMP, GIF } /** * Only adds the extension of the BitmapFormat to the filename if the filename doesn't already have it. * * @param fileName * @param bitmapFormat * @return filename (if extension already exists), otherwise;: filename + "." + extension */ public static String addFileExtension(String fileName, BitmapFormat bitmapFormat) { String fileNameWithFileExtension = fileName; final String newFileExtension = "." + bitmapFormat.toString().toLowerCase(); if (fileName.length() <= newFileExtension.length() || !fileName.substring(fileName.length() - newFileExtension.length(), fileName.length()).equalsIgnoreCase(newFileExtension)) { fileNameWithFileExtension = fileName + newFileExtension; } return fileNameWithFileExtension; } /** * Save a Chart as an image file * * @param chart * @param fileName * @param bitmapFormat * @throws IOException */ public static void saveBitmap(Chart chart, String fileName, BitmapFormat bitmapFormat) throws IOException { OutputStream out = new FileOutputStream(addFileExtension(fileName, bitmapFormat)); try { saveBitmap(chart, out, bitmapFormat); } finally { out.close(); } } /** * Write a Chart into a given stream. Does not close the target stream automatically at the end of the operation * * @param chart * @param targetStream * @param bitmapFormat * @throws IOException */ public static void saveBitmap(Chart chart, OutputStream targetStream, BitmapFormat bitmapFormat) throws IOException { BufferedImage bufferedImage = getBufferedImage(chart); ImageIO.write(bufferedImage, bitmapFormat.toString().toLowerCase(), targetStream); } /** * Save list of Charts as an image file. * Function assumes that all charts are the same size (width, height). * Number of charts should equal rows multiplied by cols. * * @param charts * @param rows number of rows * @param cols number of columns * @param fileName * @param bitmapFormat * @throws IOException */ public static void saveBitmap(List<Chart> charts, Integer rows, Integer cols, String fileName, BitmapEncoder.BitmapFormat bitmapFormat) throws IOException { OutputStream out = new FileOutputStream(addFileExtension(fileName, bitmapFormat)); try { saveBitmap(charts, rows, cols, out, bitmapFormat); } finally { out.close(); } } /** * Save list of Charts into a given stream. Does not close the target stream automatically at the end of the operation. * Function assumes that all charts are the same size (width, height). * Number of charts should equal rows multiplied by cols. * * @param charts * @param rows number of rows * @param cols number of columns * @param targetStream * @param bitmapFormat * @throws IOException */ public static void saveBitmap(List<Chart> charts, Integer rows, Integer cols, OutputStream targetStream, BitmapEncoder.BitmapFormat bitmapFormat) throws IOException { List<BufferedImage> chartImages = new LinkedList<BufferedImage>(); for (Chart c : charts) chartImages.add(getBufferedImage(c)); BufferedImage bufferedImage = mergeImages( chartImages, rows, cols ); ImageIO.write(bufferedImage, bitmapFormat.toString().toLowerCase(), targetStream); } /** * Save a chart as a PNG with a custom DPI. The default DPI is 72, which is fine for displaying charts on a computer monitor, but for printing * charts, a DPI of around 300 is much better. * * @param chart * @param fileName * @param DPI * @throws IOException */ public static void saveBitmapWithDPI(Chart chart, String fileName, BitmapFormat bitmapFormat, int DPI) throws IOException { double scaleFactor = DPI / 72.0; BufferedImage bufferedImage = new BufferedImage((int) (chart.getWidth() * scaleFactor), (int) (chart.getHeight() * scaleFactor), BufferedImage.TYPE_INT_RGB); Graphics2D graphics2D = bufferedImage.createGraphics(); AffineTransform at = graphics2D.getTransform(); at.scale(scaleFactor, scaleFactor); graphics2D.setTransform(at); chart.paint(graphics2D, chart.getWidth(), chart.getHeight()); Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(bitmapFormat.toString().toLowerCase()); if (writers.hasNext()) { ImageWriter writer = writers.next(); // instantiate an ImageWriteParam object with default compression options ImageWriteParam iwp = writer.getDefaultWriteParam(); ImageTypeSpecifier typeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_RGB); IIOMetadata metadata = writer.getDefaultImageMetadata(typeSpecifier, iwp); if (metadata.isReadOnly() || !metadata.isStandardMetadataFormatSupported()) { throw new IllegalArgumentException("It is not possible to set the DPI on a bitmap with " + bitmapFormat + " format!! Try another format."); } setDPI(metadata, DPI); File file = new File(addFileExtension(fileName, bitmapFormat)); FileImageOutputStream output = new FileImageOutputStream(file); writer.setOutput(output); IIOImage image = new IIOImage(bufferedImage, null, metadata); try { writer.write(null, image, iwp); writer.dispose(); } finally { output.close(); } } } /** * Sets the metadata correctly * * @param metadata * @param DPI * @throws IIOInvalidTreeException */ private static void setDPI(IIOMetadata metadata, int DPI) throws IIOInvalidTreeException { // for PNG, it's dots per millimeter double dotsPerMilli = 1.0 * DPI / 10 / 2.54; IIOMetadataNode horiz = new IIOMetadataNode("HorizontalPixelSize"); horiz.setAttribute("value", Double.toString(dotsPerMilli)); IIOMetadataNode vert = new IIOMetadataNode("VerticalPixelSize"); vert.setAttribute("value", Double.toString(dotsPerMilli)); IIOMetadataNode dim = new IIOMetadataNode("Dimension"); dim.appendChild(horiz); dim.appendChild(vert); IIOMetadataNode root = new IIOMetadataNode("javax_imageio_1.0"); root.appendChild(dim); metadata.mergeTree("javax_imageio_1.0", root); } /** * Save a Chart as a JPEG file * * @param chart * @param fileName * @param quality - a float between 0 and 1 (1 = maximum quality) * @throws IOException */ public static void saveJPGWithQuality(Chart chart, String fileName, float quality) throws IOException { BufferedImage bufferedImage = getBufferedImage(chart); Iterator<ImageWriter> iter = ImageIO.getImageWritersByFormatName("jpeg"); ImageWriter writer = iter.next(); // instantiate an ImageWriteParam object with default compression options ImageWriteParam iwp = writer.getDefaultWriteParam(); iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); iwp.setCompressionQuality(quality); File file = new File(fileName); FileImageOutputStream output = new FileImageOutputStream(file); try { writer.setOutput(output); IIOImage image = new IIOImage(bufferedImage, null, null); writer.write(null, image, iwp); writer.dispose(); } finally { output.close(); } } /** * Generates a byte[] for a given chart * * @param chart * @return a byte[] for a given chart * @throws IOException */ public static byte[] getBitmapBytes(Chart chart, BitmapFormat bitmapFormat) throws IOException { BufferedImage bufferedImage = getBufferedImage(chart); byte[] imageInBytes; ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(bufferedImage, bitmapFormat.toString().toLowerCase(), baos); try { baos.flush(); imageInBytes = baos.toByteArray(); } finally { baos.close(); } return imageInBytes; } public static BufferedImage getBufferedImage(Chart chart) { BufferedImage bufferedImage = new BufferedImage(chart.getWidth(), chart.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D graphics2D = bufferedImage.createGraphics(); chart.paint(graphics2D, chart.getWidth(), chart.getHeight()); return bufferedImage; } private static BufferedImage mergeImages(List<BufferedImage> images, Integer rows, Integer cols) { BufferedImage first = images.get(0); int singleImageWidth = first.getWidth(); int singleImageHeight = first.getHeight(); int totalWidth = singleImageWidth * cols; int totalHeight = singleImageHeight * rows; BufferedImage mergedImage = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_ARGB); Graphics g = mergedImage.getGraphics(); for (int row = 0; row < rows; row++) { for (int col = 0; col < cols; col++) { BufferedImage image = images.get(row * cols + col); g.drawImage(image, col * singleImageWidth, row * singleImageHeight, null); } } return mergedImage; } }