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

    Некоторое время тому на хабре появилась статья о художнике, который рисует картины в 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 (или какая-либо другая либа) работать с веркторными рисунками в офисных документах я не знаю :(
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 20
    • +1
      Код без отступов выглядит нечитаемо. Воспользуйтесь тегом <source>!
    • +1
      А почему именно с проприетарными форматами? Если попробовать с OpenDocument или Office Open XML?
      • 0
        С XML документами Microsoft Office (docx, xlsx) Apache POI тоже работает. Касательно форматов OpenOffice (Libre Office и т.п.), так большинство моих знакомых вообще не в курсе что это такое :(
        Обязательно попробую на досуге посмотреть как сделать аналогичную вещь с документами odf; уверен что проблем особых быть не должно
        • 0
          посмотрели?
          Apache poi AFAIK не поддерживает ods.
          • 0
            Таки не поддерживает.
            Вплотную не занимался, нагуглил либу ODFDOM но пока не пробовал ее использовать.
            Если начинать копать, то отсюда: incubator.apache.org/odftoolkit/odfdom/Layers.html
      • –4
        Но зачем?
        • +2
          Just for fun.

          Иногда такие задачи решать просто интересно.
          • +2
            А Вы ничего никогда не делаете просто так, просто потому что захотелось?
            • 0
              Никакого практического применения этому я придумать, конечно, не могу. Такая цель и не ставилась, просто было интересно именно поиграться. Да и смотреть на удивленный взгляд человека, которому показываешь картину в экселе — то еще удовольствие
              • 0
                Давным давно видел картинку в вебе, которая показывается даже с отключенными картинками и сохранить её было сложно (только PrtScrn). Да, нарисовано было ячейками в один пиксель.
                • НЛО прилетело и опубликовало эту надпись здесь
                  • 0
                    Не факт. Я подобным баловался ещё в универе: никакого CSS, только table, td в один пиксел и bgcolor! :D
                    • 0
                      Тогда и слов-то таких не было. Это наверно конец того века был.
                • 0
                  Немного VSTO и получаем довольно точную копию картинки =)
                  image

                  Документ и пачка dll-ок к нему

                  При желании, можно добавить ресайз и уменьшение глубины цвета — это дело техники.

                  Ключевой кусок кода:
                  using (var image = Image.FromFile(Target.Text) as Bitmap)
                  {
                      var range = Range[Cells[2, 2], Cells[image.Height + 2, image.Width + 2]] as Excel.Range;
                      range.ColumnWidth = 1.0 / 12; // магия,  для столбцов и колонок используются разные единицы измерения
                      range.RowHeight = 1.0;
                      for (int rowIndex = 0; rowIndex < image.Height; rowIndex++)
                      {
                          for (int colIndex = 0; colIndex < image.Width; colIndex++)
                          {
                              var cell = Range[Cells[rowIndex + 2, colIndex + 2], Cells[rowIndex + 2, colIndex + 2]] as Excel.Range;
                              cell.Interior.Color = ColorTranslator.ToOle(image.GetPixel(colIndex, rowIndex));
                          }
                      }
                  }


                  P.S. На больших картинках лучше не эксперементировать — работает сие поделие не слишком быстро.
                  • 0
                    для столбцов и колонок

                    конечно же, для столбцов и строк =) спать пора идти
                    • 0
                      Ячейки размером в пиксель будут смотреться шикарно. Правда ограничение на количество ячеек остается, так что большие (по разрешению) картинки все равно остаются не у дел :(
                      • 0
                        там и так ячейки в один пиксель =) а по количеству строк и столбцов — боюсь, вероятность того, что пользователь задолбается ждать, значительно выше того, что у Экселя 2010+ закончится индекс столбцов (16384 или XFD) или тем более строк (1048576)
                        • 0
                          range.RowHeight = 1.0;

                          Понимаю что в 1, просто неправильно сформулировал предложение. А вот что у 2010+ увеличен индекс столбцов не знал, спасибо.
                          PS
                          Видимо, фраза «вставить скриншот в эксель» может быть интерпретирована даже очень буквально :)
                      • 0
                        Минут 30 по-моему работало. Делал в августе, а исходники где-то потерялись, к сожалению.
                        Скриншот

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.