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