Использование java.awt.image.BufferedImage для создания BITFAP-записи BIFF8 занимает много времени - есть ли лучший подход? - PullRequest
0 голосов
/ 20 сентября 2018

Итак, я создаю HSSFSheet с фоновым растровым изображением, установленным с помощью apache poi и собственным кодом низкого уровня.https://www.openoffice.org/sc/excelfileformat.pdf объявляет для Record BITMAP, BIFF8:

Пиксельные данные (массив линий высот растрового изображения, от нижней строки к верхней строке, см. Ниже)

...

В каждой строке все пиксели написаны слева направо.Каждый пиксель сохраняется в виде 3-байтового массива: красный, зеленый и синий компоненты цвета пикселя, в этом порядке.Размер каждой строки выравнивается с кратными 4, вставляя нулевые байты после последнего пикселя.

См. Изображение PDF для полной декларации: enter image description here

Для этого мой подход использует java.awt.image.BufferedImage типа BufferedImage.TYPE_3BYTE_BGR.Затем получает все байты RGB из растра этого BufferedImage в правильном порядке (от нижней строки к верхней строке) и заполняется до кратного 4 по ширине (направление x).

См. Код:

import java.io.FileOutputStream;
import java.io.FileInputStream;

import org.apache.poi.hssf.usermodel.*;

import org.apache.poi.hssf.record.RecordBase;
import org.apache.poi.hssf.record.StandardRecord;
import org.apache.poi.hssf.model.InternalSheet;
import org.apache.poi.util.LittleEndianOutput;

import java.lang.reflect.Field;

import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

import java.awt.image.BufferedImage;
import java.awt.Graphics2D;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import javax.imageio.ImageIO;

public class CreateExcelHSSFSheetBackgroundBitmap {

 static List<Byte> getBackgroundBitmapData(String filePath) throws Exception {

  //see https://www.openoffice.org/sc/excelfileformat.pdf - BITMAP

  List<Byte> data = new ArrayList<Byte>();

  // get file byte data in type BufferedImage.TYPE_3BYTE_BGR
  BufferedImage in = ImageIO.read(new FileInputStream(filePath));
  BufferedImage image = new BufferedImage(in.getWidth(), in.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
  Graphics2D graphics = image.createGraphics();
  graphics.drawImage(in, null, 0, 0);
  graphics.dispose();

  short width = (short)image.getWidth();
  short height = (short)image.getHeight();

  // each pixel has 3 bytes but the width bytes must be filled up to multiple of 4
  int widthBytesMultOf4 = (int)((width * 3 + 3) / 4 * 4);

// --- this part takes much time but I have not found any better possibility

  // put the bytes R G B into the data; lines of the bitmap must be from bottom line to top line
  int bytes = 0;
  for (short y = (short)(height - 1); y >= 0; y--) {
   for (short x = 0; x < width; x++) {
    int r = image.getData().getSample(x, y, 2);
    data.add(Byte.valueOf((byte)r));
    bytes++;
    int g = image.getData().getSample(x, y, 1);
    data.add(Byte.valueOf((byte)g));
    bytes++;
    int b = image.getData().getSample(x, y, 0);
    data.add(Byte.valueOf((byte)b));
    bytes++;
   } 
   // fill up x with 0 bytes up to multiple of 4
   for (int x = width * 3; x < widthBytesMultOf4; x++) {
    data.add(Byte.valueOf((byte)0));
    bytes++;
   }
  }

// ---

  // size  12 bytes (additional headers, see below) + picture bytes
  int size = 12 + bytes;

  // get size int as LITTLE_ENDIAN bytes
  ByteBuffer bSize = ByteBuffer.allocate(4);
  bSize.order(ByteOrder.LITTLE_ENDIAN);
  bSize.putInt(size);

  // get width short as LITTLE_ENDIAN bytes
  ByteBuffer bWidth = ByteBuffer.allocate(2);
  bWidth.order(ByteOrder.LITTLE_ENDIAN);
  bWidth.putShort(width);

  // get height short as LITTLE_ENDIAN bytes
  ByteBuffer bHeight = ByteBuffer.allocate(2);
  bHeight.order(ByteOrder.LITTLE_ENDIAN);
  bHeight.putShort(height);

  // put the record headers into the data
  Byte[] dataPart = new Byte[] { 0x09, 0x00, 0x01, 0x00, 
     bSize.array()[0], bSize.array()[1], bSize.array()[2], bSize.array()[3], // size
     //now 12 bytes follow
     0x0C, 0x00, 0x00, 0x00, 
     bWidth.array()[0], bWidth.array()[1], // width
     bHeight.array()[0], bHeight.array()[1], // height
     0x01, 0x00, 0x18, 0x00
   }; 

  data.addAll(0, Arrays.asList(dataPart));

  return data;
 }

 public static void main(String[] args) throws Exception {

  HSSFWorkbook workbook = new HSSFWorkbook();
  HSSFSheet sheet = workbook.createSheet("Sheet1");
  sheet = workbook.createSheet("Sheet2"); // this sheet gets the background image set

  // we need the binary records of the sheet
  // get InternalSheet
  Field _sheet = HSSFSheet.class.getDeclaredField("_sheet");
  _sheet.setAccessible(true); 
  InternalSheet internalsheet = (InternalSheet)_sheet.get(sheet); 

  // get List of RecordBase
  Field _records = InternalSheet.class.getDeclaredField("_records");
  _records.setAccessible(true);
  @SuppressWarnings("unchecked") 
  List<RecordBase> records = (List<RecordBase>)_records.get(internalsheet);

  // get bytes of the image file
  List<Byte> data = getBackgroundBitmapData("dummyText.png"); //PNG must not have transparency

  // do creating BitmapRecord and ContinueRecords from the data in parts of 8220 bytes
  BitmapRecord bitmapRecord = null;
  List<ContinueRecord> continueRecords = new ArrayList<ContinueRecord>();
  int bytes = 0;
  if (data.size() > 8220) {
   bitmapRecord = new BitmapRecord(data.subList(0, 8220));
   bytes = 8220;
   while (bytes < data.size()) {
    if ((bytes + 8220) < data.size()) {
     continueRecords.add(new ContinueRecord(data.subList(bytes, bytes + 8220)));
     bytes += 8220;
    } else {
     continueRecords.add(new ContinueRecord(data.subList(bytes, data.size())));
     break;
    }
   }
  } else {
   bitmapRecord = new BitmapRecord(data);
  }

  // add the records after PageSettingsBlock
  int i = 0;
  for (RecordBase r : records) {
   if (r instanceof org.apache.poi.hssf.record.aggregates.PageSettingsBlock) {
    break;
   }
   i++;
  }
  records.add(++i, bitmapRecord);
  for (ContinueRecord continueRecord : continueRecords) {
   records.add(++i, continueRecord);  
  }

  // debug output
  for (RecordBase r : internalsheet.getRecords()) {
   System.out.println(r);
  }

  // write out workbook
  workbook.write(new FileOutputStream("CreateExcelHSSFSheetBackgroundBitmap.xls"));
  workbook.close();

 }

 static class BitmapRecord extends StandardRecord {

  //see https://www.openoffice.org/sc/excelfileformat.pdf - BITMAP

  List<Byte> data = new ArrayList<Byte>();

  BitmapRecord(List<Byte> data) {
   this.data = data;
  }

  public int getDataSize() { 
   return data.size(); 
  }

  public short getSid() {
   return (short)0x00E9;
  }

  public void serialize(LittleEndianOutput out) {
   for (Byte b : data) {
    out.writeByte(b);
   }
  }
 }

 static class ContinueRecord extends StandardRecord {

  //see https://www.openoffice.org/sc/excelfileformat.pdf - CONTINUE

  List<Byte> data = new ArrayList<Byte>();

  ContinueRecord(List<Byte> data) {
   this.data = data;
  }

  public int getDataSize() { 
   return data.size(); 
  }

  public short getSid() {
   return (short)0x003C;
  }

  public void serialize(LittleEndianOutput out) {
   for (Byte b : data) {
    out.writeByte(b);
   }
  }
 }

}

Код работает, но часть между

// --- this part takes much time but I have not found any better possibility

и

// ---

занимает много времени, так как необходимо получить 3 байта RGB для каждого отдельного пикселя для получения их в соответствии свыше странного формата.

Кто-нибудь знает лучший подход?Может быть, приведенный выше странный формат не такой странный, как я думаю, и его уже можно использовать?

Ответы [ 2 ]

0 голосов
/ 21 сентября 2018

Вот измененная версия вашего кода, которая работает для меня, и довольно быстро.

  1. Я использую byte[]ByteArrayOutputStream) вокруг, не более List<Byte>,
  2. Поскольку у нас уже есть BufferedImage из TYPE_3BYTE_BGR, мы можем использовать его почти напрямую в качестве выхода BMP.Нам просто нужно: a) добавить действительный заголовок BMP и b) записать снизу вверх, c) добавить каждую строку развертки (строку) к 32-битной границе и d) переключить порядок BGR -> RGB.
  3. Я использую Raster для копирования (дополненных) строк данных в вывод, поскольку копирование больших кусков происходит быстрее, чем копирование отдельных байтов.

Как уже отмечалось вкомментарии, структура представляет собой стандартный BMP с BITMAPCOREHEADER (и без заголовка файла).К сожалению, ImageIO BMPImageWriter всегда записывает заголовок файла и использует BITMAPINFOHEADER, что составляет 40 байт.Возможно, вы могли бы обойти эти вещи и использовать стандартную программу записи, немного массируя данные (подсказка: заголовок файла содержит смещение по отношению к пиксельным данным со смещением 10), но поскольку основной формат BMP тривиален для реализации, онэто может быть так же просто, как показано ниже.

Хотя документация, безусловно, подразумевает, что при использовании других форматов, таких как PNG и JPEG напрямую, мне не удалось сделать это должным образом.

Вероятно, есть еще возможности для улучшения, если хотите, чтобы избежать копирования некоторого байтового массива (т. Е. Использовать смещение / длину и передавать весь массив данных в Bitmap/ContinueRecord с вместо Arrays.copyOfRange()).

Код:

import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.imageio.ImageIO;

import org.apache.poi.hssf.model.InternalSheet;
import org.apache.poi.hssf.record.RecordBase;
import org.apache.poi.hssf.record.StandardRecord;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.util.LittleEndianOutput;

public class CreateExcelHSSFSheetBackgroundBitmap {

    static byte[] getBackgroundBitmapData(String filePath) throws Exception {

        //see https://www.openoffice.org/sc/excelfileformat.pdf - BITMAP

        // get file byte data in type BufferedImage.TYPE_3BYTE_BGR
        BufferedImage in = ImageIO.read(new FileInputStream(filePath));
        BufferedImage image = new BufferedImage(in.getWidth(), in.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
        Graphics2D graphics = image.createGraphics();
        graphics.drawImage(in, null, 0, 0);
        graphics.dispose();

        // calculate row size (c)
        int rowSize = ((24 * image.getWidth() + 31) / 32) * 4;

        ByteArrayOutputStream output = new ByteArrayOutputStream(image.getHeight() * rowSize * 3 + 1024);

        // put the record headers into the data
        ByteBuffer header = ByteBuffer.allocate(8 + 12);
        header.order(ByteOrder.LITTLE_ENDIAN);

        // Undocumented XLS stuff
        header.putShort((short) 0x09);
        header.putShort((short) 0x01);
        header.putInt(image.getHeight() * rowSize + 12); // Size of image stream

        // BITMAPCOREHEADER (a)
        header.putInt(12);

        header.putShort((short) image.getWidth());
        header.putShort((short) image.getHeight()); // Use -height if writing top-down

        header.putShort((short) 1); // planes, always 1
        header.putShort((short) 24); // bitcount

        output.write(header.array());

        // Output rows bottom-up (b)
        Raster raster = image.getRaster()
                             .createChild(0, 0, image.getWidth(), image.getHeight(), 0, 0, new int[]{2, 1, 0}); // Reverse BGR -> RGB (d)
        byte[] row = new byte[rowSize]; // padded (c)

        for (int i = image.getHeight() - 1; i >= 0; i--) {
            row = (byte[]) raster.getDataElements(0, i, image.getWidth(), 1, row);
            output.write(row);
        }

        return output.toByteArray();
    }

    public static void main(String[] args) throws Exception {
        HSSFWorkbook workbook = new HSSFWorkbook();
        HSSFSheet sheet = workbook.createSheet("Sheet2"); // this sheet gets the background image set

        // we need the binary records of the sheet
        // get InternalSheet
        Field _sheet = HSSFSheet.class.getDeclaredField("_sheet");
        _sheet.setAccessible(true);
        InternalSheet internalsheet = (InternalSheet)_sheet.get(sheet);

        // get List of RecordBase
        Field _records = InternalSheet.class.getDeclaredField("_records");
        _records.setAccessible(true);
        @SuppressWarnings("unchecked")
        List<RecordBase> records = (List<RecordBase>)_records.get(internalsheet);

        // get bytes of the image file
        byte[] data = getBackgroundBitmapData("dummy.png"); //PNG must not have transparency

        // do creating BitmapRecord and ContinueRecords from the data in parts of 8220 bytes
        BitmapRecord bitmapRecord;
        List<ContinueRecord> continueRecords = new ArrayList<>();
        int bytes;

        if (data.length > 8220) {
            bitmapRecord = new BitmapRecord(Arrays.copyOfRange(data, 0, 8220));
            bytes = 8220;
            while (bytes < data.length) {
                if ((bytes + 8220) < data.length) {
                    continueRecords.add(new ContinueRecord(Arrays.copyOfRange(data, bytes, bytes + 8220)));
                    bytes += 8220;
                } else {
                    continueRecords.add(new ContinueRecord(Arrays.copyOfRange(data, bytes, data.length)));
                    break;
                }
            }
        } else {
            bitmapRecord = new BitmapRecord(data);
        }

        // add the records after PageSettingsBlock
        int i = 0;
        for (RecordBase r : records) {
            if (r instanceof org.apache.poi.hssf.record.aggregates.PageSettingsBlock) {
                break;
            }
            i++;
        }
        records.add(++i, bitmapRecord);
        for (ContinueRecord continueRecord : continueRecords) {
            records.add(++i, continueRecord);
        }

        // debug output
        for (RecordBase r : internalsheet.getRecords()) {
            System.out.println(r);
        }

        // write out workbook
        workbook.write(new FileOutputStream("backgroundImage.xls"));
        workbook.close();

    }

    static class BitmapRecord extends StandardRecord {

        //see https://www.openoffice.org/sc/excelfileformat.pdf - BITMAP

        byte[] data;

        BitmapRecord(byte[] data) {
            this.data = data;
        }

        public int getDataSize() {
            return data.length;
        }

        public short getSid() {
            return (short)0x00E9;
        }

        public void serialize(LittleEndianOutput out) {
            out.write(data);
        }
    }

    static class ContinueRecord extends StandardRecord {

        //see https://www.openoffice.org/sc/excelfileformat.pdf - CONTINUE

        byte[] data;

        ContinueRecord(byte[] data) {
            this.data = data;
        }

        public int getDataSize() {
            return data.length;
        }

        public short getSid() {
            return (short)0x003C;
        }

        public void serialize(LittleEndianOutput out) {
            out.write(data);
        }
    }
}
0 голосов
/ 20 сентября 2018

Как часто это просто глупость самого программиста ;-).В немецком языке есть поговорка: «Я не могу видеть лес из-за деревьев».

Просто получить Raster из BufferedImage один раз за пределами цикла вместо того, чтобы поместить его внутрь цикла.и снова невероятно увеличивает скорость:

...
  // put the bytes R G B into the data; lines of the bitmap must be from bottom line to top line
  int bytes = 0;
  Raster raster = image.getData();
  for (short y = (short)(height - 1); y >= 0; y--) {
   for (short x = 0; x < width; x++) {
    int r = raster.getSample(x, y, 2);
    data.add(Byte.valueOf((byte)r));
    bytes++;
    int g = raster.getSample(x, y, 1);
    data.add(Byte.valueOf((byte)g));
    bytes++;
    int b = raster.getSample(x, y, 0);
    data.add(Byte.valueOf((byte)b));
    bytes++;
   } 
   // fill up x with 0 bytes up to multiple of 4
   for (int x = width * 3; x < widthBytesMultOf4; x++) {
    data.add(Byte.valueOf((byte)0));
    bytes++;
   }
  }
...

Но намек на @Gagravarr в его комментарии выглядит также интересно.

2.4.19 BkHim

Запись BkHim определяет данные изображения для фона листа (1)

cf (2 байта) : целое число со знаком, определяющее формат изображения,ДОЛЖНО быть значением из следующего:

Значение: 0x0009

Значение: формат растрового изображения.Данные изображения сохраняются в растровом формате, как описано в [MSDN-BMP]

Значение: 0x000E

Значение: собственный формат.Данные изображения хранятся в собственном формате другого приложения и не могут обрабатываться напрямую.

Звучит так, как будто бы 0x000E первые байты вместо 0x0009, а затем собственные байты изображения (PNG, JPG, BMP, ...) могут быть сохранены напрямую.Попробую это завтра.


Ну, как часто, только бесполезные усилия с «документациями» Microsoft.https://interoperability.blob.core.windows.net/files/MS-XLS/[MS-XLS].pdf кажется правильным, но неполным, по крайней мере на стр. 211: 2.4.19 BkHim.Так что использование 0x000E вместо 0x0009 не позволит просто сохранить нативный imageBlob.

Но также описание 0x0009:

Растровое изображение.Данные изображения хранятся в формате растрового изображения, как описано в [MSDN-BMP]

, только ссылки на неполное описание того, какой тип растрового изображения должен использоваться здесь.Ничего о структуре заголовка в начале и пиксельных байтах от нижней строки до верхней строки.А также ничего о необходимости выравнивания размера каждой строки с кратностью 4, вставляя нулевые байты после последнего пикселя.Но, не зная этого, даже использование растрового изображения 0x0009 не будет работать.

Когда я помещаю фоновое изображение любого типа в лист в файле *.xls с использованием графического интерфейса пользователя Excel, а затем получаюпосмотрите в этот файл, используя шестнадцатеричный дамп, тогда это всегда выглядит так:

0x e900 SSSS 09000100 SSSSSSSS 0c000000 WWWWHHHH 01001800 PPP ...

, где S означает размер, W означает ширину, H означает высоту и P означает пиксельные байты.

Это не зависит от того, поместил я BMP, JPG или PNGв качестве фонового изображения на листе.Всегда используется этот особый вид данных BMP.

Так что то, что OpenOffice распознал с помощью обратного инжиниринга в https://www.openoffice.org/sc/excelfileformat.pdf, более полезно, чем «документация» от Microsoft.

код для вставки фонового изображения в HSSFSheet работает у меня, протестировано с несколькими различными типами файлов изображений (BMP, PNG, JPG).

Также изменено с помощью List<Byte> data на byte[] data.Так что org.apache.poi.hssf.record.ContinueRecord может использоваться напрямую и не должен быть воссоздан.

import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ByteArrayOutputStream;

import org.apache.poi.hssf.usermodel.*;

import org.apache.poi.hssf.record.RecordBase;
import org.apache.poi.hssf.record.StandardRecord;
import org.apache.poi.hssf.record.ContinueRecord;
import org.apache.poi.hssf.model.InternalSheet;

import org.apache.poi.util.LittleEndianOutput;

import java.lang.reflect.Field;

import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.Graphics2D;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import javax.imageio.ImageIO;

public class CreateExcelHSSFSheetBackgroundBMP {

 static byte[] getBackgroundBitmapData(String filePath) throws Exception {

  // see https://www.openoffice.org/sc/excelfileformat.pdf - 5.6 BITMAP
  // and https://interoperability.blob.core.windows.net/files/MS-XLS/[MS-XLS].pdf - 2.4.19 BkHim

  // get file byte data in type BufferedImage.TYPE_3BYTE_BGR
  BufferedImage in = ImageIO.read(new FileInputStream(filePath));
  BufferedImage image = new BufferedImage(in.getWidth(), in.getHeight(), BufferedImage.TYPE_3BYTE_BGR);
  Graphics2D graphics = image.createGraphics();
  graphics.drawImage(in, null, 0, 0);
  graphics.dispose();

  short width = (short)image.getWidth();
  short height = (short)image.getHeight();

  // each pixel has 3 bytes but the width bytes must be filled up to multiple of 4
  int widthBytesMultOf4 = (int)((width * 3 + 3) / 4 * 4);

  // size 12 bytes (additional headers, see below) + picture bytes
  int size = 12 + height * widthBytesMultOf4;

  // create the header section
  ByteBuffer headers = ByteBuffer.allocate(20);
  headers.order(ByteOrder.LITTLE_ENDIAN);
  headers.putShort((short)0x09); // 0x0009 = signed integer that specifies the image format BMP
  headers.putShort((short)0x01); // reserved (2 bytes): MUST be 0x0001
  headers.putInt(size); // signed integer that specifies the size of imageBlob in bytes
  // BMP header section:
  headers.putInt(0x0C); // length 0x0C = 12 bytes
  headers.putShort(width); // pixels width
  headers.putShort(height); // pixels heigth
  headers.putShort((short)0x01); // number of planes: always 1
  headers.putShort((short)0x18); // color depth 0x018 = 24 bit

  //create data ByteArrayOutputStream
  ByteArrayOutputStream data = new ByteArrayOutputStream();

  // write headers section
  data.write(headers.array());

  // put the bytes R G B into the data; lines of the bitmap must be from bottom line to top line
  Raster raster = image.getData();
  for (short y = (short)(height - 1); y >= 0; y--) {
   for (short x = 0; x < width; x++) {
    int r = raster.getSample(x, y, 2);
    data.write((byte)r);
    int g = raster.getSample(x, y, 1);
    data.write((byte)g);
    int b = raster.getSample(x, y, 0);
    data.write((byte)b);
   } 
   // fill up x with 0 bytes up to multiple of 4
   for (int x = width * 3; x < widthBytesMultOf4; x++) {
    data.write((byte)0);
   }
  }

  return data.toByteArray();
 }

 public static void main(String[] args) throws Exception {

  HSSFWorkbook workbook = new HSSFWorkbook();
  HSSFSheet sheet = workbook.createSheet("Sheet1");
  sheet = workbook.createSheet("Sheet2"); // this sheet gets the background image set

  // we need the binary records of the sheet
  // get InternalSheet
  Field _sheet = HSSFSheet.class.getDeclaredField("_sheet");
  _sheet.setAccessible(true); 
  InternalSheet internalsheet = (InternalSheet)_sheet.get(sheet); 

  // get List of RecordBase
  Field _records = InternalSheet.class.getDeclaredField("_records");
  _records.setAccessible(true);
  @SuppressWarnings("unchecked") 
  List<RecordBase> records = (List<RecordBase>)_records.get(internalsheet);

  // get bytes of the image file
  byte[] data = getBackgroundBitmapData("dummyText.png"); //PNG must not have transparency

  // do creating BitmapRecord and ContinueRecords from the data in parts of 8220 bytes
  BitmapRecord bitmapRecord = null;
  List<ContinueRecord> continueRecords = new ArrayList<ContinueRecord>();
  int bytes = 0;
  if (data.length > 8220) {
   bitmapRecord = new BitmapRecord(Arrays.copyOfRange(data, 0, 8220));
   bytes = 8220;
   while (bytes < data.length) {
    if ((bytes + 8220) < data.length) {
     continueRecords.add(new ContinueRecord(Arrays.copyOfRange(data, bytes, bytes + 8220)));
     bytes += 8220;
    } else {
     continueRecords.add(new ContinueRecord(Arrays.copyOfRange(data, bytes, data.length)));
     break;
    }
   }
  } else {
   bitmapRecord = new BitmapRecord(data);
  }

  // add the records after PageSettingsBlock
  int i = 0;
  for (RecordBase r : records) {
   if (r instanceof org.apache.poi.hssf.record.aggregates.PageSettingsBlock) {
    break;
   }
   i++;
  }
  records.add(++i, bitmapRecord);
  for (ContinueRecord continueRecord : continueRecords) {
   records.add(++i, continueRecord);  
  }

  // debug output
  for (RecordBase r : internalsheet.getRecords()) {
   System.out.println(r.getClass());
  }

  // write out workbook
  FileOutputStream out = new FileOutputStream("CreateExcelHSSFSheetBackgroundBMP.xls");
  workbook.write(out);
  workbook.close();
  out.close();

 }

 static class BitmapRecord extends StandardRecord {

  // see https://www.openoffice.org/sc/excelfileformat.pdf - 5.6 BITMAP
  // and https://interoperability.blob.core.windows.net/files/MS-XLS/[MS-XLS].pdf - 2.4.19 BkHim

  byte[] data;

  BitmapRecord(byte[] data) {
   this.data = data;
  }

  public int getDataSize() { 
   return data.length; 
  }

  public short getSid() {
   return (short)0x00E9;
  }

  public void serialize(LittleEndianOutput out) {
   out.write(data);
  }
 }

}
...