Pull to refresh

Рисунки в MS Excel при помощи Apache POI

Reading time 4 min
Views 18K
Некоторое время тому на хабре появилась статья о художнике, который рисует картины в MS Excel, используя его векторные возможности. Но задолго до этого я натыкался на истории о феноменальных растровых рисунках в том же экселе, идея которых базируется на пиксель-арте. Т.е. кто-то попросту уменьшает размеры ячеек и использует заливку цветом для получения своеобразной мозаики. Выглядит довольно впечатляюще, хотя до векторных рисунков по качеству, конечно, не дотягивает.

Увидев такие картины я, конечно же, усомнился в том, что кому-то хватит усидчивости для создания их в ручном режиме и решил поискать способ автоматизировать «офисное творчество». Задача оказалась несложной для реализации на языке Java при условии использования библиотеки Apache POI, предназначенной для работы с проприетарными форматами Microsoft Office. Подробности под катом.

Итак, что мы имеем. Я поставил себе задачу создать приложение для конвертирования самой обыкновенной картинки JPG (или, в принципе, любого другого распространенного формата) в документ Excel. Сразу стоит упомянуть существующие ограничения:
ширина «картинки» не должна превышать 255 точек (максимальное количество столбцов на листе)
максимальное количество стилей оформления (в нашем случае это количество цветов) равно 4000
Таким образом либо придется предварительно найти и подготовить картинку (уменьшить размеры и глубину цвета) или же делать это программно. Мы пойдем вторым путем :)
image
Для начала набросаем Main-класс нашей программы, содержащий единственный метод:

public class Main {
    public static void main(String[] args) {
        IMGRead ir = new IMGRead();
        Map<String, Object[]> data = ir.read("C:\\picture.jpg");
        POIWrite pw = new POIWrite();
       pw.write(data);
    }
}


Конечно, убого и хардкод, но для демонстрации сойдет.

Рассмотрим класс для чтения картинки из файла. Он содержит метод для собственно чтения картинки и возвращает результат в виде карты, содержащей объекты типа RGBColor, в которых хранится информация о трех составляющих цвета точки:

public Map<String, Object[]> read(String fileName) {
        File file = new File(fileName);
        BufferedImage source, image;//source and resized images
        Map<String, Object[]> data = new TreeMap<String, Object[]>();
        try {
            source = ImageIO.read(file);//read picture from file
            int type = source.getType() == 0? BufferedImage.TYPE_INT_ARGB : source.getType();//get type
            image = resizeImage(source, type);//resize
            source = convert8(image);
            image = source; // :)

            // Getting pixel color for every pixel
            for (Integer y = 0; y < image.getHeight(); y++) {
                Object[] line = new Object[image.getWidth()];
                for (int x = 0; x < image.getWidth(); x++) {
                    int clr = image.getRGB(x, y);
                    int red = (clr & 0x00ff0000) >> 16;
                    int green = (clr & 0x0000ff00) >> 8;
                    int blue = clr & 0x000000ff;
                    line[x] = new RGBColor(red, green, blue);

                }
                data.put(String.format("%03d", y), line);

            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return data;
    }


Так же мы имеем метод для ресайза картинки:

private static BufferedImage resizeImage(BufferedImage originalImage, int type) {
        BufferedImage resizedImage = new BufferedImage(IMG_WIDTH, IMG_HEIGHT, type);
        Graphics2D g = resizedImage.createGraphics();
        g.drawImage(originalImage, 0, 0, IMG_WIDTH, IMG_HEIGHT, null);
        g.dispose();

        return resizedImage;
    }


И метод для уменьшения цветности (я использую восьмибитный цвет, для других вариантов смотрите источник):

public static BufferedImage convert8(BufferedImage src) {
        BufferedImage dest = new BufferedImage(src.getWidth(), src.getHeight(),
            BufferedImage.TYPE_BYTE_INDEXED);
        ColorConvertOp cco = new ColorConvertOp(src.getColorModel()
            .getColorSpace(), dest.getColorModel().getColorSpace(), null);
        cco.filter(src, dest);
        return dest;
      }


Переходим к классу, реализующему запись «картинки» в документ экселя. Тут у нас 2 метода, в первом из них осуществляется запись в файл:

public void write(Map<String, Object[]> data) {
        HSSFWorkbook workbook = new HSSFWorkbook();
        HSSFSheet sheet = workbook.createSheet("Picture");
        Map<String, HSSFCellStyle> colorToStyle = new HashMap<String, HSSFCellStyle>();
        HSSFCellStyle style;

        Set<String> keyset = data.keySet();
        int rownum = 0;
        for (String key : keyset) {
            Row row = sheet.createRow(rownum++);
            row.setHeight((short) 50);
            Object[] objArr = data.get(key);
            int cellnum = 0;
            for (Object obj : objArr) {
                sheet.setColumnWidth(cellnum, 100);
                Cell cell = row.createCell(cellnum++);
                RGBColor rgb = (RGBColor) obj;
                try {
                    style = colorToStyle.get(rgb.toString());
                    cell.setCellStyle(style);
                } catch (Exception e) {
                    style = workbook.createCellStyle();
                    style.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
                    style.setFillForegroundColor(setColor(workbook, rgb.getR(), rgb.getG(), rgb.getB()).getIndex());
                    colorToStyle.put(rgb.toString(), style);
                    cell.setCellStyle(style);
                }
            }
        }

        try {
            FileOutputStream out =
                    new FileOutputStream(new File("C:\\picture.xls"));
            workbook.write(out);
            out.close();

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

Карта colorToStyle служит для хранения стилей оформления ячеек. Создавая новый стиль, мы ассоциируем его с определенным цветом и каждый раз при закраске новой ячейки проверяем существует ли для нужного цвета готовый стиль, или же необходимо его создать.

Ну и, наконец, метод для преобразования RGB-цвета в формат HSSFColor, используемый в Apache POI. Обратите внимание на то, что используется метод findSimilarColor(), который пытается автоматически подобрать похожий цвет в палитре.

public HSSFColor setColor(HSSFWorkbook workbook, byte r, byte g, byte b) {
        HSSFPalette palette = workbook.getCustomPalette();
        HSSFColor hssfColor = null;
        try {
            hssfColor = palette.findSimilarColor(r, g, b);
            if (hssfColor == null) {
                System.err.println("null " + r + " " + g + " " + b);
                palette.setColorAtIndex(HSSFColor.RED.index, r, g, b);
                hssfColor = palette.getColor(HSSFColor.RED.index);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return hssfColor;
    }


Результаты «творчества»:

Конечно, до шедевра таким мозаикам далеко, но при правильном подборе цветов в изображении и его размера можно получить довольно симпатичные «рисунки». Умеет ли Apache POI (или какая-либо другая либа) работать с веркторными рисунками в офисных документах я не знаю :(
Tags:
Hubs:
+22
Comments 20
Comments Comments 20

Articles