4 votes

En utilisant java.awt.image.BufferedImage pour créer un enregistrement BIFF8 BITMAP prend beaucoup de temps - y a-t-il une meilleure approche ?

Donc, je crée une HSSFSheet ayant un fond bitmap défini en utilisant apache poi et un code de bas niveau. Le https://www.openoffice.org/sc/excelfileformat.pdf déclare pour le Record BITMAP, BIFF8:

Données de pixels (tableau de lignes de hauteur du bitmap, de la ligne inférieure à la ligne supérieure, voir ci-dessous)

...

Dans chaque ligne, tous les pixels sont écrits de gauche à droite. Chaque pixel est stocké sous forme d'un tableau de 3 octets : composant rouge, vert et bleu de la couleur du pixel, dans cet ordre. La taille de chaque ligne est alignée sur des multiples de 4 en insérant des octets nuls après le dernier pixel.

Voir l'image du PDF pour la déclaration complète : entrez la description de l'image ici

Pour réaliser cela, j'utilise java.awt.image.BufferedImage ayant le type BufferedImage.TYPE_3BYTE_BGR. Ensuite, obtenir tous les octets R V B de ce raster BufferedImage dans le bon ordre (de la ligne inférieure à la ligne supérieure) et remplir jusqu'à un multiple de 4 en largeur (direction x).

Voir le code :

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 getBackgroundBitmapData(String filePath) throws Exception {

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

  List data = new ArrayList();

  // obtenir les données d'octets de fichier en 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();

  // chaque pixel a 3 octets mais les octets en largeur doivent être remplis jusqu'à un multiple de 4
  int widthBytesMultOf4 = (int)((width * 3 + 3) / 4 * 4);

// --- cette partie prend beaucoup de temps mais je n'ai pas trouvé de meilleure possibilité

  // mettre les octets RVB dans les données ; les lignes du bitmap doivent être de la ligne inférieure à la ligne supérieure
  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++;
   } 
   // remplir x avec des octets 0 jusqu'à un multiple de 4
   for (int x = width * 3; x < widthBytesMultOf4; x++) {
    data.add(Byte.valueOf((byte)0));
    bytes++;
   }
  }

// ---

  // taille  12 octets (entêtes supplémentaires, voir ci-dessous) + octets de l'image
  int size = 12 + bytes;

  // obtenir la taille int en tant qu'octets LITTLE_ENDIAN
  ByteBuffer bSize = ByteBuffer.allocate(4);
  bSize.order(ByteOrder.LITTLE_ENDIAN);
  bSize.putInt(size);

  // obtenir la largeur short en tant qu'octets LITTLE_ENDIAN
  ByteBuffer bWidth = ByteBuffer.allocate(2);
  bWidth.order(ByteOrder.LITTLE_ENDIAN);
  bWidth.putShort(width);

  // obtenir la hauteur short en tant qu'octets LITTLE_ENDIAN
  ByteBuffer bHeight = ByteBuffer.allocate(2);
  bHeight.order(ByteOrder.LITTLE_ENDIAN);
  bHeight.putShort(height);

  // mettre les en-têtes de l'enregistrement dans les données
  Byte[] dataPart = new Byte[] { 0x09, 0x00, 0x01, 0x00, 
     bSize.array()[0], bSize.array()[1], bSize.array()[2], bSize.array()[3], // taille
     //maintenant 12 octets suivent
     0x0C, 0x00, 0x00, 0x00, 
     bWidth.array()[0], bWidth.array()[1], // largeur
     bHeight.array()[0], bHeight.array()[1], // hauteur
     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"); // cette feuille reçoit l'image de fond définie

  // nous avons besoin des enregistrements binaires de la feuille
  // obtenir InternalSheet
  Field _sheet = HSSFSheet.class.getDeclaredField("_sheet");
  _sheet.setAccessible(true); 
  InternalSheet internalsheet = (InternalSheet)_sheet.get(sheet); 

  // obtenir la liste de RecordBase
  Field _records = InternalSheet.class.getDeclaredField("_records");
  _records.setAccessible(true);
  @SuppressWarnings("unchecked") 
  List records = (List)_records.get(internalsheet);

  // obtenir les octets du fichier image
  List data = getBackgroundBitmapData("dummyText.png"); // PNG ne doit pas avoir de transparence

  // créer BitmapRecord et ContinueRecords à partir des données en parties de 8220 octets
  BitmapRecord bitmapRecord = null;
  List continueRecords = new ArrayList();
  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);
  }

  // ajouter les enregistrements après 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);  
  }

  // sortie de débogage
  for (RecordBase r : internalsheet.getRecords()) {
   System.out.println(r);
  }

  // écrire le classeur
  workbook.write(new FileOutputStream("CreateExcelHSSFSheetBackgroundBitmap.xls"));
  workbook.close();

 }

 static class BitmapRecord extends StandardRecord {

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

  List data = new ArrayList();

  BitmapRecord(List 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 {

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

  List data = new ArrayList();

  ContinueRecord(List 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);
   }
  }
 }

}

Le code fonctionne mais la partie entre

// --- cette partie prend beaucoup de temps mais je n'ai pas trouvé de meilleure possibilité

et

// ---

prend beaucoup de temps car 3 octets RVB pour chaque pixel individuel doivent être obtenus pour les obtenir selon le format étrange ci-dessus.

Est-ce que quelqu'un connaît une meilleure approche ? Peut-être que le format étrange ci-dessus n'est pas aussi étrange que je le pense et qu'il est déjà utilisé autre part ?

2voto

haraldK Points 5751

Voici une version modifiée de votre code qui fonctionne pour moi, ET est assez rapide.

  1. J'utilise byte[] (et ByteArrayOutputStream) partout, plus de List.
  2. Comme nous avons déjà un BufferedImage de TYPE_3BYTE_BGR, nous pouvons l'utiliser presque directement comme sortie BMP. Nous devons simplement a) ajouter un en-tête BMP valide et b) écrire de bas en haut, c) remplir chaque ligne de balayage (rangée) à une limite de 32 bits et d) changer l'ordre BGR -> RGB.
  3. J'utilise le Raster pour copier les rangées de données (remplies) dans la sortie, car copier des morceaux plus grands est plus rapide que copier des octets un par un.

Comme déjà mentionné dans les commentaires, la structure est un BMP standard avec BITMAPCOREHEADER (et pas d'en-tête de fichier). Malheureusement, le ImageIO BMPImageWriter écrit toujours l'en-tête du fichier et utilise le BITMAPINFOHEADER qui fait 40 octets. Vous pourriez probablement contourner ces choses, et utiliser l'écrivain standard, en modifiant un peu les données (indice : l'en-tête de fichier contient un décalage vers les données de pixel à l'offset 10), mais comme le format de base BMP est trivial à implémenter, cela pourrait être aussi facile à faire comme ci-dessous.

Alors que la documentation implique certainement qu'utiliser d'autres formats comme PNG et JPEG directement, je n'ai pas réussi à le faire correctement.

Il y a probablement encore de la place pour amélioration si vous le souhaitez, pour éviter certaines copies de tableaux d'octets (c'est-à-dire utiliser offset/length et passer le tableau de données entier aux Bitmap/ContinueRecords au lieu de Arrays.copyOfRange()).

Code :

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 {

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

        // obtenir les données d'octets du fichier en 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();

        // calculer la taille de la rangée (c)
        int rowSize = ((24 * image.getWidth() + 31) / 32) * 4;

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

        // mettre les en-têtes d'enregistrement dans les données
        ByteBuffer header = ByteBuffer.allocate(8 + 12);
        header.order(ByteOrder.LITTLE_ENDIAN);

        // Choses XLS non documentées
        header.putShort((short) 0x09);
        header.putShort((short) 0x01);
        header.putInt(image.getHeight() * rowSize + 12); // Taille de flux d'image

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

        header.putShort((short) image.getWidth());
        header.putShort((short) image.getHeight()); // Utiliser -height si écrit de haut en bas

        header.putShort((short) 1); // plans, toujours 1
        header.putShort((short) 24); // bitcount

        output.write(header.array());

        // Rendu des lignes de bas en haut (b)
        Raster raster = image.getRaster()
                             .createChild(0, 0, image.getWidth(), image.getHeight(), 0, 0, new int[]{2, 1, 0}); // Inverser BGR -> RGB (d)
        byte[] row = new byte[rowSize]; // rembourré (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"); // cette feuille obtient l'image d'arrière-plan

        // nous avons besoin des enregistrements binaires de la feuille
        // obtenir InternalSheet
        Field _sheet = HSSFSheet.class.getDeclaredField("_sheet");
        _sheet.setAccessible(true);
        InternalSheet internalsheet = (InternalSheet)_sheet.get(sheet);

        // obtenir la liste des RecordBase
        Field _records = InternalSheet.class.getDeclaredField("_records");
        _records.setAccessible(true);
        @SuppressWarnings("unchecked")
        List records = (List)_records.get(internalsheet);

        // obtenir les octets du fichier image
        byte[] data = getBackgroundBitmapData("dummy.png"); //PNG ne doit pas avoir de transparence

        // créer les BitmapRecord et ContinueRecords à partir des données en parties de 8220 octets
        BitmapRecord bitmapRecord;
        List 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);
        }

        // ajouter les enregistrements après 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);
        }

        // sortie de débogage
        for (RecordBase r : internalsheet.getRecords()) {
            System.out.println(r);
        }

        // écrire le classeur
        workbook.write(new FileOutputStream("imageFond.xls"));
        workbook.close();

    }

    static class BitmapRecord extends StandardRecord {

        //voir 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 {

        //voir 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);
        }
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X