001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.imaging.formats.bmp; 018 019import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes; 020import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes; 021import static org.apache.commons.imaging.common.BinaryFunctions.readByte; 022import static org.apache.commons.imaging.common.BinaryFunctions.readBytes; 023 024import java.awt.Dimension; 025import java.awt.image.BufferedImage; 026import java.io.ByteArrayOutputStream; 027import java.io.IOException; 028import java.io.InputStream; 029import java.io.OutputStream; 030import java.io.PrintWriter; 031import java.nio.ByteOrder; 032import java.util.ArrayList; 033import java.util.List; 034import java.util.logging.Level; 035import java.util.logging.Logger; 036 037import org.apache.commons.imaging.AbstractImageParser; 038import org.apache.commons.imaging.FormatCompliance; 039import org.apache.commons.imaging.ImageFormat; 040import org.apache.commons.imaging.ImageFormats; 041import org.apache.commons.imaging.ImageInfo; 042import org.apache.commons.imaging.ImagingException; 043import org.apache.commons.imaging.PixelDensity; 044import org.apache.commons.imaging.bytesource.ByteSource; 045import org.apache.commons.imaging.common.AbstractBinaryOutputStream; 046import org.apache.commons.imaging.common.ImageBuilder; 047import org.apache.commons.imaging.common.ImageMetadata; 048import org.apache.commons.imaging.palette.PaletteFactory; 049import org.apache.commons.imaging.palette.SimplePalette; 050 051public class BmpImageParser extends AbstractImageParser<BmpImagingParameters> { 052 053 private static final Logger LOGGER = Logger.getLogger(BmpImageParser.class.getName()); 054 055 private static final String DEFAULT_EXTENSION = ImageFormats.BMP.getDefaultExtension(); 056 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.BMP.getExtensions(); 057 private static final byte[] BMP_HEADER_SIGNATURE = { 0x42, 0x4d, }; 058 private static final int BI_RGB = 0; 059 private static final int BI_RLE4 = 2; 060 private static final int BI_RLE8 = 1; 061 private static final int BI_BITFIELDS = 3; 062 private static final int BITMAP_FILE_HEADER_SIZE = 14; 063 private static final int BITMAP_INFO_HEADER_SIZE = 40; 064 065 /** 066 * Constructs a new instance with the little-endian byte order. 067 */ 068 public BmpImageParser() { 069 super(ByteOrder.LITTLE_ENDIAN); 070 } 071 072 @Override 073 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException { 074 pw.println("bmp.dumpImageFile"); 075 076 final ImageInfo imageData = getImageInfo(byteSource, null); 077 078 imageData.toString(pw, ""); 079 080 pw.println(""); 081 082 return true; 083 } 084 085 @Override 086 protected String[] getAcceptedExtensions() { 087 return ACCEPTED_EXTENSIONS; 088 } 089 090 @Override 091 protected ImageFormat[] getAcceptedTypes() { 092 return new ImageFormat[] { ImageFormats.BMP }; 093 } 094 095 private String getBmpTypeDescription(final int identifier1, final int identifier2) { 096 if (identifier1 == 'B' && identifier2 == 'M') { 097 return "Windows 3.1x, 95, NT,"; 098 } 099 if (identifier1 == 'B' && identifier2 == 'A') { 100 return "OS/2 Bitmap Array"; 101 } 102 if (identifier1 == 'C' && identifier2 == 'I') { 103 return "OS/2 Color Icon"; 104 } 105 if (identifier1 == 'C' && identifier2 == 'P') { 106 return "OS/2 Color Pointer"; 107 } 108 if (identifier1 == 'I' && identifier2 == 'C') { 109 return "OS/2 Icon"; 110 } 111 if (identifier1 == 'P' && identifier2 == 'T') { 112 return "OS/2 Pointer"; 113 } 114 115 return "Unknown"; 116 } 117 118 @Override 119 public BufferedImage getBufferedImage(final ByteSource byteSource, final BmpImagingParameters params) throws ImagingException, IOException { 120 try (InputStream is = byteSource.getInputStream()) { 121 return getBufferedImage(is, params); 122 } 123 } 124 125 public BufferedImage getBufferedImage(final InputStream inputStream, final BmpImagingParameters params) throws ImagingException, IOException { 126 final BmpImageContents ic = readImageContents(inputStream, FormatCompliance.getDefault()); 127 128 final BmpHeaderInfo bhi = ic.bhi; 129 // byte[] colorTable = ic.colorTable; 130 // byte[] imageData = ic.imageData; 131 132 final int width = bhi.width; 133 final int height = bhi.height; 134 135 if (LOGGER.isLoggable(Level.FINE)) { 136 LOGGER.fine("width: " + width); 137 LOGGER.fine("height: " + height); 138 LOGGER.fine("width*height: " + width * height); 139 LOGGER.fine("width*height*4: " + width * height * 4); 140 } 141 142 final AbstractPixelParser abstractPixelParser = ic.abstractPixelParser; 143 final ImageBuilder imageBuilder = new ImageBuilder(width, height, true); 144 abstractPixelParser.processImage(imageBuilder); 145 146 return imageBuilder.getBufferedImage(); 147 148 } 149 150 @Override 151 public String getDefaultExtension() { 152 return DEFAULT_EXTENSION; 153 } 154 155 @Override 156 public BmpImagingParameters getDefaultParameters() { 157 return new BmpImagingParameters(); 158 } 159 160 @Override 161 public FormatCompliance getFormatCompliance(final ByteSource byteSource) throws ImagingException, IOException { 162 final FormatCompliance result = new FormatCompliance(byteSource.toString()); 163 164 try (InputStream is = byteSource.getInputStream()) { 165 readImageContents(is, result); 166 } 167 168 return result; 169 } 170 171 @Override 172 public byte[] getIccProfileBytes(final ByteSource byteSource, final BmpImagingParameters params) { 173 return null; 174 } 175 176 @Override 177 public ImageInfo getImageInfo(final ByteSource byteSource, final BmpImagingParameters params) throws ImagingException, IOException { 178 BmpImageContents ic = null; 179 try (InputStream is = byteSource.getInputStream()) { 180 ic = readImageContents(is, FormatCompliance.getDefault()); 181 } 182 183 final BmpHeaderInfo bhi = ic.bhi; 184 final byte[] colorTable = ic.colorTable; 185 186 if (bhi == null) { 187 throw new ImagingException("BMP: couldn't read header"); 188 } 189 190 final int height = bhi.height; 191 final int width = bhi.width; 192 193 final List<String> comments = new ArrayList<>(); 194 // TODO: comments... 195 196 final int bitsPerPixel = bhi.bitsPerPixel; 197 final ImageFormat format = ImageFormats.BMP; 198 final String name = "BMP Windows Bitmap"; 199 final String mimeType = "image/x-ms-bmp"; 200 // we ought to count images, but don't yet. 201 final int numberOfImages = -1; 202 // not accurate ... only reflects first 203 final boolean progressive = false; 204 // boolean progressive = (fPNGChunkIHDR.InterlaceMethod != 0); 205 // 206 // pixels per meter 207 final int physicalWidthDpi = (int) Math.round(bhi.hResolution * .0254); 208 final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi); 209 // int physicalHeightDpi = 72; 210 final int physicalHeightDpi = (int) Math.round(bhi.vResolution * .0254); 211 final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi); 212 213 final String formatDetails = "Bmp (" + (char) bhi.identifier1 + (char) bhi.identifier2 + ": " + getBmpTypeDescription(bhi.identifier1, bhi.identifier2) 214 + ")"; 215 216 final boolean transparent = false; 217 218 final boolean usesPalette = colorTable != null; 219 final ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB; 220 final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.RLE; 221 222 return new ImageInfo(formatDetails, bitsPerPixel, comments, format, name, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch, 223 physicalWidthDpi, physicalWidthInch, width, progressive, transparent, usesPalette, colorType, compressionAlgorithm); 224 } 225 226 @Override 227 public Dimension getImageSize(final ByteSource byteSource, final BmpImagingParameters params) throws ImagingException, IOException { 228 final BmpHeaderInfo bhi = readBmpHeaderInfo(byteSource); 229 230 return new Dimension(bhi.width, bhi.height); 231 232 } 233 234 @Override 235 public ImageMetadata getMetadata(final ByteSource byteSource, final BmpImagingParameters params) { 236 // TODO this should throw UnsupportedOperationException, but RoundtripTest has to be refactored completely before this can be changed 237 return null; 238 } 239 240 @Override 241 public String getName() { 242 return "Bmp-Custom"; 243 } 244 245 private byte[] getRleBytes(final InputStream is, final int rleSamplesPerByte) throws IOException { 246 final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 247 248 // this.setDebug(true); 249 250 boolean done = false; 251 while (!done) { 252 final int a = 0xff & readByte("RLE a", is, "BMP: Bad RLE"); 253 baos.write(a); 254 final int b = 0xff & readByte("RLE b", is, "BMP: Bad RLE"); 255 baos.write(b); 256 257 if (a == 0) { 258 switch (b) { 259 case 0: // EOL 260 break; 261 case 1: // EOF 262 // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 263 // ); 264 done = true; 265 break; 266 case 2: { 267 // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 268 // ); 269 final int c = 0xff & readByte("RLE c", is, "BMP: Bad RLE"); 270 baos.write(c); 271 final int d = 0xff & readByte("RLE d", is, "BMP: Bad RLE"); 272 baos.write(d); 273 274 } 275 break; 276 default: { 277 int size = b / rleSamplesPerByte; 278 if (b % rleSamplesPerByte > 0) { 279 size++; 280 } 281 if (size % 2 != 0) { 282 size++; 283 } 284 285 // System.out.println("b: " + b); 286 // System.out.println("size: " + size); 287 // System.out.println("RLESamplesPerByte: " + 288 // RLESamplesPerByte); 289 // System.out.println("xXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 290 // ); 291 final byte[] bytes = readBytes("bytes", is, size, "RLE: Absolute Mode"); 292 baos.write(bytes); 293 } 294 break; 295 } 296 } 297 } 298 299 return baos.toByteArray(); 300 } 301 302 private BmpHeaderInfo readBmpHeaderInfo(final ByteSource byteSource) throws ImagingException, IOException { 303 try (InputStream is = byteSource.getInputStream()) { 304 // readSignature(is); 305 return readBmpHeaderInfo(is, null); 306 } 307 } 308 309 private BmpHeaderInfo readBmpHeaderInfo(final InputStream is, final FormatCompliance formatCompliance) throws ImagingException, IOException { 310 final byte identifier1 = readByte("Identifier1", is, "Not a Valid BMP File"); 311 final byte identifier2 = readByte("Identifier2", is, "Not a Valid BMP File"); 312 313 if (formatCompliance != null) { 314 formatCompliance.compareBytes("Signature", BMP_HEADER_SIGNATURE, new byte[] { identifier1, identifier2 }); 315 } 316 317 final int fileSize = read4Bytes("File Size", is, "Not a Valid BMP File", getByteOrder()); 318 final int reserved = read4Bytes("Reserved", is, "Not a Valid BMP File", getByteOrder()); 319 final int bitmapDataOffset = read4Bytes("Bitmap Data Offset", is, "Not a Valid BMP File", getByteOrder()); 320 321 final int bitmapHeaderSize = read4Bytes("Bitmap Header Size", is, "Not a Valid BMP File", getByteOrder()); 322 int width = 0; 323 int height = 0; 324 int planes = 0; 325 int bitsPerPixel = 0; 326 int compression = 0; 327 int bitmapDataSize = 0; 328 int hResolution = 0; 329 int vResolution = 0; 330 int colorsUsed = 0; 331 int colorsImportant = 0; 332 int redMask = 0; 333 int greenMask = 0; 334 int blueMask = 0; 335 int alphaMask = 0; 336 int colorSpaceType = 0; 337 final BmpHeaderInfo.ColorSpace colorSpace = new BmpHeaderInfo.ColorSpace(); 338 colorSpace.red = new BmpHeaderInfo.ColorSpaceCoordinate(); 339 colorSpace.green = new BmpHeaderInfo.ColorSpaceCoordinate(); 340 colorSpace.blue = new BmpHeaderInfo.ColorSpaceCoordinate(); 341 int gammaRed = 0; 342 int gammaGreen = 0; 343 int gammaBlue = 0; 344 int intent = 0; 345 int profileData = 0; 346 int profileSize = 0; 347 int reservedV5 = 0; 348 349 if (bitmapHeaderSize < 40) { 350 throw new ImagingException("Invalid/unsupported BMP file"); 351 } 352 // BITMAPINFOHEADER 353 width = read4Bytes("Width", is, "Not a Valid BMP File", getByteOrder()); 354 height = read4Bytes("Height", is, "Not a Valid BMP File", getByteOrder()); 355 planes = read2Bytes("Planes", is, "Not a Valid BMP File", getByteOrder()); 356 bitsPerPixel = read2Bytes("Bits Per Pixel", is, "Not a Valid BMP File", getByteOrder()); 357 compression = read4Bytes("Compression", is, "Not a Valid BMP File", getByteOrder()); 358 bitmapDataSize = read4Bytes("Bitmap Data Size", is, "Not a Valid BMP File", getByteOrder()); 359 hResolution = read4Bytes("HResolution", is, "Not a Valid BMP File", getByteOrder()); 360 vResolution = read4Bytes("VResolution", is, "Not a Valid BMP File", getByteOrder()); 361 colorsUsed = read4Bytes("ColorsUsed", is, "Not a Valid BMP File", getByteOrder()); 362 colorsImportant = read4Bytes("ColorsImportant", is, "Not a Valid BMP File", getByteOrder()); 363 if (bitmapHeaderSize >= 52 || compression == BI_BITFIELDS) { 364 // 52 = BITMAPV2INFOHEADER, now undocumented 365 // see https://en.wikipedia.org/wiki/BMP_file_format 366 redMask = read4Bytes("RedMask", is, "Not a Valid BMP File", getByteOrder()); 367 greenMask = read4Bytes("GreenMask", is, "Not a Valid BMP File", getByteOrder()); 368 blueMask = read4Bytes("BlueMask", is, "Not a Valid BMP File", getByteOrder()); 369 } 370 if (bitmapHeaderSize >= 56) { 371 // 56 = the now undocumented BITMAPV3HEADER sometimes used by 372 // Photoshop 373 // see [BROKEN URL] http://forums.adobe.com/thread/751592?tstart=1 374 alphaMask = read4Bytes("AlphaMask", is, "Not a Valid BMP File", getByteOrder()); 375 } 376 if (bitmapHeaderSize >= 108) { 377 // BITMAPV4HEADER 378 colorSpaceType = read4Bytes("ColorSpaceType", is, "Not a Valid BMP File", getByteOrder()); 379 colorSpace.red.x = read4Bytes("ColorSpaceRedX", is, "Not a Valid BMP File", getByteOrder()); 380 colorSpace.red.y = read4Bytes("ColorSpaceRedY", is, "Not a Valid BMP File", getByteOrder()); 381 colorSpace.red.z = read4Bytes("ColorSpaceRedZ", is, "Not a Valid BMP File", getByteOrder()); 382 colorSpace.green.x = read4Bytes("ColorSpaceGreenX", is, "Not a Valid BMP File", getByteOrder()); 383 colorSpace.green.y = read4Bytes("ColorSpaceGreenY", is, "Not a Valid BMP File", getByteOrder()); 384 colorSpace.green.z = read4Bytes("ColorSpaceGreenZ", is, "Not a Valid BMP File", getByteOrder()); 385 colorSpace.blue.x = read4Bytes("ColorSpaceBlueX", is, "Not a Valid BMP File", getByteOrder()); 386 colorSpace.blue.y = read4Bytes("ColorSpaceBlueY", is, "Not a Valid BMP File", getByteOrder()); 387 colorSpace.blue.z = read4Bytes("ColorSpaceBlueZ", is, "Not a Valid BMP File", getByteOrder()); 388 gammaRed = read4Bytes("GammaRed", is, "Not a Valid BMP File", getByteOrder()); 389 gammaGreen = read4Bytes("GammaGreen", is, "Not a Valid BMP File", getByteOrder()); 390 gammaBlue = read4Bytes("GammaBlue", is, "Not a Valid BMP File", getByteOrder()); 391 } 392 if (bitmapHeaderSize >= 124) { 393 // BITMAPV5HEADER 394 intent = read4Bytes("Intent", is, "Not a Valid BMP File", getByteOrder()); 395 profileData = read4Bytes("ProfileData", is, "Not a Valid BMP File", getByteOrder()); 396 profileSize = read4Bytes("ProfileSize", is, "Not a Valid BMP File", getByteOrder()); 397 reservedV5 = read4Bytes("Reserved", is, "Not a Valid BMP File", getByteOrder()); 398 } 399 400 if (LOGGER.isLoggable(Level.FINE)) { 401 debugNumber("identifier1", identifier1, 1); 402 debugNumber("identifier2", identifier2, 1); 403 debugNumber("fileSize", fileSize, 4); 404 debugNumber("reserved", reserved, 4); 405 debugNumber("bitmapDataOffset", bitmapDataOffset, 4); 406 debugNumber("bitmapHeaderSize", bitmapHeaderSize, 4); 407 debugNumber("width", width, 4); 408 debugNumber("height", height, 4); 409 debugNumber("planes", planes, 2); 410 debugNumber("bitsPerPixel", bitsPerPixel, 2); 411 debugNumber("compression", compression, 4); 412 debugNumber("bitmapDataSize", bitmapDataSize, 4); 413 debugNumber("hResolution", hResolution, 4); 414 debugNumber("vResolution", vResolution, 4); 415 debugNumber("colorsUsed", colorsUsed, 4); 416 debugNumber("colorsImportant", colorsImportant, 4); 417 if (bitmapHeaderSize >= 52 || compression == BI_BITFIELDS) { 418 debugNumber("redMask", redMask, 4); 419 debugNumber("greenMask", greenMask, 4); 420 debugNumber("blueMask", blueMask, 4); 421 } 422 if (bitmapHeaderSize >= 56) { 423 debugNumber("alphaMask", alphaMask, 4); 424 } 425 if (bitmapHeaderSize >= 108) { 426 debugNumber("colorSpaceType", colorSpaceType, 4); 427 debugNumber("colorSpace.red.x", colorSpace.red.x, 1); 428 debugNumber("colorSpace.red.y", colorSpace.red.y, 1); 429 debugNumber("colorSpace.red.z", colorSpace.red.z, 1); 430 debugNumber("colorSpace.green.x", colorSpace.green.x, 1); 431 debugNumber("colorSpace.green.y", colorSpace.green.y, 1); 432 debugNumber("colorSpace.green.z", colorSpace.green.z, 1); 433 debugNumber("colorSpace.blue.x", colorSpace.blue.x, 1); 434 debugNumber("colorSpace.blue.y", colorSpace.blue.y, 1); 435 debugNumber("colorSpace.blue.z", colorSpace.blue.z, 1); 436 debugNumber("gammaRed", gammaRed, 4); 437 debugNumber("gammaGreen", gammaGreen, 4); 438 debugNumber("gammaBlue", gammaBlue, 4); 439 } 440 if (bitmapHeaderSize >= 124) { 441 debugNumber("intent", intent, 4); 442 debugNumber("profileData", profileData, 4); 443 debugNumber("profileSize", profileSize, 4); 444 debugNumber("reservedV5", reservedV5, 4); 445 } 446 } 447 448 return new BmpHeaderInfo(identifier1, identifier2, fileSize, reserved, bitmapDataOffset, bitmapHeaderSize, width, height, planes, bitsPerPixel, 449 compression, bitmapDataSize, hResolution, vResolution, colorsUsed, colorsImportant, redMask, greenMask, blueMask, alphaMask, colorSpaceType, 450 colorSpace, gammaRed, gammaGreen, gammaBlue, intent, profileData, profileSize, reservedV5); 451 } 452 453 private BmpImageContents readImageContents(final InputStream is, final FormatCompliance formatCompliance) throws ImagingException, IOException { 454 final BmpHeaderInfo bhi = readBmpHeaderInfo(is, formatCompliance); 455 456 int colorTableSize = bhi.colorsUsed; 457 if (colorTableSize == 0) { 458 colorTableSize = 1 << bhi.bitsPerPixel; 459 } 460 461 if (LOGGER.isLoggable(Level.FINE)) { 462 debugNumber("ColorsUsed", bhi.colorsUsed, 4); 463 debugNumber("BitsPerPixel", bhi.bitsPerPixel, 4); 464 debugNumber("ColorTableSize", colorTableSize, 4); 465 debugNumber("bhi.colorsUsed", bhi.colorsUsed, 4); 466 debugNumber("Compression", bhi.compression, 4); 467 } 468 469 // A palette is always valid, even for images that don't need it 470 // (like 32 bpp), it specifies the "optimal color palette" for 471 // when the image is displayed on a <= 256 color graphics card. 472 final int paletteLength; 473 int rleSamplesPerByte = 0; 474 boolean rle = false; 475 476 switch (bhi.compression) { 477 case BI_RGB: 478 if (LOGGER.isLoggable(Level.FINE)) { 479 LOGGER.fine("Compression: BI_RGB"); 480 } 481 if (bhi.bitsPerPixel <= 8) { 482 paletteLength = 4 * colorTableSize; 483 } else { 484 paletteLength = 0; 485 } 486 // BytesPerPaletteEntry = 0; 487 // System.out.println("Compression: BI_RGBx2: " + bhi.BitsPerPixel); 488 // System.out.println("Compression: BI_RGBx2: " + (bhi.BitsPerPixel 489 // <= 16)); 490 break; 491 492 case BI_RLE4: 493 if (LOGGER.isLoggable(Level.FINE)) { 494 LOGGER.fine("Compression: BI_RLE4"); 495 } 496 paletteLength = 4 * colorTableSize; 497 rleSamplesPerByte = 2; 498 // ExtraBitsPerPixel = 4; 499 rle = true; 500 // // BytesPerPixel = 2; 501 // // BytesPerPaletteEntry = 0; 502 break; 503 // 504 case BI_RLE8: 505 if (LOGGER.isLoggable(Level.FINE)) { 506 LOGGER.fine("Compression: BI_RLE8"); 507 } 508 paletteLength = 4 * colorTableSize; 509 rleSamplesPerByte = 1; 510 // ExtraBitsPerPixel = 8; 511 rle = true; 512 // BytesPerPixel = 2; 513 // BytesPerPaletteEntry = 0; 514 break; 515 // 516 case BI_BITFIELDS: 517 if (LOGGER.isLoggable(Level.FINE)) { 518 LOGGER.fine("Compression: BI_BITFIELDS"); 519 } 520 if (bhi.bitsPerPixel <= 8) { 521 paletteLength = 4 * colorTableSize; 522 } else { 523 paletteLength = 0; 524 } 525 // BytesPerPixel = 2; 526 // BytesPerPaletteEntry = 4; 527 break; 528 529 default: 530 throw new ImagingException("BMP: Unknown Compression: " + bhi.compression); 531 } 532 533 if (paletteLength < 0) { 534 throw new ImagingException("BMP: Invalid negative palette length: " + paletteLength); 535 } 536 537 byte[] colorTable = null; 538 if (paletteLength > 0) { 539 colorTable = readBytes("ColorTable", is, paletteLength, "Not a Valid BMP File"); 540 } 541 542 if (LOGGER.isLoggable(Level.FINE)) { 543 debugNumber("paletteLength", paletteLength, 4); 544 LOGGER.fine("ColorTable: " + (colorTable == null ? "null" : Integer.toString(colorTable.length))); 545 } 546 547 int imageLineLength = (bhi.bitsPerPixel * bhi.width + 7) / 8; 548 549 if (LOGGER.isLoggable(Level.FINE)) { 550 final int pixelCount = bhi.width * bhi.height; 551 // this.debugNumber("Total BitsPerPixel", 552 // (ExtraBitsPerPixel + bhi.BitsPerPixel), 4); 553 // this.debugNumber("Total Bit Per Line", 554 // ((ExtraBitsPerPixel + bhi.BitsPerPixel) * bhi.Width), 4); 555 // this.debugNumber("ExtraBitsPerPixel", ExtraBitsPerPixel, 4); 556 debugNumber("bhi.Width", bhi.width, 4); 557 debugNumber("bhi.Height", bhi.height, 4); 558 debugNumber("ImageLineLength", imageLineLength, 4); 559 // this.debugNumber("imageDataSize", imageDataSize, 4); 560 debugNumber("PixelCount", pixelCount, 4); 561 } 562 // int ImageLineLength = BytesPerPixel * bhi.Width; 563 while (imageLineLength % 4 != 0) { 564 imageLineLength++; 565 } 566 567 final int headerSize = BITMAP_FILE_HEADER_SIZE + bhi.bitmapHeaderSize + (bhi.bitmapHeaderSize == 40 && bhi.compression == BI_BITFIELDS ? 3 * 4 : 0); 568 final int expectedDataOffset = headerSize + paletteLength; 569 570 if (LOGGER.isLoggable(Level.FINE)) { 571 debugNumber("bhi.BitmapDataOffset", bhi.bitmapDataOffset, 4); 572 debugNumber("expectedDataOffset", expectedDataOffset, 4); 573 } 574 final int extraBytes = bhi.bitmapDataOffset - expectedDataOffset; 575 if (extraBytes < 0 || extraBytes > bhi.fileSize) { 576 throw new ImagingException("BMP has invalid image data offset: " + bhi.bitmapDataOffset + " (expected: " + expectedDataOffset + ", paletteLength: " 577 + paletteLength + ", headerSize: " + headerSize + ")"); 578 } 579 if (extraBytes > 0) { 580 readBytes("BitmapDataOffset", is, extraBytes, "Not a Valid BMP File"); 581 } 582 583 final int imageDataSize = bhi.height * imageLineLength; 584 585 if (LOGGER.isLoggable(Level.FINE)) { 586 debugNumber("imageDataSize", imageDataSize, 4); 587 } 588 589 final byte[] imageData; 590 if (rle) { 591 imageData = getRleBytes(is, rleSamplesPerByte); 592 } else { 593 imageData = readBytes("ImageData", is, imageDataSize, "Not a Valid BMP File"); 594 } 595 596 if (LOGGER.isLoggable(Level.FINE)) { 597 debugNumber("ImageData.length", imageData.length, 4); 598 } 599 600 final AbstractPixelParser abstractPixelParser; 601 switch (bhi.compression) { 602 case BI_RLE4: 603 case BI_RLE8: 604 abstractPixelParser = new PixelParserRle(bhi, colorTable, imageData); 605 break; 606 case BI_RGB: 607 abstractPixelParser = new PixelParserRgb(bhi, colorTable, imageData); 608 break; 609 case BI_BITFIELDS: 610 abstractPixelParser = new PixelParserBitFields(bhi, colorTable, imageData); 611 break; 612 default: 613 throw new ImagingException("BMP: Unknown Compression: " + bhi.compression); 614 } 615 616 return new BmpImageContents(bhi, colorTable, imageData, abstractPixelParser); 617 } 618 619 @Override 620 public void writeImage(final BufferedImage src, final OutputStream os, BmpImagingParameters params) throws ImagingException, IOException { 621 if (params == null) { 622 params = new BmpImagingParameters(); 623 } 624 final PixelDensity pixelDensity = params.getPixelDensity(); 625 626 final SimplePalette palette = new PaletteFactory().makeExactRgbPaletteSimple(src, 256); 627 628 final BmpWriter writer; 629 if (palette == null) { 630 writer = new BmpWriterRgb(); 631 } else { 632 writer = new BmpWriterPalette(palette); 633 } 634 635 final byte[] imageData = writer.getImageData(src); 636 @SuppressWarnings("resource") // Caller closes 'os'. 637 final AbstractBinaryOutputStream bos = AbstractBinaryOutputStream.littleEndian(os); 638 639 // write BitmapFileHeader 640 os.write(0x42); // B, Windows 3.1x, 95, NT, Bitmap 641 os.write(0x4d); // M 642 643 final int fileSize = BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE + // header size 644 4 * writer.getPaletteSize() + // palette size in bytes 645 imageData.length; 646 bos.write4Bytes(fileSize); 647 648 bos.write4Bytes(0); // reserved 649 bos.write4Bytes(BITMAP_FILE_HEADER_SIZE + BITMAP_INFO_HEADER_SIZE + 4 * writer.getPaletteSize()); // Bitmap Data Offset 650 651 final int width = src.getWidth(); 652 final int height = src.getHeight(); 653 654 // write BitmapInfoHeader 655 bos.write4Bytes(BITMAP_INFO_HEADER_SIZE); // Bitmap Info Header Size 656 bos.write4Bytes(width); // width 657 bos.write4Bytes(height); // height 658 bos.write2Bytes(1); // Number of Planes 659 bos.write2Bytes(writer.getBitsPerPixel()); // Bits Per Pixel 660 661 bos.write4Bytes(BI_RGB); // Compression 662 bos.write4Bytes(imageData.length); // Bitmap Data Size 663 bos.write4Bytes(pixelDensity != null ? (int) Math.round(pixelDensity.horizontalDensityMetres()) : 0); // HResolution 664 bos.write4Bytes(pixelDensity != null ? (int) Math.round(pixelDensity.verticalDensityMetres()) : 0); // VResolution 665 if (palette == null) { 666 bos.write4Bytes(0); // Colors 667 } else { 668 bos.write4Bytes(palette.length()); // Colors 669 } 670 bos.write4Bytes(0); // Important Colors 671 // bos.write_4_bytes(0); // Compression 672 673 // write Palette 674 writer.writePalette(bos); 675 // write Image Data 676 bos.write(imageData); 677 } 678}