001 /* 002 * PNMCodec 003 * 004 * Copyright (c) 2000, 2001, 2002, 2003 Marco Schmidt. 005 * All rights reserved. 006 */ 007 008 package net.sourceforge.jiu.codecs; 009 010 import java.io.DataOutput; 011 import java.io.InputStream; 012 import java.io.IOException; 013 import java.io.PushbackInputStream; 014 import java.util.NoSuchElementException; 015 import java.util.StringTokenizer; 016 import net.sourceforge.jiu.data.BilevelImage; 017 import net.sourceforge.jiu.data.GrayImage; 018 import net.sourceforge.jiu.data.Gray16Image; 019 import net.sourceforge.jiu.data.Gray8Image; 020 import net.sourceforge.jiu.data.GrayIntegerImage; 021 import net.sourceforge.jiu.data.IntegerImage; 022 import net.sourceforge.jiu.data.MemoryBilevelImage; 023 import net.sourceforge.jiu.data.MemoryGray16Image; 024 import net.sourceforge.jiu.data.MemoryGray8Image; 025 import net.sourceforge.jiu.data.MemoryRGB24Image; 026 import net.sourceforge.jiu.data.MemoryRGB48Image; 027 import net.sourceforge.jiu.data.PixelImage; 028 import net.sourceforge.jiu.data.RGB24Image; 029 import net.sourceforge.jiu.data.RGB48Image; 030 import net.sourceforge.jiu.data.RGBIndex; 031 import net.sourceforge.jiu.data.RGBIntegerImage; 032 import net.sourceforge.jiu.ops.MissingParameterException; 033 import net.sourceforge.jiu.ops.OperationFailedException; 034 import net.sourceforge.jiu.ops.WrongParameterException; 035 036 /** 037 * A codec to read and write Portable Anymap (PNM) image files. 038 * This format includes three file types well-known in the Unix world: 039 * <ul> 040 * <li>PBM (Portable Bitmap - 1 bit per pixel bilevel image),</li> 041 * <li>PGM (Portable Graymap - grayscale image) and</li> 042 * <li>PPM (Portable Pixmap - RGB truecolor image).</li> 043 * </ul> 044 * <p> 045 * 046 * <h3>Compression</h3> 047 * The file format only allows for uncompressed storage. 048 * 049 * <h3>ASCII mode / binary mode</h3> 050 * PNM streams can be stored in binary mode or ASCII mode. 051 * ASCII mode files are text files with numbers representing the pixels. 052 * They become larger than their binary counterparts, but as they are 053 * very redundant they can be compressed well with archive programs. 054 * ASCII PGM and PPM files can have all kinds of maximum sample values, 055 * thus allowing for arbitrary precision. 056 * They are not restricted by byte limits. 057 * PBM streams always have two colors, no matter if they are ASCII or binary. 058 * 059 * <h3>Color depth for PGM / PPM</h3> 060 * <p> 061 * The header of a PGM and PPM file stores a maximum sample value 062 * (such a value is not stored for PBM, where the maximum value is always 1). 063 * When in binary mode, PGM and PPM typically have a maximum sample value of 255, 064 * which makes PGM 8 bits per pixel and PPM 24 bits per pixel large. 065 * One sample will be stored as a single byte. 066 * However, there also exist binary PGM files with a maximum sample value larger than 067 * 255 and smaller than 65536. 068 * These files use two bytes per sample, in network byte order (big endian). 069 * I have yet to see PPM files with that property, but they are of course imagineable. 070 * 16 bpp 071 * </p> 072 * 073 * <h3>DPI values</h3> 074 * PNM files cannot store the physical resolution in DPI. 075 * 076 * <h3>Number of images</h3> 077 * Only one image can be stored in a PNM file. 078 * 079 * <h3>Usage example - load an image from a PNM file</h3> 080 * <pre> 081 * PNMCodec codec = new PNMCodec(); 082 * codec.setFile("test.ppm", CodecMode.LOAD); 083 * codec.process(); 084 * codec.close(); 085 * PixelImage image = codec.getImage(); 086 * </pre> 087 * 088 * <h3>Usage example - save an image to a PNM file</h3> 089 * <pre> 090 * PNMCodec codec = new PNMCodec(); 091 * BilevelImage myFax = ...; // initialize 092 * codec.setImage(myFax); 093 * codec.setFile("out.pbm", CodecMode.SAVE); 094 * codec.process(); 095 * codec.close(); 096 * </pre> 097 * 098 * @author Marco Schmidt 099 */ 100 public class PNMCodec extends ImageCodec 101 { 102 /** 103 * Image type constant for images of unknown type. 104 */ 105 public static final int IMAGE_TYPE_UNKNOWN = -1; 106 107 /** 108 * Image type constant for bilevel images, stored in PBM files. 109 */ 110 public static final int IMAGE_TYPE_BILEVEL = 0; 111 112 /** 113 * Image type constant for grayscale images, stored in PGM files. 114 */ 115 public static final int IMAGE_TYPE_GRAY = 1; 116 117 /** 118 * Image type constant for RGB truecolor images, stored in PPM files. 119 */ 120 public static final int IMAGE_TYPE_COLOR = 2; 121 private static final String[] IMAGE_TYPE_FILE_EXTENSIONS = 122 {".pbm", ".pgm", ".ppm"}; 123 private Boolean ascii; 124 private int columns; 125 private int imageType; 126 private PushbackInputStream in; 127 private DataOutput out; 128 private int height; 129 private int maxSample; 130 private int width; 131 132 /** 133 * Attempts to find the appropriate image type by looking at a file's name. 134 * Ignores case when comparing. 135 * Returns {@link #IMAGE_TYPE_BILEVEL} for <code>.pbm</code>, 136 * {@link #IMAGE_TYPE_GRAY} for <code>.pgm</code> and 137 * {@link #IMAGE_TYPE_COLOR} for <code>.ppm</code>. 138 * Otherwise, {@link #IMAGE_TYPE_UNKNOWN} is returned. 139 * To get a file extension given that you have an image type, use 140 * {@link #getTypicalFileExtension}. 141 * 142 * @param fileName the file name to be examined 143 * @return one of the <code>IMAGE_TYPE_xxx</code> constants of this class 144 */ 145 public static int determineImageTypeFromFileName(String fileName) 146 { 147 if (fileName == null || fileName.length() < 4) 148 { 149 return IMAGE_TYPE_UNKNOWN; 150 } 151 String ext = fileName.substring(fileName.length() - 3); 152 ext = ext.toLowerCase(); 153 for (int i = 0; i < IMAGE_TYPE_FILE_EXTENSIONS.length; i++) 154 { 155 if (IMAGE_TYPE_FILE_EXTENSIONS[i].equals(ext)) 156 { 157 return i; 158 } 159 } 160 return IMAGE_TYPE_UNKNOWN; 161 } 162 163 /** 164 * Returns if ASCII mode was used for loading an image or will 165 * be used to store an image. 166 * @return true for ASCII mode, false for binary mode, null if that information is not available 167 * @see #setAscii 168 */ 169 public Boolean getAscii() 170 { 171 return ascii; 172 } 173 174 public String getFormatName() 175 { 176 return "Portable Anymap (PBM, PGM, PPM)"; 177 } 178 179 public String[] getMimeTypes() 180 { 181 return new String[] {"image/x-ppm", "image/x-pgm", "image/x-pbm", "image/x-pnm", 182 "image/x-portable-pixmap", "image/x-portable-bitmap", "image/x-portable-graymap", 183 "image/x-portable-anymap"}; 184 } 185 186 /** 187 * Returns the typical file extension (including leading dot) for an 188 * image type. 189 * Returns <code>null</code> for {@link #IMAGE_TYPE_UNKNOWN}. 190 * To get the image type given that you have a file name, use 191 * {@link #determineImageTypeFromFileName}. 192 * 193 * @param imageType the image type for which the extension is required 194 * @return the file extension or null 195 */ 196 public static String getTypicalFileExtension(int imageType) 197 { 198 if (imageType >= 0 && imageType < IMAGE_TYPE_FILE_EXTENSIONS.length) 199 { 200 return IMAGE_TYPE_FILE_EXTENSIONS[imageType]; 201 } 202 else 203 { 204 return null; 205 } 206 } 207 208 public boolean isLoadingSupported() 209 { 210 return true; 211 } 212 213 public boolean isSavingSupported() 214 { 215 return true; 216 } 217 218 /** 219 * Loads an image from a PNM input stream. 220 * It is assumed that a stream was given to this codec using {@link #setInputStream(InputStream)}. 221 * 222 * @return the image as an instance of a class that implements {@link IntegerImage} 223 * @throws InvalidFileStructureException if the input stream is not a valid PNM stream (or unsupported) 224 * @throws java.io.IOException if there were problems reading from the input stream 225 */ 226 private void load() throws 227 InvalidFileStructureException, 228 IOException, 229 MissingParameterException, 230 UnsupportedTypeException, 231 WrongFileFormatException, 232 WrongParameterException 233 { 234 InputStream is = getInputStream(); 235 if (is != null) 236 { 237 if (is instanceof PushbackInputStream) 238 { 239 in = (PushbackInputStream)is; 240 } 241 else 242 { 243 in = new PushbackInputStream(is); 244 } 245 } 246 else 247 { 248 throw new MissingParameterException("InputStream object required for loading."); 249 } 250 loadType(); 251 String resolutionLine = loadTextLine(); 252 setResolution(resolutionLine); 253 setBoundsIfNecessary(width, height); 254 if (imageType == IMAGE_TYPE_BILEVEL) 255 { 256 maxSample = 1; 257 } 258 else 259 { 260 // load maximum value 261 String maxSampleLine = loadTextLine(); 262 setMaximumSample(maxSampleLine); 263 } 264 if (maxSample > 65535) 265 { 266 throw new UnsupportedTypeException("Cannot deal with samples larger than 65535."); 267 } 268 checkImageResolution(); 269 switch (imageType) 270 { 271 case(IMAGE_TYPE_BILEVEL): 272 { 273 loadBilevelImage(); 274 break; 275 } 276 case(IMAGE_TYPE_COLOR): 277 { 278 loadColorImage(); 279 break; 280 } 281 case(IMAGE_TYPE_GRAY): 282 { 283 loadGrayImage(); 284 break; 285 } 286 default: 287 { 288 throw new UnsupportedTypeException("Cannot deal with image type."); 289 } 290 } 291 } 292 293 private int loadAsciiNumber() throws 294 InvalidFileStructureException, 295 IOException 296 { 297 boolean hasDigit = false; 298 int result = -1; 299 do 300 { 301 int b = in.read(); 302 if (b >= 48 && b <= 57) 303 { 304 // decimal digit 305 if (hasDigit) 306 { 307 result = result * 10 + (b - 48); 308 } 309 else 310 { 311 hasDigit = true; 312 result = b - 48; 313 } 314 } 315 else 316 if (b == 32 || b == 10 || b == 13 || b == 9) 317 { 318 // whitespace 319 if (hasDigit) 320 { 321 if (result > maxSample) 322 { 323 throw new InvalidFileStructureException("Read number " + 324 "from PNM stream that is larger than allowed " + 325 "maximum sample value " + maxSample + " (" + result + ")."); 326 } 327 return result; 328 } 329 // ignore whitespace 330 } 331 else 332 if (b == 35) 333 { 334 // the # character, indicating a comment row 335 if (hasDigit) 336 { 337 in.unread(b); 338 if (result > maxSample) 339 { 340 throw new InvalidFileStructureException("Read " + 341 "number from PNM stream that is larger than " + 342 "allowed maximum sample value " + maxSample + 343 " (" + result + ")."); 344 } 345 return result; 346 } 347 StringBuffer sb = new StringBuffer(); 348 do 349 { 350 b = in.read(); 351 } 352 while (b != -1 && b != 10 && b != 13); 353 if (b == 13) 354 { 355 } 356 // put it into the comment list 357 } 358 else 359 if (b == -1) 360 { 361 // the end of file character 362 if (hasDigit) 363 { 364 if (result > maxSample) 365 { 366 throw new InvalidFileStructureException("Read number from PNM stream that is larger than allowed maximum sample value " + 367 maxSample + " (" + result + ")"); 368 } 369 return result; 370 } 371 throw new InvalidFileStructureException("Unexpected end of file while reading ASCII number from PNM stream."); 372 } 373 else 374 { 375 throw new InvalidFileStructureException("Read invalid character from PNM stream: " + b + 376 " dec."); 377 } 378 } 379 while(true); 380 } 381 382 private void loadBilevelImage() throws 383 InvalidFileStructureException, 384 IOException, 385 WrongParameterException 386 { 387 PixelImage image = getImage(); 388 if (image == null) 389 { 390 setImage(new MemoryBilevelImage(getBoundsWidth(), getBoundsHeight())); 391 } 392 else 393 { 394 if (!(image instanceof BilevelImage)) 395 { 396 throw new WrongParameterException("Specified input image must implement BilevelImage for this image type."); 397 } 398 } 399 if (getAscii().booleanValue()) 400 { 401 loadBilevelImageAscii(); 402 } 403 else 404 { 405 loadBilevelImageBinary(); 406 } 407 } 408 409 private void loadBilevelImageAscii() throws 410 InvalidFileStructureException, 411 IOException 412 { 413 BilevelImage image = (BilevelImage)getImage(); 414 // skip the pixels of the first getBoundsY1() rows 415 int pixelsToSkip = width * getBoundsY1(); 416 for (int i = 0; i < pixelsToSkip; i++) 417 { 418 int value = loadAsciiNumber(); 419 } 420 final int NUM_ROWS = getBoundsHeight(); 421 final int COLUMNS = getBoundsWidth(); 422 final int X1 = getBoundsX1(); 423 int[] row = new int[width]; 424 // now read and store getBoundsHeight() rows 425 for (int y = 0; y < NUM_ROWS; y++) 426 { 427 for (int x = 0; x < width; x++) 428 { 429 int value = loadAsciiNumber(); 430 if (value == 0) 431 { 432 row[x] = BilevelImage.WHITE; 433 } 434 else 435 if (value == 1) 436 { 437 row[x] = BilevelImage.BLACK; 438 } 439 else 440 { 441 throw new InvalidFileStructureException("Loaded " + 442 "number for position x=" + x + ", y=" + (y + getBoundsY1()) + 443 " is neither 0 nor 1 in PBM stream: " + value); 444 } 445 } 446 image.putSamples(0, 0, y, COLUMNS, 1, row, X1); 447 setProgress(y, NUM_ROWS); 448 } 449 } 450 451 private void loadBilevelImageBinary() throws 452 InvalidFileStructureException, 453 IOException 454 { 455 BilevelImage image = (BilevelImage)getImage(); 456 int bytesPerRow = (width + 7) / 8; 457 // skip the first getBoundsY1() rows 458 long bytesToSkip = (long)getBoundsY1() * (long)bytesPerRow; 459 // Note: 460 // removed in.skip(bytesToSkip) because that was only available in Java 1.2 461 // instead the following while loop is used 462 while (bytesToSkip-- > 0) 463 { 464 int value = in.read(); 465 } 466 // allocate buffer large enough for a complete row 467 byte[] row = new byte[bytesPerRow]; 468 final int numRows = getBoundsHeight(); 469 // read and store the next getBoundsHeight() rows 470 for (int y = 0; y < numRows; y++) 471 { 472 // read bytesPerRow bytes into row 473 int bytesToRead = bytesPerRow; 474 int index = 0; 475 while (bytesToRead > 0) 476 { 477 int result = in.read(row, index, bytesToRead); 478 if (result >= 0) 479 { 480 index += result; 481 bytesToRead -= result; 482 } 483 else 484 { 485 throw new InvalidFileStructureException("Unexpected end of input stream while reading."); 486 } 487 } 488 // invert values 489 for (int x = 0; x < row.length; x++) 490 { 491 row[x] = (byte)~row[x]; 492 } 493 //image.putPackedBytes(0, y, bytesPerRow, buffer, 0); 494 if (isRowRequired(y)) 495 { 496 image.putPackedBytes(0, y - getBoundsY1(), getBoundsWidth(), row, getBoundsX1() >> 3, getBoundsX1() & 7); 497 } 498 setProgress(y, numRows); 499 } 500 } 501 502 private void loadColorImage() throws InvalidFileStructureException, IOException 503 { 504 RGBIntegerImage image = null; 505 RGB24Image image24 = null; 506 if (maxSample <= 255) 507 { 508 image24 = new MemoryRGB24Image(width, height); 509 image = image24; 510 setImage(image); 511 } 512 else 513 { 514 image = new MemoryRGB48Image(width, height); 515 setImage(image); 516 } 517 for (int y = 0, destY = - getBoundsY1(); y < height; y++, destY++) 518 { 519 if (getAscii().booleanValue()) 520 { 521 for (int x = 0; x < width; x++) 522 { 523 int red = loadAsciiNumber(); 524 if (red < 0 || red > maxSample) 525 { 526 throw new InvalidFileStructureException("Invalid " + 527 "sample value " + red + " for red sample at " + 528 "(x=" + x + ", y=" + y + ")."); 529 } 530 image.putSample(RGBIndex.INDEX_RED, x, y, red); 531 532 int green = loadAsciiNumber(); 533 if (green < 0 || green > maxSample) 534 { 535 throw new InvalidFileStructureException("Invalid " + 536 "sample value " + green + " for green sample at " + 537 "(x=" + x + ", y=" + y + ")."); 538 } 539 image.putSample(RGBIndex.INDEX_GREEN, x, y, green); 540 541 int blue = loadAsciiNumber(); 542 if (blue < 0 || blue > maxSample) 543 { 544 throw new InvalidFileStructureException("Invalid " + 545 "sample value " + blue + " for blue sample at " + 546 "(x=" + x + ", y=" + y + ")."); 547 } 548 image.putSample(RGBIndex.INDEX_BLUE, x, y, blue); 549 } 550 } 551 else 552 { 553 if (image24 != null) 554 { 555 for (int x = 0; x < width; x++) 556 { 557 int red = in.read(); 558 if (red == -1) 559 { 560 throw new InvalidFileStructureException("Unexpected " + 561 "end of file while reading red sample for pixel " + 562 "x=" + x + ", y=" + y + "."); 563 } 564 image24.putByteSample(RGBIndex.INDEX_RED, x, y, (byte)(red & 0xff)); 565 int green = in.read(); 566 if (green == -1) 567 { 568 throw new InvalidFileStructureException("Unexpected " + 569 "end of file while reading green sample for pixel " + 570 "x=" + x + ", y=" + y + "."); 571 } 572 image24.putByteSample(RGBIndex.INDEX_GREEN, x, y, (byte)(green & 0xff)); 573 int blue = in.read(); 574 if (blue == -1) 575 { 576 throw new InvalidFileStructureException("Unexpected " + 577 "end of file while reading blue sample for pixel " + 578 "x=" + x + ", y=" + y + "."); 579 } 580 image24.putByteSample(RGBIndex.INDEX_BLUE, x, y, (byte)(blue & 0xff)); 581 } 582 } 583 } 584 setProgress(y, getBoundsHeight()); 585 } 586 } 587 588 private void loadGrayImage() throws InvalidFileStructureException, IOException, UnsupportedTypeException 589 { 590 final int WIDTH = getBoundsWidth(); 591 final int HEIGHT = getBoundsHeight(); 592 PixelImage pimage = getImage(); 593 if (pimage == null) 594 { 595 if (maxSample < 256) 596 { 597 pimage = new MemoryGray8Image(WIDTH, HEIGHT); 598 } 599 else 600 if (maxSample < 65536) 601 { 602 pimage = new MemoryGray16Image(WIDTH, HEIGHT); 603 } 604 else 605 { 606 throw new UnsupportedTypeException("Gray images with more than 16 bits per pixel are not supported."); 607 } 608 setImage(pimage); 609 } 610 else 611 { 612 } 613 GrayIntegerImage image = (GrayIntegerImage)pimage; 614 int[] buffer = new int[width]; 615 for (int y = 0, destY = -getBoundsY1(); destY < getBoundsHeight(); y++, destY++) 616 { 617 if (getAscii().booleanValue()) 618 { 619 for (int x = 0; x < width; x++) 620 { 621 buffer[x] = loadAsciiNumber(); 622 } 623 } 624 else 625 { 626 if (maxSample < 256) 627 { 628 for (int x = 0; x < width; x++) 629 { 630 buffer[x] = in.read(); 631 } 632 } 633 else 634 { 635 for (int x = 0; x < width; x++) 636 { 637 int msb = in.read(); 638 int lsb = in.read(); 639 buffer[x] = (msb << 8) | lsb; 640 } 641 } 642 } 643 if (destY >= 0 && destY < getBoundsHeight()) 644 { 645 image.putSamples(0, 0, destY, getBoundsWidth(), 1, buffer, getBoundsX1()); 646 } 647 setProgress(y, getBoundsY2() + 1); 648 } 649 } 650 651 private String loadTextLine() throws InvalidFileStructureException, IOException 652 { 653 // load text lines until 654 // 1) a normal text line is found 655 // 2) an error occurs 656 // any comment lines starting with # are added to the 657 // comments Vector 658 boolean isComment; 659 StringBuffer sb; 660 do 661 { 662 sb = new StringBuffer(); 663 int b; 664 boolean crOrLf; 665 do 666 { 667 b = in.read(); 668 if (b == -1) 669 { 670 throw new InvalidFileStructureException("Unexpected end of file in PNM stream."); 671 } 672 crOrLf = (b == 0x0a || b == 0x0d); 673 if (!crOrLf) 674 { 675 sb.append((char)b); 676 } 677 } 678 while (!crOrLf); 679 if (b == 0x0d) 680 { 681 b = in.read(); 682 if (b != 0x0a) 683 { 684 throw new InvalidFileStructureException("Unexpected end of file in PNM stream."); 685 } 686 } 687 isComment = (sb.length() > 0 && sb.charAt(0) == '#'); 688 if (isComment) 689 { 690 //sb.deleteCharAt(0); 691 //sb.delete(0, 1); 692 StringBuffer result = new StringBuffer(sb.length() - 1); 693 int i = 1; 694 while (i < sb.length()) 695 { 696 result.append(sb.charAt(i++)); 697 } 698 appendComment(result.toString()); 699 } 700 } 701 while (isComment); 702 return sb.toString(); 703 } 704 705 /** 706 * Loads the first two characters (which are expected to be a capital P 707 * followed by a decimal digit between 1 and 6, inclusively) and skips 708 * following LF and CR characters. 709 * This method not only checks the two bytes, it also initializes internal fields 710 * for storage mode (ASCII or binary) and image type. 711 * 712 * @throws WrongFileFormatException if the input stream is not a PNM stream 713 * @throws InvalidFileStructureException if the format that 714 * is described above was not encountered 715 * @throws java.io.IOException if there were errors reading data 716 * @throws java.lang.IllegalArgumentException if the input stream was not given to this codec 717 */ 718 private void loadType() throws InvalidFileStructureException, IOException, WrongFileFormatException 719 { 720 // read two bytes 721 int v1 = in.read(); 722 int v2 = in.read(); 723 // check if first byte is P 724 if (v1 != 0x50) 725 { 726 throw new WrongFileFormatException("Not a PNM stream. First byte " + 727 "in PNM stream is expected to be 0x50 ('P'); found: " + 728 v1 + " (dec)."); 729 } 730 // check if second byte is ASCII of digit from 1 to 6 731 if (v2 < 0x31 || v2 > 0x36) 732 { 733 throw new WrongFileFormatException("Not a PNM stream. Second byte " + 734 "in PNM stream is expected to be the ASCII value of decimal " + 735 "digit between 1 and 6 (49 dec to 54 dec); found " + 736 v2 + " dec."); 737 } 738 // determine mode (ASCII or binary) from second byte 739 ascii = new Boolean(v2 < 0x34); 740 // determine image type from second byte 741 v2 = v2 - 0x30; 742 imageType = (v2 - 1) % 3; 743 // skip LF and CR 744 int b; 745 do 746 { 747 b = in.read(); 748 } 749 while (b == 0x0a || b == 0x0d || b == ' '); 750 if (b == -1) 751 { 752 throw new InvalidFileStructureException("Read type (" + 753 v2 + "). Unexpected end of file in input PNM stream."); 754 } 755 in.unread(b); 756 } 757 758 public void process() throws 759 MissingParameterException, 760 OperationFailedException 761 { 762 initModeFromIOObjects(); 763 try 764 { 765 if (getMode() == CodecMode.LOAD) 766 { 767 load(); 768 } 769 else 770 { 771 save(); 772 } 773 } 774 catch (IOException ioe) 775 { 776 throw new OperationFailedException("I/O error: " + ioe.toString()); 777 } 778 } 779 780 private void save() throws 781 IOException, 782 MissingParameterException, 783 WrongParameterException 784 { 785 out = getOutputAsDataOutput(); 786 if (out == null) 787 { 788 throw new WrongParameterException("Cannot get a DataOutput object to use for saving."); 789 } 790 PixelImage pi = getImage(); 791 if (pi == null) 792 { 793 throw new MissingParameterException("Input image missing."); 794 } 795 if (!(pi instanceof IntegerImage)) 796 { 797 throw new WrongParameterException("Input image must implement IntegerImage."); 798 } 799 IntegerImage image = (IntegerImage)pi; 800 width = image.getWidth(); 801 height = image.getHeight(); 802 setBoundsIfNecessary(width, height); 803 if (image instanceof RGB24Image) 804 { 805 imageType = IMAGE_TYPE_COLOR; 806 maxSample = 255; 807 save((RGB24Image)image); 808 } 809 else 810 if (image instanceof RGB48Image) 811 { 812 imageType = IMAGE_TYPE_COLOR; 813 maxSample = 65535; 814 save((RGB48Image)image); 815 } 816 else 817 if (image instanceof BilevelImage) 818 { 819 imageType = IMAGE_TYPE_BILEVEL; 820 maxSample = 1; 821 save((BilevelImage)image); 822 } 823 else 824 if (image instanceof Gray8Image) 825 { 826 imageType = IMAGE_TYPE_GRAY; 827 maxSample = 255; 828 save((Gray8Image)image); 829 } 830 else 831 if (image instanceof Gray16Image) 832 { 833 imageType = IMAGE_TYPE_GRAY; 834 maxSample = 65535; 835 save((Gray16Image)image); 836 } 837 else 838 { 839 throw new WrongParameterException("Unsupported input image type: " + 840 image.getClass().getName()); 841 } 842 close(); 843 } 844 845 private void save(BilevelImage image) throws IOException 846 { 847 saveHeader(); 848 final int WIDTH = getBoundsWidth(); 849 final int HEIGHT = getBoundsHeight(); 850 final int BYTES_PER_ROW = (WIDTH + 7) / 8; 851 byte[] buffer = new byte[BYTES_PER_ROW]; 852 for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++) 853 { 854 if (getAscii().booleanValue()) 855 { 856 for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++) 857 { 858 if (image.isBlack(srcX, srcY)) 859 { 860 out.write(49); // 1 861 } 862 else 863 { 864 out.write(48); // 0 865 } 866 columns ++; 867 if (columns > 70) 868 { 869 columns = 0; 870 out.write(10); 871 } 872 else 873 { 874 out.write(32); 875 columns++; 876 } 877 } 878 } 879 else 880 { 881 image.getPackedBytes(getBoundsX1(), srcY, WIDTH, buffer, 0, 0); 882 for (int x = 0; x < buffer.length; x++) 883 { 884 buffer[x] = (byte)(~buffer[x]); 885 } 886 out.write(buffer); 887 } 888 setProgress(y, HEIGHT); 889 } 890 } 891 892 private void save(Gray8Image image) throws IOException 893 { 894 saveHeader(); 895 final int HEIGHT = getBoundsHeight(); 896 final int WIDTH = getBoundsWidth(); 897 final int X1 = getBoundsX1(); 898 System.out.println(WIDTH + " " + HEIGHT + " " + X1); 899 byte[] buffer = new byte[WIDTH]; 900 for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++) 901 { 902 image.getByteSamples(0, X1, srcY, WIDTH, 1, buffer, 0); 903 if (getAscii().booleanValue()) 904 { 905 for (int x = 0; x < WIDTH; x++) 906 { 907 saveAsciiNumber(buffer[x] & 0xff); 908 out.write(32); 909 columns += 2; 910 if (columns > 70) 911 { 912 columns = 0; 913 out.write(10); 914 } 915 else 916 { 917 out.write(32); 918 columns++; 919 } 920 } 921 } 922 else 923 { 924 out.write(buffer); 925 } 926 setProgress(y, HEIGHT); 927 } 928 } 929 930 private void save(Gray16Image image) throws IOException 931 { 932 saveHeader(); 933 final int HEIGHT = getBoundsHeight(); 934 final int WIDTH = getBoundsWidth(); 935 final int X1 = getBoundsX1(); 936 short[] buffer = new short[WIDTH]; 937 for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++) 938 { 939 image.getShortSamples(0, X1, srcY, WIDTH, 1, buffer, 0); 940 if (getAscii().booleanValue()) 941 { 942 for (int x = 0; x < WIDTH; x++) 943 { 944 saveAsciiNumber(buffer[x] & 0xffff); 945 out.write(32); 946 columns += 4; 947 if (columns > 70) 948 { 949 columns = 0; 950 out.write(10); 951 } 952 else 953 { 954 out.write(32); 955 columns++; 956 } 957 } 958 } 959 else 960 { 961 for (int x = 0; x < WIDTH; x++) 962 { 963 int sample = buffer[x] & 0xffff; 964 out.write((sample >> 8) & 0xff); 965 out.write(sample & 0xff); 966 } 967 } 968 setProgress(y, HEIGHT); 969 } 970 } 971 972 private void save(RGB24Image image) throws IOException 973 { 974 saveHeader(); 975 final int WIDTH = getBoundsWidth(); 976 final int HEIGHT = getBoundsHeight(); 977 for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++) 978 { 979 if (getAscii().booleanValue()) 980 { 981 for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++) 982 { 983 int red = image.getSample(RGBIndex.INDEX_RED, srcX, srcY); 984 int green = image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY); 985 int blue = image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY); 986 saveAsciiNumber(red); 987 out.write(32); 988 saveAsciiNumber(green); 989 out.write(32); 990 saveAsciiNumber(blue); 991 columns += 11; 992 if (columns > 80) 993 { 994 columns = 0; 995 out.write(10); 996 } 997 else 998 { 999 out.write(32); 1000 columns++; 1001 } 1002 } 1003 } 1004 else 1005 { 1006 for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++) 1007 { 1008 out.write(image.getSample(RGBIndex.INDEX_RED, srcX, srcY)); 1009 out.write(image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY)); 1010 out.write(image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY)); 1011 } 1012 } 1013 setProgress(y, HEIGHT); 1014 } 1015 } 1016 1017 private void save(RGB48Image image) throws IOException 1018 { 1019 saveHeader(); 1020 final int WIDTH = getBoundsWidth(); 1021 final int HEIGHT = getBoundsHeight(); 1022 for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++) 1023 { 1024 if (getAscii().booleanValue()) 1025 { 1026 for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++) 1027 { 1028 int red = image.getSample(RGBIndex.INDEX_RED, srcX, srcY); 1029 int green = image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY); 1030 int blue = image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY); 1031 saveAsciiNumber(red); 1032 out.write(32); 1033 saveAsciiNumber(green); 1034 out.write(32); 1035 saveAsciiNumber(blue); 1036 columns += 13; 1037 if (columns > 80) 1038 { 1039 columns = 0; 1040 out.write(10); 1041 } 1042 else 1043 { 1044 out.write(32); 1045 columns++; 1046 } 1047 } 1048 } 1049 else 1050 { 1051 /* 1052 for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++) 1053 { 1054 out.write(image.getSample(RGBIndex.INDEX_RED, srcX, srcY)); 1055 out.write(image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY)); 1056 out.write(image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY)); 1057 } 1058 */ 1059 } 1060 setProgress(y, HEIGHT); 1061 } 1062 } 1063 1064 private void saveAsciiNumber(int number) throws 1065 IOException 1066 { 1067 String s = Integer.toString(number); 1068 for (int i = 0; i < s.length(); i++) 1069 { 1070 char c = s.charAt(i); 1071 out.write(c); 1072 } 1073 columns += s.length(); 1074 } 1075 1076 private void saveHeader() throws IOException 1077 { 1078 out.write(80); // 'P' 1079 int pnmType = 49 + imageType; 1080 if (getAscii() == null) 1081 { 1082 setAscii(maxSample > 255); 1083 } 1084 if (!getAscii().booleanValue()) 1085 { 1086 pnmType += 3; 1087 } 1088 out.write(pnmType); // '1' .. '6' 1089 out.write(10); // line feed 1090 saveAsciiNumber(getBoundsWidth()); 1091 out.write(32); // space 1092 saveAsciiNumber(getBoundsHeight()); 1093 out.write(10); // line feed 1094 if (imageType != IMAGE_TYPE_BILEVEL) 1095 { 1096 // bilevel max sample is always 1 and MUST NOT be saved 1097 saveAsciiNumber(maxSample); 1098 out.write(10);// line feed 1099 } 1100 } 1101 1102 /** 1103 * Specify whether ASCII mode is to be used when saving an image. 1104 * Default is binary mode. 1105 * @param asciiMode if true, ASCII mode is used, binary mode otherwise 1106 */ 1107 public void setAscii(boolean asciiMode) 1108 { 1109 ascii = new Boolean(asciiMode); 1110 } 1111 1112 private void setMaximumSample(String line) throws InvalidFileStructureException 1113 { 1114 line = line.trim(); 1115 try 1116 { 1117 maxSample = Integer.parseInt(line); 1118 } 1119 catch (NumberFormatException nfe) 1120 { 1121 throw new InvalidFileStructureException("Not a valid value for the maximum sample: " + line); 1122 } 1123 if (maxSample < 0) 1124 { 1125 throw new InvalidFileStructureException("The value for the maximum sample must not be negative; found " + maxSample); 1126 } 1127 } 1128 1129 /* 1130 * Reads resolution from argument String and sets private variables 1131 * width and height. 1132 */ 1133 private void setResolution(String line) throws InvalidFileStructureException 1134 { 1135 line = line.trim(); 1136 StringTokenizer st = new StringTokenizer(line, " "); 1137 try 1138 { 1139 if (!st.hasMoreTokens()) 1140 { 1141 throw new InvalidFileStructureException("No width value found in line \"" + 1142 line + "\"."); 1143 } 1144 String number = st.nextToken(); 1145 try 1146 { 1147 width = Integer.parseInt(number); 1148 } 1149 catch (NumberFormatException nfe) 1150 { 1151 throw new InvalidFileStructureException("Not a valid int value for width: " + 1152 number); 1153 } 1154 if (width < 1) 1155 { 1156 throw new InvalidFileStructureException("The width value must be larger than " + 1157 "zero; found " + width + "."); 1158 } 1159 if (!st.hasMoreTokens()) 1160 { 1161 throw new InvalidFileStructureException("No height value found in line \"" + 1162 line + "\"."); 1163 } 1164 number = st.nextToken(); 1165 try 1166 { 1167 height = Integer.parseInt(number); 1168 } 1169 catch (NumberFormatException nfe) 1170 { 1171 throw new InvalidFileStructureException("Not a valid int value for height: " + 1172 number); 1173 } 1174 if (height < 1) 1175 { 1176 throw new InvalidFileStructureException("The height value must be larger than " + 1177 "zero; found " + width + "."); 1178 } 1179 } 1180 catch (NoSuchElementException nsee) 1181 { 1182 // should not happen because we always check if there is a token 1183 } 1184 } 1185 1186 public String suggestFileExtension(PixelImage image) 1187 { 1188 if (image == null) 1189 { 1190 return null; 1191 } 1192 if (image instanceof BilevelImage) 1193 { 1194 return IMAGE_TYPE_FILE_EXTENSIONS[IMAGE_TYPE_BILEVEL]; 1195 } 1196 else 1197 if (image instanceof GrayImage) 1198 { 1199 return IMAGE_TYPE_FILE_EXTENSIONS[IMAGE_TYPE_GRAY]; 1200 } 1201 else 1202 if (image instanceof RGB24Image) 1203 { 1204 return IMAGE_TYPE_FILE_EXTENSIONS[IMAGE_TYPE_COLOR]; 1205 } 1206 return null; 1207 } 1208 }