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