001/* 002 * $RCSfile: PNMImageWriter.java,v $ 003 * 004 * 005 * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. 006 * 007 * Redistribution and use in source and binary forms, with or without 008 * modification, are permitted provided that the following conditions 009 * are met: 010 * 011 * - Redistribution of source code must retain the above copyright 012 * notice, this list of conditions and the following disclaimer. 013 * 014 * - Redistribution in binary form must reproduce the above copyright 015 * notice, this list of conditions and the following disclaimer in 016 * the documentation and/or other materials provided with the 017 * distribution. 018 * 019 * Neither the name of Sun Microsystems, Inc. or the names of 020 * contributors may be used to endorse or promote products derived 021 * from this software without specific prior written permission. 022 * 023 * This software is provided "AS IS," without a warranty of any 024 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND 025 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, 026 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY 027 * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL 028 * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF 029 * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS 030 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR 031 * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, 032 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND 033 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR 034 * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE 035 * POSSIBILITY OF SUCH DAMAGES. 036 * 037 * You acknowledge that this software is not designed or intended for 038 * use in the design, construction, operation or maintenance of any 039 * nuclear facility. 040 * 041 * $Revision: 1.1 $ 042 * $Date: 2005/02/11 05:01:40 $ 043 * $State: Exp $ 044 */ 045package com.github.jaiimageio.impl.plugins.pnm; 046 047import java.awt.Rectangle; 048import java.awt.color.ColorSpace; 049import java.awt.image.ColorModel; 050import java.awt.image.ComponentSampleModel; 051import java.awt.image.DataBuffer; 052import java.awt.image.DataBufferByte; 053import java.awt.image.IndexColorModel; 054import java.awt.image.MultiPixelPackedSampleModel; 055import java.awt.image.Raster; 056import java.awt.image.RenderedImage; 057import java.awt.image.SampleModel; 058import java.io.IOException; 059import java.util.Iterator; 060 061import javax.imageio.IIOException; 062import javax.imageio.IIOImage; 063import javax.imageio.ImageTypeSpecifier; 064import javax.imageio.ImageWriteParam; 065import javax.imageio.ImageWriter; 066import javax.imageio.metadata.IIOInvalidTreeException; 067import javax.imageio.metadata.IIOMetadata; 068import javax.imageio.spi.ImageWriterSpi; 069import javax.imageio.stream.ImageOutputStream; 070 071import com.github.jaiimageio.impl.common.ImageUtil; 072import com.github.jaiimageio.plugins.pnm.PNMImageWriteParam; 073/** 074 * The Java Image IO plugin writer for encoding a binary RenderedImage into 075 * a PNM format. 076 * 077 * The encoding process may clip, subsample using the parameters 078 * specified in the <code>ImageWriteParam</code>. 079 * 080 * @see com.github.jaiimageio.plugins.PNMImageWriteParam 081 */ 082public class PNMImageWriter extends ImageWriter { 083 private static final int PBM_ASCII = '1'; 084 private static final int PGM_ASCII = '2'; 085 private static final int PPM_ASCII = '3'; 086 private static final int PBM_RAW = '4'; 087 private static final int PGM_RAW = '5'; 088 private static final int PPM_RAW = '6'; 089 090 private static final int SPACE = ' '; 091 092 private static final String COMMENT = 093 "# written by com.github.jaiimageio.impl.PNMImageWriter"; 094 095 private static byte[] lineSeparator; 096 097 private int variant; 098 private int maxValue; 099 100 static { 101 if (lineSeparator == null) { 102 lineSeparator = System.getProperty("line.separator").getBytes(); 103 } 104 } 105 106 /** The output stream to write into */ 107 private ImageOutputStream stream = null; 108 109 /** Constructs <code>PNMImageWriter</code> based on the provided 110 * <code>ImageWriterSpi</code>. 111 */ 112 public PNMImageWriter(ImageWriterSpi originator) { 113 super(originator); 114 } 115 116 public void setOutput(Object output) { 117 super.setOutput(output); // validates output 118 if (output != null) { 119 if (!(output instanceof ImageOutputStream)) 120 throw new IllegalArgumentException(I18N.getString("PNMImageWriter0")); 121 this.stream = (ImageOutputStream)output; 122 } else 123 this.stream = null; 124 } 125 126 public ImageWriteParam getDefaultWriteParam() { 127 return new PNMImageWriteParam(); 128 } 129 130 public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { 131 return null; 132 } 133 134 public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, 135 ImageWriteParam param) { 136 return new PNMMetadata(imageType, param); 137 } 138 139 public IIOMetadata convertStreamMetadata(IIOMetadata inData, 140 ImageWriteParam param) { 141 return null; 142 } 143 144 public IIOMetadata convertImageMetadata(IIOMetadata inData, 145 ImageTypeSpecifier imageType, 146 ImageWriteParam param) { 147 // Check arguments. 148 if(inData == null) { 149 throw new IllegalArgumentException("inData == null!"); 150 } 151 if(imageType == null) { 152 throw new IllegalArgumentException("imageType == null!"); 153 } 154 155 PNMMetadata outData = null; 156 157 // Obtain a PNMMetadata object. 158 if(inData instanceof PNMMetadata) { 159 // Clone the input metadata. 160 outData = (PNMMetadata)((PNMMetadata)inData).clone(); 161 } else { 162 try { 163 outData = new PNMMetadata(inData); 164 } catch(IIOInvalidTreeException e) { 165 // XXX Warning 166 outData = new PNMMetadata(); 167 } 168 } 169 170 // Update the metadata per the image type and param. 171 outData.initialize(imageType, param); 172 173 return outData; 174 } 175 176 public boolean canWriteRasters() { 177 return true; 178 } 179 180 public void write(IIOMetadata streamMetadata, 181 IIOImage image, 182 ImageWriteParam param) throws IOException { 183 clearAbortRequest(); 184 processImageStarted(0); 185 if (param == null) 186 param = getDefaultWriteParam(); 187 188 RenderedImage input = null; 189 Raster inputRaster = null; 190 boolean writeRaster = image.hasRaster(); 191 Rectangle sourceRegion = param.getSourceRegion(); 192 SampleModel sampleModel = null; 193 ColorModel colorModel = null; 194 195 if (writeRaster) { 196 inputRaster = image.getRaster(); 197 sampleModel = inputRaster.getSampleModel(); 198 if (sourceRegion == null) 199 sourceRegion = inputRaster.getBounds(); 200 else 201 sourceRegion = sourceRegion.intersection(inputRaster.getBounds()); 202 } else { 203 input = image.getRenderedImage(); 204 sampleModel = input.getSampleModel(); 205 colorModel = input.getColorModel(); 206 Rectangle rect = new Rectangle(input.getMinX(), input.getMinY(), 207 input.getWidth(), input.getHeight()); 208 if (sourceRegion == null) 209 sourceRegion = rect; 210 else 211 sourceRegion = sourceRegion.intersection(rect); 212 } 213 214 if (sourceRegion.isEmpty()) 215 throw new RuntimeException(I18N.getString("PNMImageWrite1")); 216 217 ImageUtil.canEncodeImage(this, colorModel, sampleModel); 218 219 int scaleX = param.getSourceXSubsampling(); 220 int scaleY = param.getSourceYSubsampling(); 221 int xOffset = param.getSubsamplingXOffset(); 222 int yOffset = param.getSubsamplingYOffset(); 223 224 sourceRegion.translate(xOffset, yOffset); 225 sourceRegion.width -= xOffset; 226 sourceRegion.height -= yOffset; 227 228 int minX = sourceRegion.x / scaleX; 229 int minY = sourceRegion.y / scaleY; 230 int w = (sourceRegion.width + scaleX - 1) / scaleX; 231 int h = (sourceRegion.height + scaleY - 1) / scaleY; 232 233 Rectangle destinationRegion = new Rectangle(minX, minY, w, h); 234 235 int tileHeight = sampleModel.getHeight(); 236 int tileWidth = sampleModel.getWidth(); 237 238 // Raw data can only handle bytes, everything greater must be ASCII. 239 int[] sampleSize = sampleModel.getSampleSize(); 240 int[] sourceBands = param.getSourceBands(); 241 boolean noSubband = true; 242 int numBands = sampleModel.getNumBands(); 243 244 if (sourceBands != null) { 245 sampleModel = sampleModel.createSubsetSampleModel(sourceBands); 246 colorModel = null; 247 noSubband = false; 248 numBands = sampleModel.getNumBands(); 249 } else { 250 sourceBands = new int[numBands]; 251 for (int i = 0; i < numBands; i++) 252 sourceBands[i] = i; 253 } 254 255 // Colormap populated for non-bilevel IndexColorModel only. 256 byte[] reds = null; 257 byte[] greens = null; 258 byte[] blues = null; 259 260 // Flag indicating that PB data should be inverted before writing. 261 boolean isPBMInverted = false; 262 263 if (numBands == 1) { 264 if (colorModel instanceof IndexColorModel) { 265 IndexColorModel icm = (IndexColorModel)colorModel; 266 267 int mapSize = icm.getMapSize(); 268 if (mapSize < (1 << sampleSize[0])) 269 throw new RuntimeException(I18N.getString("PNMImageWrite2")); 270 271 if(sampleSize[0] == 1) { 272 variant = PBM_RAW; 273 274 // Set PBM inversion flag if 1 maps to a higher color 275 // value than 0: PBM expects white-is-zero so if this 276 // does not obtain then inversion needs to occur. 277 isPBMInverted = icm.getRed(1) > icm.getRed(0); 278 } else { 279 variant = PPM_RAW; 280 281 reds = new byte[mapSize]; 282 greens = new byte[mapSize]; 283 blues = new byte[mapSize]; 284 285 icm.getReds(reds); 286 icm.getGreens(greens); 287 icm.getBlues(blues); 288 } 289 } else if (sampleSize[0] == 1) { 290 variant = PBM_RAW; 291 } else if (sampleSize[0] <= 8) { 292 variant = PGM_RAW; 293 } else { 294 variant = PGM_ASCII; 295 } 296 } else if (numBands == 3) { 297 if (sampleSize[0] <= 8 && sampleSize[1] <= 8 && 298 sampleSize[2] <= 8) { // all 3 bands must be <= 8 299 variant = PPM_RAW; 300 } else { 301 variant = PPM_ASCII; 302 } 303 } else { 304 throw new RuntimeException(I18N.getString("PNMImageWrite3")); 305 } 306 307 IIOMetadata inputMetadata = image.getMetadata(); 308 ImageTypeSpecifier imageType; 309 if(colorModel != null) { 310 imageType = new ImageTypeSpecifier(colorModel, sampleModel); 311 } else { 312 int dataType = sampleModel.getDataType(); 313 switch(numBands) { 314 case 1: 315 imageType = 316 ImageTypeSpecifier.createGrayscale(sampleSize[0], dataType, 317 false); 318 break; 319 case 3: 320 ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); 321 imageType = 322 ImageTypeSpecifier.createInterleaved(cs, 323 new int[] {0, 1, 2}, 324 dataType, 325 false, false); 326 break; 327 default: 328 throw new IIOException("Cannot encode image with "+ 329 numBands+" bands!"); 330 } 331 } 332 333 PNMMetadata metadata; 334 if(inputMetadata != null) { 335 // Convert metadata. 336 metadata = (PNMMetadata)convertImageMetadata(inputMetadata, 337 imageType, param); 338 } else { 339 // Use default. 340 metadata = (PNMMetadata)getDefaultImageMetadata(imageType, param); 341 } 342 343 // Read parameters 344 boolean isRawPNM; 345 if(param instanceof PNMImageWriteParam) { 346 isRawPNM = ((PNMImageWriteParam)param).getRaw(); 347 } else { 348 isRawPNM = metadata.isRaw(); 349 } 350 351 maxValue = metadata.getMaxValue(); 352 for (int i = 0; i < sampleSize.length; i++) { 353 int v = (1 << sampleSize[i]) - 1; 354 if (v > maxValue) { 355 maxValue = v; 356 } 357 } 358 359 if (isRawPNM) { 360 // Raw output is desired. 361 int maxBitDepth = metadata.getMaxBitDepth(); 362 if (!isRaw(variant) && maxBitDepth <= 8) { 363 // Current variant is ASCII and the bit depth is acceptable 364 // so convert to RAW variant by adding '3' to variant. 365 variant += 0x3; 366 } else if(isRaw(variant) && maxBitDepth > 8) { 367 // Current variant is RAW and the bit depth it too large for 368 // RAW so convert to ASCII. 369 variant -= 0x3; 370 } 371 // Omitted cases are (variant == RAW && max <= 8) and 372 // (variant == ASCII && max > 8) neither of which requires action. 373 } else if(isRaw(variant)) { 374 // Raw output is NOT desired so convert to ASCII 375 variant -= 0x3; 376 } 377 378 // Write PNM file. 379 stream.writeByte('P'); // magic value: 'P' 380 stream.writeByte(variant); 381 382 stream.write(lineSeparator); 383 stream.write(COMMENT.getBytes()); // comment line 384 385 // Write the comments provided in the metadata 386 Iterator comments = metadata.getComments(); 387 if(comments != null) { 388 while(comments.hasNext()) { 389 stream.write(lineSeparator); 390 String comment = "# " + (String)comments.next(); 391 stream.write(comment.getBytes()); 392 } 393 } 394 395 stream.write(lineSeparator); 396 writeInteger(stream, w); // width 397 stream.write(SPACE); 398 writeInteger(stream, h); // height 399 400 // Write sample max value for non-binary images 401 if ((variant != PBM_RAW) && (variant != PBM_ASCII)) { 402 stream.write(lineSeparator); 403 writeInteger(stream, maxValue); 404 } 405 406 // The spec allows a single character between the 407 // last header value and the start of the raw data. 408 if (variant == PBM_RAW || 409 variant == PGM_RAW || 410 variant == PPM_RAW) { 411 stream.write('\n'); 412 } 413 414 // Set flag for optimal image writing case: row-packed data with 415 // correct band order if applicable. 416 boolean writeOptimal = false; 417 if (variant == PBM_RAW && 418 sampleModel.getTransferType() == DataBuffer.TYPE_BYTE && 419 sampleModel instanceof MultiPixelPackedSampleModel) { 420 421 MultiPixelPackedSampleModel mppsm = 422 (MultiPixelPackedSampleModel)sampleModel; 423 424 int originX = 0; 425 if (writeRaster) 426 originX = inputRaster.getMinX(); 427 else 428 originX = input.getMinX(); 429 430 // Must have left-aligned bytes with unity bit stride. 431 if(mppsm.getBitOffset((sourceRegion.x - originX) % tileWidth) == 0 && 432 mppsm.getPixelBitStride() == 1 && scaleX == 1) 433 writeOptimal = true; 434 } else if ((variant == PGM_RAW || variant == PPM_RAW) && 435 sampleModel instanceof ComponentSampleModel && 436 !(colorModel instanceof IndexColorModel)) { 437 438 ComponentSampleModel csm = 439 (ComponentSampleModel)sampleModel; 440 441 // Pixel stride must equal band count. 442 if(csm.getPixelStride() == numBands && scaleX == 1) { 443 writeOptimal = true; 444 445 // Band offsets must equal band indices. 446 if(variant == PPM_RAW) { 447 int[] bandOffsets = csm.getBandOffsets(); 448 for(int b = 0; b < numBands; b++) { 449 if(bandOffsets[b] != b) { 450 writeOptimal = false; 451 break; 452 } 453 } 454 } 455 } 456 } 457 458 // Write using an optimal approach if possible. 459 if(writeOptimal) { 460 int bytesPerRow = variant == PBM_RAW ? 461 (w + 7)/8 : w * sampleModel.getNumBands(); 462 byte[] bdata = null; 463 byte[] invertedData = new byte[bytesPerRow]; 464 465 // Loop over tiles to minimize cobbling. 466 for(int j = 0; j < sourceRegion.height; j++) { 467 if (abortRequested()) 468 break; 469 Raster lineRaster = null; 470 if (writeRaster) { 471 lineRaster = inputRaster.createChild(sourceRegion.x, 472 j, 473 sourceRegion.width, 474 1, 0, 0, null); 475 } else { 476 lineRaster = 477 input.getData(new Rectangle(sourceRegion.x, 478 sourceRegion.y + j, 479 w, 1)); 480 lineRaster = lineRaster.createTranslatedChild(0, 0); 481 } 482 483 bdata = ((DataBufferByte)lineRaster.getDataBuffer()).getData(); 484 485 sampleModel = lineRaster.getSampleModel(); 486 int offset = 0; 487 if (sampleModel instanceof ComponentSampleModel) { 488 offset = 489 ((ComponentSampleModel)sampleModel).getOffset(lineRaster.getMinX()-lineRaster.getSampleModelTranslateX(), 490 lineRaster.getMinY()-lineRaster.getSampleModelTranslateY()); 491 } else if (sampleModel instanceof MultiPixelPackedSampleModel) { 492 offset = ((MultiPixelPackedSampleModel)sampleModel).getOffset(lineRaster.getMinX() - 493 lineRaster.getSampleModelTranslateX(), 494 lineRaster.getMinX()-lineRaster.getSampleModelTranslateY()); 495 } 496 497 if (isPBMInverted) { 498 for(int k = offset, m = 0; m < bytesPerRow; k++, m++) 499 invertedData[m] = (byte)~bdata[k]; 500 bdata = invertedData; 501 offset = 0; 502 } 503 504 stream.write(bdata, offset, bytesPerRow); 505 processImageProgress(100.0F * j / sourceRegion.height); 506 } 507 508 // Write all buffered bytes and return. 509 stream.flush(); 510 if (abortRequested()) 511 processWriteAborted(); 512 else 513 processImageComplete(); 514 return; 515 } 516 517 // Buffer for 1 rows of original pixels 518 int size = sourceRegion.width * numBands; 519 520 int[] pixels = new int[size]; 521 522 // Also allocate a buffer to hold the data to be written to the file, 523 // so we can use array writes. 524 byte[] bpixels = 525 reds == null ? new byte[w * numBands] : new byte[w * 3]; 526 527 // The index of the sample being written, used to 528 // place a line separator after every 16th sample in 529 // ASCII mode. Not used in raw mode. 530 int count = 0; 531 532 // Process line by line 533 int lastRow = sourceRegion.y + sourceRegion.height; 534 535 for (int row = sourceRegion.y; row < lastRow; row += scaleY) { 536 if (abortRequested()) 537 break; 538 // Grab the pixels 539 Raster src = null; 540 541 if (writeRaster) 542 src = inputRaster.createChild(sourceRegion.x, 543 row, 544 sourceRegion.width, 1, 545 sourceRegion.x, row, sourceBands); 546 else 547 src = input.getData(new Rectangle(sourceRegion.x, row, 548 sourceRegion.width, 1)); 549 src.getPixels(sourceRegion.x, row, sourceRegion.width, 1, pixels); 550 551 if (isPBMInverted) 552 for (int i = 0; i < size; i += scaleX) 553 bpixels[i] ^= 1; 554 555 switch (variant) { 556 case PBM_ASCII: 557 case PGM_ASCII: 558 for (int i = 0; i < size; i += scaleX) { 559 if ((count++ % 16) == 0) 560 stream.write(lineSeparator); 561 else 562 stream.write(SPACE); 563 564 writeInteger(stream, pixels[i]); 565 } 566 stream.write(lineSeparator); 567 break; 568 569 case PPM_ASCII: 570 if (reds == null) { // no need to expand 571 for (int i = 0; i < size; i += scaleX * numBands) { 572 for (int j = 0; j < numBands; j++) { 573 if ((count++ % 16) == 0) 574 stream.write(lineSeparator); 575 else 576 stream.write(SPACE); 577 578 writeInteger(stream, pixels[i + j]); 579 } 580 } 581 } else { 582 for (int i = 0; i < size; i += scaleX) { 583 if ((count++ % 5) == 0) 584 stream.write(lineSeparator); 585 else 586 stream.write(SPACE); 587 588 writeInteger(stream, (reds[pixels[i]] & 0xFF)); 589 stream.write(SPACE); 590 writeInteger(stream, (greens[pixels[i]] & 0xFF)); 591 stream.write(SPACE); 592 writeInteger(stream, (blues[pixels[i]] & 0xFF)); 593 } 594 } 595 stream.write(lineSeparator); 596 break; 597 598 case PBM_RAW: 599 // 8 pixels packed into 1 byte, the leftovers are padded. 600 int kdst = 0; 601 int ksrc = 0; 602 int b = 0; 603 int pos = 7; 604 for (int i = 0; i < size; i += scaleX) { 605 b |= pixels[i] << pos; 606 pos--; 607 if (pos == -1) { 608 bpixels[kdst++] = (byte)b; 609 b = 0; 610 pos = 7; 611 } 612 } 613 614 if (pos != 7) 615 bpixels[kdst++] = (byte)b; 616 617 stream.write(bpixels, 0, kdst); 618 break; 619 620 case PGM_RAW: 621 for (int i = 0, j = 0; i < size; i += scaleX) { 622 bpixels[j++] = (byte)(pixels[i]); 623 } 624 stream.write(bpixels, 0, w); 625 break; 626 627 case PPM_RAW: 628 if (reds == null) { // no need to expand 629 for (int i = 0, k = 0; i < size; i += scaleX * numBands) { 630 for (int j = 0; j < numBands; j++) 631 bpixels[k++] = (byte)(pixels[i + j] & 0xFF); 632 } 633 } else { 634 for (int i = 0, j = 0; i < size; i += scaleX) { 635 bpixels[j++] = reds[pixels[i]]; 636 bpixels[j++] = greens[pixels[i]]; 637 bpixels[j++] = blues[pixels[i]]; 638 } 639 } 640 stream.write(bpixels, 0, bpixels.length); 641 break; 642 } 643 644 processImageProgress(100.0F * (row - sourceRegion.y) / 645 sourceRegion.height); 646 } 647 648 // Force all buffered bytes to be written out. 649 stream.flush(); 650 651 if (abortRequested()) 652 processWriteAborted(); 653 else 654 processImageComplete(); 655 } 656 657 public void reset() { 658 super.reset(); 659 stream = null; 660 } 661 662 /** Writes an integer to the output in ASCII format. */ 663 private void writeInteger(ImageOutputStream output, int i) throws IOException { 664 output.write(Integer.toString(i).getBytes()); 665 } 666 667 /** Writes a byte to the output in ASCII format. */ 668 private void writeByte(ImageOutputStream output, byte b) throws IOException { 669 output.write(Byte.toString(b).getBytes()); 670 } 671 672 /** Returns true if file variant is raw format, false if ASCII. */ 673 private boolean isRaw(int v) { 674 return (v >= PBM_RAW); 675 } 676}