001 /* 002 * BMPCodec 003 * 004 * Copyright (c) 2000, 2001, 2002, 2003, 2004 Marco Schmidt. 005 * All rights reserved. 006 */ 007 008 package net.sourceforge.jiu.codecs; 009 010 import java.io.DataInput; 011 import java.io.DataOutput; 012 import java.io.IOException; 013 import net.sourceforge.jiu.codecs.ImageCodec; 014 import net.sourceforge.jiu.codecs.InvalidFileStructureException; 015 import net.sourceforge.jiu.codecs.UnsupportedTypeException; 016 import net.sourceforge.jiu.codecs.WrongFileFormatException; 017 import net.sourceforge.jiu.data.BilevelImage; 018 import net.sourceforge.jiu.data.ByteChannelImage; 019 import net.sourceforge.jiu.data.Gray8Image; 020 import net.sourceforge.jiu.data.MemoryBilevelImage; 021 import net.sourceforge.jiu.data.MemoryPaletted8Image; 022 import net.sourceforge.jiu.data.MemoryRGB24Image; 023 import net.sourceforge.jiu.data.Paletted8Image; 024 import net.sourceforge.jiu.data.PixelImage; 025 import net.sourceforge.jiu.data.Palette; 026 import net.sourceforge.jiu.data.RGB24Image; 027 import net.sourceforge.jiu.data.RGBIndex; 028 import net.sourceforge.jiu.ops.MissingParameterException; 029 import net.sourceforge.jiu.ops.OperationFailedException; 030 import net.sourceforge.jiu.util.ArrayConverter; 031 032 /** 033 * A codec to read and write Windows BMP image files. 034 * <p> 035 * Typical file extensions are <code>.bmp</code> and <code>.rle</code> 036 * (the latter is only used for compressed files). 037 * <h3>Bounds</h3> 038 * <p> 039 * This codec supports the bounds concept for loading and saving. 040 * </p> 041 * <h3>Supported BMP types when loading</h3> 042 * <ul> 043 * <li>Bilevel, 1 bit per pixel, uncompressed. 044 * BMP supports palettes for bilevel images, but the content of that 045 * palette is ignored and 0 is considered black and 1 white. 046 * Any class implementing {@link net.sourceforge.jiu.data.BilevelImage} 047 * can be given to the codec and it will load the image to that object 048 * (if the image's resolution is sufficient). 049 * If no image object is given to the codec, a new 050 * {@link net.sourceforge.jiu.data.MemoryBilevelImage} will be created.</li> 051 * <li>Paletted, 4 bits per pixel, uncompressed or RLE4 compression. 052 * Both types are loaded to a {@link net.sourceforge.jiu.data.Paletted8Image} object. 053 * This requires 50 % more space than is necessary, but there is 054 * no dedicated 4 bit image data class in JIU.</li> 055 * <li>Paletted, 8 bits per pixel, uncompressed or RLE8 compression. 056 * Both types are loaded to a {@link net.sourceforge.jiu.data.Paletted8Image} object.</li> 057 * <li>RGB truecolor, 24 bits per pixel, uncompressed. 058 * This is loaded to a {@link net.sourceforge.jiu.data.RGB24Image} object.</li> 059 * </ul> 060 * There is no support for 16 bpp images or BI_BITFIELDS compression (for lack of test files). 061 * <p> 062 * <h3>Supported JIU image data classes when saving to BMP</h3> 063 * <ul> 064 * <li>{@link net.sourceforge.jiu.data.BilevelImage} objects are stored as 1 bit per pixel BMP files.</li> 065 * <li>{@link net.sourceforge.jiu.data.Gray8Image} and 066 * {@link net.sourceforge.jiu.data.Paletted8Image} objects are stored as 067 * paletted 8 bits per pixel files. 068 * It doesn't really matter how many entries the palette has, the BMP file's 069 * palette will always have 256 entries, filled up with zero entries if necessary.</li> 070 * <li>{@link net.sourceforge.jiu.data.RGB24Image} objects are stored as 24 bpp BMP files.</li> 071 * </ul> 072 * There is no support for compressed BMP files when saving. 073 * <p> 074 * <h3>I/O classes</h3> 075 * BMPCodec works with all input and output classes supported by ImageCodec 076 * ({@link java.io.InputStream}, {@link java.io.OutputStream}, 077 * {@link java.io.DataInput}, {@link java.io.DataOutput}, 078 * {@link java.io.RandomAccessFile}). 079 * <h3>Problems</h3> 080 * <p>The RLE-compressed BMP files that I could test this codec on seem to 081 * have an end-of-line code at the end of every line instead of relying 082 * on the decoder to know when it has unpacked enough bytes for a line. 083 * Whenever this codec encounters an EOL symbol and has a current column 084 * value of <code>0</code>, the EOL is ignored. 085 * <h3>Usage examples</h3> 086 * Write an image to a BMP file. 087 * <pre> 088 * BMPCodec codec = new BMPCodec(); 089 * codec.setImage(image); 090 * codec.setFile("out.bmp", CodecMode.SAVE); 091 * codec.process(); 092 * codec.close(); 093 * </pre> 094 * Read an image from a BMP file. 095 * <pre> 096 * BMPCodec codec = new BMPCodec(); 097 * codec.setFile("image.bmp", CodecMode.LOAD); 098 * codec.process(); 099 * codec.close(); 100 * PixelImage image = codec.getImage(); 101 * </pre> 102 * @author Marco Schmidt 103 * @since 0.7.0 104 */ 105 public class BMPCodec extends ImageCodec 106 { 107 private int colorDepth; 108 private int compression; 109 private int dataOffset; 110 private int imageHeight; 111 private int imageWidth; 112 private DataInput in; 113 private DataOutput out; 114 private Palette palette; 115 116 public String[] getFileExtensions() 117 { 118 return new String[] {".bmp", ".rle"}; 119 } 120 121 public String getFormatName() 122 { 123 return "Windows BMP"; 124 } 125 126 public String[] getMimeTypes() 127 { 128 return new String[] {"image/bmp", "image/x-ms-bmp"}; 129 } 130 131 public boolean isLoadingSupported() 132 { 133 return true; 134 } 135 136 public boolean isSavingSupported() 137 { 138 return true; 139 } 140 141 private void load() throws 142 MissingParameterException, 143 OperationFailedException, 144 UnsupportedTypeException, 145 WrongFileFormatException 146 { 147 in = getInputAsDataInput(); 148 if (in == null) 149 { 150 throw new MissingParameterException("Input stream / random access file parameter missing."); 151 } 152 // now write the output stream 153 try 154 { 155 loadHeader(); 156 loadStream(); 157 } 158 catch (IOException ioe) 159 { 160 // wrap any I/O failures in an OperationFailedException 161 throw new OperationFailedException("I/O failure: " + ioe.toString()); 162 } 163 } 164 165 private void loadCompressedPaletted4Stream() throws IOException 166 { 167 Paletted8Image image = (Paletted8Image)getImage(); 168 int imageBytesPerRow = imageWidth; 169 int bytesPerRow = imageBytesPerRow; 170 int mod = bytesPerRow % 4; 171 if (mod != 0) 172 { 173 bytesPerRow += 4 - mod; 174 } 175 final int COLUMNS = getBoundsWidth(); 176 final int ROWS = imageHeight - getBoundsY1(); 177 final int X1 = getBoundsX1(); 178 int processedRows = 0; 179 byte[] row = new byte[bytesPerRow]; 180 int x = 0; 181 int y = imageHeight - 1; 182 boolean endOfBitmap = false; 183 boolean delta = false; 184 int newX = 0; 185 int newY = 0; 186 while (processedRows < ROWS) 187 { 188 int v1 = in.readUnsignedByte(); 189 int v2 = in.readUnsignedByte(); 190 if (v1 == 0) 191 { 192 switch(v2) 193 { 194 case(0): 195 { 196 // end of line 197 if (x != 0) 198 { 199 x = bytesPerRow; 200 } 201 break; 202 } 203 case(1): 204 { 205 // end of bitmap 206 x = bytesPerRow; 207 endOfBitmap = true; 208 break; 209 } 210 case(2): 211 { 212 // delta 213 delta = true; 214 newX = x + in.readUnsignedByte(); 215 newY = y - in.readUnsignedByte(); 216 x = bytesPerRow; 217 break; 218 } 219 default: 220 { 221 // copy the next v2 (3..255) samples from file to output 222 // two samples are packed into one byte 223 // if the number of bytes used to pack is not a multiple of 2, 224 // an additional padding byte is in the stream and must be skipped 225 boolean paddingByte = (((v2 + 1) / 2) % 2) != 0; 226 while (v2 > 1) 227 { 228 int packed = in.readUnsignedByte(); 229 int sample1 = (packed >> 4) & 0x0f; 230 int sample2 = packed & 0x0f; 231 row[x++] = (byte)sample1; 232 row[x++] = (byte)sample2; 233 v2 -= 2; 234 } 235 if (v2 == 1) 236 { 237 int packed = in.readUnsignedByte(); 238 int sample = (packed >> 4) & 0x0f; 239 row[x++] = (byte)sample; 240 } 241 if (paddingByte) 242 { 243 v2 = in.readUnsignedByte(); 244 } 245 break; 246 } 247 } 248 } 249 else 250 { 251 // rle: replicate the two samples in v2 as many times as v1 says 252 byte sample1 = (byte)((v2 >> 4) & 0x0f); 253 byte sample2 = (byte)(v2 & 0x0f); 254 while (v1 > 1) 255 { 256 row[x++] = sample1; 257 row[x++] = sample2; 258 v1 -= 2; 259 } 260 if (v1 == 1) 261 { 262 row[x++] = sample1; 263 } 264 } 265 // end of line? 266 if (x == bytesPerRow) 267 { 268 if (y <= getBoundsY2()) 269 { 270 image.putByteSamples(0, 0, y - getBoundsY1(), COLUMNS, 1, row, X1); 271 } 272 if (delta) 273 { 274 x = newX; 275 y = newY; 276 } 277 else 278 { 279 x = 0; 280 y--; 281 } 282 if (endOfBitmap) 283 { 284 processedRows = ROWS - 1; 285 } 286 setProgress(processedRows, ROWS); 287 processedRows++; 288 delta = false; 289 } 290 } 291 } 292 293 private void loadCompressedPaletted8Stream() throws IOException 294 { 295 Paletted8Image image = (Paletted8Image)getImage(); 296 int imageBytesPerRow = imageWidth; 297 int bytesPerRow = imageBytesPerRow; 298 int mod = bytesPerRow % 4; 299 if (mod != 0) 300 { 301 bytesPerRow += 4 - mod; 302 } 303 final int COLUMNS = getBoundsWidth(); 304 final int ROWS = imageHeight - getBoundsY1(); 305 final int X1 = getBoundsX1(); 306 int processedRows = 0; 307 byte[] row = new byte[bytesPerRow]; 308 int x = 0; 309 int y = imageHeight - 1; 310 boolean endOfBitmap = false; 311 boolean delta = false; 312 int newX = 0; 313 int newY = 0; 314 while (processedRows < ROWS) 315 { 316 int v1 = in.readUnsignedByte(); 317 int v2 = in.readUnsignedByte(); 318 if (v1 == 0) 319 { 320 switch(v2) 321 { 322 case(0): 323 { 324 // end of line 325 if (x != 0) 326 { 327 x = bytesPerRow; 328 } 329 break; 330 } 331 case(1): 332 { 333 // end of bitmap 334 x = bytesPerRow; 335 endOfBitmap = true; 336 break; 337 } 338 case(2): 339 { 340 // delta 341 delta = true; 342 newX = x + in.readUnsignedByte(); 343 newY = y - in.readUnsignedByte(); 344 x = bytesPerRow; 345 break; 346 } 347 default: 348 { 349 // copy the next v2 (3..255) bytes from file to output 350 boolean paddingByte = (v2 % 2) != 0; 351 while (v2-- > 0) 352 { 353 row[x++] = (byte)in.readUnsignedByte(); 354 } 355 if (paddingByte) 356 { 357 v2 = in.readUnsignedByte(); 358 } 359 break; 360 } 361 } 362 } 363 else 364 { 365 // rle: replicate v2 as many times as v1 says 366 byte value = (byte)v2; 367 while (v1-- > 0) 368 { 369 row[x++] = value; 370 } 371 } 372 // end of line? 373 if (x == bytesPerRow) 374 { 375 if (y <= getBoundsY2()) 376 { 377 image.putByteSamples(0, 0, y - getBoundsY1(), COLUMNS, 1, row, X1); 378 } 379 if (delta) 380 { 381 x = newX; 382 y = newY; 383 } 384 else 385 { 386 x = 0; 387 y--; 388 } 389 if (endOfBitmap) 390 { 391 processedRows = ROWS - 1; 392 } 393 setProgress(processedRows, ROWS); 394 processedRows++; 395 delta = false; 396 } 397 } 398 } 399 400 private void loadHeader() throws 401 IOException, 402 MissingParameterException, 403 OperationFailedException, 404 UnsupportedTypeException, 405 WrongFileFormatException 406 { 407 byte[] header = new byte[54]; 408 in.readFully(header); 409 if (header[0] != 'B' || header[1] != 'M') 410 { 411 throw new WrongFileFormatException("Not a BMP file (first two bytes are not 0x42 0x4d)."); 412 } 413 dataOffset = ArrayConverter.getIntLE(header, 0x0a); 414 if (dataOffset < 54) 415 { 416 throw new InvalidFileStructureException("BMP data expected to be 54dec or larger, got " + dataOffset); 417 } 418 imageWidth = ArrayConverter.getIntLE(header, 0x12); 419 imageHeight = ArrayConverter.getIntLE(header, 0x16); 420 if (imageWidth < 1 || imageHeight < 1) 421 { 422 throw new InvalidFileStructureException("BMP image width and height must be larger than 0, got " + imageWidth + " x " + imageHeight); 423 } 424 int planes = ArrayConverter.getShortLE(header, 0x1a); 425 if (planes != 1) 426 { 427 throw new InvalidFileStructureException("Can only handle BMP number of planes = 1, got " + planes); 428 } 429 colorDepth = ArrayConverter.getShortLE(header, 0x1c); 430 if (colorDepth != 1 && colorDepth != 4 && colorDepth != 8 && colorDepth != 24) 431 { 432 // TO DO: add support for 16 bpp BMP reading 433 throw new InvalidFileStructureException("Unsupported BMP color depth: " + colorDepth); 434 } 435 compression = ArrayConverter.getIntLE(header, 0x1e); 436 if (compression != 0 && !(compression == 1 && colorDepth == 8) && !(compression == 2 && colorDepth == 4)) 437 { 438 throw new InvalidFileStructureException("Unsupported BMP compression type / color depth combination: " + 439 compression + " / " + colorDepth); 440 } 441 float dpiXValue = ArrayConverter.getIntLE(header, 0x26) / (100.0f / 2.54f); 442 float dpiYValue = ArrayConverter.getIntLE(header, 0x2a) / (100.0f / 2.54f); 443 setDpi((int)dpiXValue, (int)dpiYValue); 444 } 445 446 private void loadStream() throws 447 IOException, 448 MissingParameterException, 449 OperationFailedException, 450 UnsupportedTypeException 451 { 452 // 1. check bounds, initialize them if necessary 453 setBoundsIfNecessary(imageWidth, imageHeight); 454 checkBounds(imageWidth, imageHeight); 455 // 2. read palette if the image isn't truecolor (even monochrome BMPs have a palette) 456 int bytesToSkip; 457 if (colorDepth <= 8) 458 { 459 int numPaletteEntries = 1 << colorDepth; 460 int expectedPaletteSize = 4 * numPaletteEntries; 461 int headerSpaceLeft = dataOffset - 54; 462 bytesToSkip = headerSpaceLeft - expectedPaletteSize; 463 if (bytesToSkip < 0) 464 { 465 throw new InvalidFileStructureException("Not enough space in header for palette with " + 466 numPaletteEntries + "entries."); 467 } 468 palette = new Palette(numPaletteEntries); 469 for (int index = 0; index < numPaletteEntries; index++) 470 { 471 int blue = in.readUnsignedByte(); 472 int green = in.readUnsignedByte(); 473 int red = in.readUnsignedByte(); 474 int filler = in.readUnsignedByte(); 475 palette.put(index, red, green, blue); 476 } 477 } 478 else 479 { 480 bytesToSkip = dataOffset - 54; 481 } 482 // 3. seek to beginning of image data 483 while (bytesToSkip > 0) 484 { 485 int skipped = in.skipBytes(bytesToSkip); 486 if (skipped > 0) 487 { 488 bytesToSkip -= skipped; 489 } 490 } 491 // 4. check if we have an image object that we are supposed to reuse 492 // if there is one, check if it has the correct type 493 // if there is none, create a new one 494 PixelImage image = getImage(); 495 if (image == null) 496 { 497 switch(colorDepth) 498 { 499 case(1): 500 { 501 setImage(new MemoryBilevelImage(getBoundsWidth(), getBoundsHeight())); 502 break; 503 } 504 case(4): 505 case(8): 506 { 507 setImage(new MemoryPaletted8Image(getBoundsWidth(), getBoundsHeight(), palette)); 508 break; 509 } 510 case(24): 511 { 512 setImage(new MemoryRGB24Image(getBoundsWidth(), getBoundsHeight())); 513 break; 514 } 515 // loadHeader would have thrown an exception for any other color depths 516 } 517 } 518 else 519 { 520 // TODO: check if image is of correct type 521 } 522 // now read actual image data 523 if (compression == 0) 524 { 525 loadUncompressedStream(); 526 } 527 else 528 if (compression == 1) 529 { 530 loadCompressedPaletted8Stream(); 531 } 532 else 533 if (compression == 2) 534 { 535 loadCompressedPaletted4Stream(); 536 } 537 } 538 539 private void loadUncompressedBilevelStream() throws 540 IOException, 541 OperationFailedException 542 { 543 if ((getBoundsX1() % 8) != 0) 544 { 545 throw new OperationFailedException("When loading bilevel images, horizontal X1 bounds must be a multiple of 8; got " + getBoundsX1()); 546 } 547 BilevelImage image = (BilevelImage)getImage(); 548 int imageBytesPerRow = (imageWidth + 7) / 8; 549 int bytesPerRow = imageBytesPerRow; 550 int mod = bytesPerRow % 4; 551 if (mod != 0) 552 { 553 bytesPerRow += 4 - mod; 554 } 555 int bottomRowsToSkip = imageHeight - 1 - getBoundsY2(); 556 int bytesToSkip = bottomRowsToSkip * bytesPerRow; 557 while (bytesToSkip > 0) 558 { 559 int skipped = in.skipBytes(bytesToSkip); 560 if (skipped > 0) 561 { 562 bytesToSkip -= skipped; 563 } 564 } 565 final int COLUMNS = getBoundsWidth(); 566 final int ROWS = getBoundsHeight(); 567 final int SRC_OFFSET = getBoundsX1() / 8; 568 final int SRC_BIT_OFFSET = getBoundsX1() % 8; 569 int y = image.getHeight() - 1; 570 int processedRows = 0; 571 byte[] row = new byte[bytesPerRow]; 572 while (processedRows < ROWS) 573 { 574 in.readFully(row); 575 image.putPackedBytes(0, y, COLUMNS, row, SRC_OFFSET, SRC_BIT_OFFSET); 576 y--; 577 setProgress(processedRows, ROWS); 578 processedRows++; 579 } 580 } 581 582 private void loadUncompressedPaletted4Stream() throws 583 IOException 584 { 585 Paletted8Image image = (Paletted8Image)getImage(); 586 int imageBytesPerRow = (imageWidth + 1) / 2; 587 int bytesPerRow = imageBytesPerRow; 588 int mod = bytesPerRow % 4; 589 if (mod != 0) 590 { 591 bytesPerRow += 4 - mod; 592 } 593 int bottomRowsToSkip = imageHeight - 1 - getBoundsY2(); 594 int bytesToSkip = bottomRowsToSkip * bytesPerRow; 595 while (bytesToSkip > 0) 596 { 597 int skipped = in.skipBytes(bytesToSkip); 598 if (skipped > 0) 599 { 600 bytesToSkip -= skipped; 601 } 602 } 603 final int COLUMNS = getBoundsWidth(); 604 final int ROWS = getBoundsHeight(); 605 final int X1 = getBoundsX1(); 606 int y = image.getHeight() - 1; 607 int processedRows = 0; 608 byte[] row = new byte[bytesPerRow]; 609 byte[] samples = new byte[bytesPerRow * 2]; 610 while (processedRows < ROWS) 611 { 612 in.readFully(row); 613 ArrayConverter.decodePacked4Bit(row, 0, samples, 0, row.length); 614 image.putByteSamples(0, 0, y, COLUMNS, 1, samples, X1); 615 y--; 616 setProgress(processedRows, ROWS); 617 processedRows++; 618 } 619 } 620 621 private void loadUncompressedPaletted8Stream() throws IOException 622 { 623 Paletted8Image image = (Paletted8Image)getImage(); 624 int imageBytesPerRow = imageWidth; 625 int bytesPerRow = imageBytesPerRow; 626 int mod = bytesPerRow % 4; 627 if (mod != 0) 628 { 629 bytesPerRow += 4 - mod; 630 } 631 int bottomRowsToSkip = imageHeight - 1 - getBoundsY2(); 632 int bytesToSkip = bottomRowsToSkip * bytesPerRow; 633 while (bytesToSkip > 0) 634 { 635 int skipped = in.skipBytes(bytesToSkip); 636 if (skipped > 0) 637 { 638 bytesToSkip -= skipped; 639 } 640 } 641 final int COLUMNS = getBoundsWidth(); 642 final int ROWS = getBoundsHeight(); 643 final int X1 = getBoundsX1(); 644 int y = image.getHeight() - 1; 645 int processedRows = 0; 646 byte[] row = new byte[bytesPerRow]; 647 while (processedRows < ROWS) 648 { 649 in.readFully(row); 650 image.putByteSamples(0, 0, y, COLUMNS, 1, row, X1); 651 y--; 652 setProgress(processedRows, ROWS); 653 processedRows++; 654 } 655 } 656 657 private void loadUncompressedRgb24Stream() throws IOException 658 { 659 RGB24Image image = (RGB24Image)getImage(); 660 int imageBytesPerRow = imageWidth * 3; 661 int bytesPerRow = imageBytesPerRow; 662 int mod = bytesPerRow % 4; 663 if (mod != 0) 664 { 665 bytesPerRow += 4 - mod; 666 } 667 int bottomRowsToSkip = imageHeight - 1 - getBoundsY2(); 668 int bytesToSkip = bottomRowsToSkip * bytesPerRow; 669 while (bytesToSkip > 0) 670 { 671 int skipped = in.skipBytes(bytesToSkip); 672 if (skipped > 0) 673 { 674 bytesToSkip -= skipped; 675 } 676 } 677 final int COLUMNS = getBoundsWidth(); 678 final int ROWS = getBoundsHeight(); 679 final int X1 = getBoundsX1(); 680 int y = image.getHeight() - 1; 681 int processedRows = 0; 682 byte[] row = new byte[bytesPerRow]; 683 byte[] samples = new byte[COLUMNS]; 684 while (processedRows < ROWS) 685 { 686 in.readFully(row); 687 // copy red samples to array samples and store those samples 688 for (int x = X1 * 3 + 2, i = 0; i < COLUMNS; x += 3, i++) 689 { 690 samples[i] = row[x]; 691 } 692 image.putByteSamples(RGBIndex.INDEX_RED, 0, y, COLUMNS, 1, samples, 0); 693 // copy green samples to array samples and store those samples 694 for (int x = X1 * 3 + 1, i = 0; i < COLUMNS; x += 3, i++) 695 { 696 samples[i] = row[x]; 697 } 698 image.putByteSamples(RGBIndex.INDEX_GREEN, 0, y, COLUMNS, 1, samples, 0); 699 // copy blue samples to array samples and store those samples 700 for (int x = X1 * 3, i = 0; i < COLUMNS; x += 3, i++) 701 { 702 samples[i] = row[x]; 703 } 704 image.putByteSamples(RGBIndex.INDEX_BLUE, 0, y, COLUMNS, 1, samples, 0); 705 y--; 706 setProgress(processedRows, ROWS); 707 processedRows++; 708 } 709 } 710 711 private void loadUncompressedStream() throws 712 IOException, 713 OperationFailedException 714 { 715 switch(colorDepth) 716 { 717 case(1): 718 { 719 loadUncompressedBilevelStream(); 720 break; 721 } 722 case(4): 723 { 724 loadUncompressedPaletted4Stream(); 725 break; 726 } 727 case(8): 728 { 729 loadUncompressedPaletted8Stream(); 730 break; 731 } 732 case(24): 733 { 734 loadUncompressedRgb24Stream(); 735 break; 736 } 737 } 738 } 739 740 public void process() throws 741 MissingParameterException, 742 OperationFailedException 743 { 744 initModeFromIOObjects(); 745 if (getMode() == CodecMode.LOAD) 746 { 747 load(); 748 } 749 else 750 { 751 save(); 752 } 753 } 754 755 private void save() throws 756 MissingParameterException, 757 OperationFailedException, 758 UnsupportedTypeException 759 { 760 // check parameters of this operation 761 // 1 image to be saved 762 // 1.1 is it available? 763 PixelImage image = getImage(); 764 if (image == null) 765 { 766 throw new MissingParameterException("No image available."); 767 } 768 // 1.2 is it supported? 769 if (!(image instanceof Paletted8Image || 770 image instanceof Gray8Image || 771 image instanceof BilevelImage || 772 image instanceof RGB24Image)) 773 { 774 throw new UnsupportedTypeException("Unsupported image type: " + image.getClass().getName()); 775 } 776 // 2 is output stream available? 777 out = getOutputAsDataOutput(); 778 if (out == null) 779 { 780 throw new MissingParameterException("Output stream / random access file parameter missing."); 781 } 782 // now write the output stream 783 try 784 { 785 writeStream(); 786 } 787 catch (IOException ioe) 788 { 789 throw new OperationFailedException("I/O failure: " + ioe.toString()); 790 } 791 } 792 793 public String suggestFileExtension(PixelImage image) 794 { 795 return ".bmp"; 796 } 797 798 private void writeHeader(PixelImage image, int filesize, int offset, int numBits) throws IOException 799 { 800 out.write(0x42); // 'B' 801 out.write(0x4d); // 'M' 802 writeInt(filesize); 803 writeShort(0); 804 writeShort(0); 805 writeInt(offset); 806 807 writeInt(40); // BITMAP_INFO header length 808 writeInt(getBoundsWidth()); 809 writeInt(getBoundsHeight()); 810 writeShort(1); // # of planes 811 writeShort(numBits); 812 writeInt(0); // compression (0 = none) 813 writeInt(filesize - offset); // size of image data in bytes 814 writeInt((int)(getDpiX() * (100f / 2.54f))); // horizontal resolution in dpi 815 writeInt((int)(getDpiY() * (100f / 2.54f))); // vertical resolution in dpi 816 writeInt(0); // # of used colors 817 writeInt(0); // # of important colors 818 } 819 820 // we can't use out.writeInt because we need little endian byte order 821 private void writeInt(int value) throws IOException 822 { 823 out.write(value & 0xff); 824 out.write((value >> 8) & 0xff); 825 out.write((value >> 16) & 0xff); 826 out.write((value >> 24) & 0xff); 827 } 828 829 /** 830 * Write the palette associated with the image getImage(). 831 * Required not only for image objects that implement PalettedImage 832 * but also for BilevelImage and Grayscale8Image. 833 * For the latter two the palette values must be explicitly written into the file. 834 */ 835 private void writePalette() throws IOException 836 { 837 PixelImage pi = getImage(); 838 if (pi == null) 839 { 840 return; 841 } 842 if (pi instanceof Paletted8Image) 843 { 844 // always write 256 entries; if there aren't enough 845 // in the palette, fill it up to 256 with (0, 0, 0, 0) 846 Palette palette = ((Paletted8Image)pi).getPalette(); 847 for (int i = 0; i < 256; i++) 848 { 849 if (i < palette.getNumEntries()) 850 { 851 out.write(palette.getSample(RGBIndex.INDEX_BLUE, i)); 852 out.write(palette.getSample(RGBIndex.INDEX_GREEN, i)); 853 out.write(palette.getSample(RGBIndex.INDEX_RED, i)); 854 out.write(0); 855 } 856 else 857 { 858 out.writeInt(0); // writes four 0 bytes 859 } 860 } 861 } 862 if (pi instanceof Gray8Image) 863 { 864 for (int i = 0; i < 256; i++) 865 { 866 out.write(i); 867 out.write(i); 868 out.write(i); 869 out.write(0); 870 } 871 } 872 if (pi instanceof BilevelImage) 873 { 874 for (int i = 0; i < 2; i++) 875 { 876 out.write(i * 255); 877 out.write(i * 255); 878 out.write(i * 255); 879 out.write(0); 880 } 881 } 882 } 883 884 // we can't use out.writeShort because we need little endian byte order 885 private void writeShort(int value) throws IOException 886 { 887 out.write(value & 0xff); 888 out.write((value >> 8) & 0xff); 889 } 890 891 private void writeStream() throws IOException 892 { 893 PixelImage image = getImage(); 894 setBoundsIfNecessary(image.getWidth(), image.getHeight()); 895 int width = getBoundsWidth(); 896 int height = getBoundsHeight(); 897 ByteChannelImage bcimg = null; 898 BilevelImage bilevelImage = null; 899 RGB24Image rgbimg = null; 900 int bytesPerRow = 0; 901 int offset = 54; 902 int numBits = 0; 903 int numPackedBytes = 0; 904 if (image instanceof Paletted8Image || 905 image instanceof Gray8Image) 906 { 907 bcimg = (ByteChannelImage)image; 908 bytesPerRow = width; 909 offset += 1024; 910 numBits = 8; 911 } 912 else 913 if (image instanceof BilevelImage) 914 { 915 bilevelImage = (BilevelImage)image; 916 numPackedBytes = (width + 7) / 8; 917 bytesPerRow = numPackedBytes; 918 offset += 8; 919 numBits = 1; 920 } 921 else 922 if (image instanceof RGB24Image) 923 { 924 rgbimg = (RGB24Image)image; 925 bytesPerRow = width * 3; 926 numBits = 24; 927 } 928 if ((bytesPerRow % 4) != 0) 929 { 930 bytesPerRow = ((bytesPerRow + 3) / 4) * 4; 931 } 932 int filesize = offset + bytesPerRow * height; 933 writeHeader(image, filesize, offset, numBits); 934 writePalette(); 935 byte[] row = new byte[bytesPerRow]; 936 final int X1 = getBoundsX1(); 937 for (int y = getBoundsY2(), processed = 0; processed < height; y--, processed++) 938 { 939 if (bilevelImage != null) 940 { 941 bilevelImage.getPackedBytes(X1, y, width, row, 0, 0); 942 } 943 else 944 if (bcimg != null) 945 { 946 bcimg.getByteSamples(0, 0, y, width, 1, row, 0); 947 } 948 else 949 if (rgbimg != null) 950 { 951 int offs = 0; 952 for (int x = X1; x < X1 + width; x++) 953 { 954 row[offs++] = rgbimg.getByteSample(RGBIndex.INDEX_BLUE, x, y); 955 row[offs++] = rgbimg.getByteSample(RGBIndex.INDEX_GREEN, x, y); 956 row[offs++] = rgbimg.getByteSample(RGBIndex.INDEX_RED, x, y); 957 } 958 } 959 else 960 { 961 // error 962 } 963 out.write(row); 964 setProgress(processed, height); 965 if (getAbort()) 966 { 967 break; 968 } 969 } 970 close(); 971 } 972 }