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 String ls = (String)java.security.AccessController.doPrivileged( 103 new sun.security.action.GetPropertyAction("line.separator")); 104 lineSeparator = ls.getBytes(); 105 } 106 } 107 108 /** The output stream to write into */ 109 private ImageOutputStream stream = null; 110 111 /** Constructs <code>PNMImageWriter</code> based on the provided 112 * <code>ImageWriterSpi</code>. 113 */ 114 public PNMImageWriter(ImageWriterSpi originator) { 115 super(originator); 116 } 117 118 public void setOutput(Object output) { 119 super.setOutput(output); // validates output 120 if (output != null) { 121 if (!(output instanceof ImageOutputStream)) 122 throw new IllegalArgumentException(I18N.getString("PNMImageWriter0")); 123 this.stream = (ImageOutputStream)output; 124 } else 125 this.stream = null; 126 } 127 128 public ImageWriteParam getDefaultWriteParam() { 129 return new PNMImageWriteParam(); 130 } 131 132 public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { 133 return null; 134 } 135 136 public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, 137 ImageWriteParam param) { 138 return new PNMMetadata(imageType, param); 139 } 140 141 public IIOMetadata convertStreamMetadata(IIOMetadata inData, 142 ImageWriteParam param) { 143 return null; 144 } 145 146 public IIOMetadata convertImageMetadata(IIOMetadata inData, 147 ImageTypeSpecifier imageType, 148 ImageWriteParam param) { 149 // Check arguments. 150 if(inData == null) { 151 throw new IllegalArgumentException("inData == null!"); 152 } 153 if(imageType == null) { 154 throw new IllegalArgumentException("imageType == null!"); 155 } 156 157 PNMMetadata outData = null; 158 159 // Obtain a PNMMetadata object. 160 if(inData instanceof PNMMetadata) { 161 // Clone the input metadata. 162 outData = (PNMMetadata)((PNMMetadata)inData).clone(); 163 } else { 164 try { 165 outData = new PNMMetadata(inData); 166 } catch(IIOInvalidTreeException e) { 167 // XXX Warning 168 outData = new PNMMetadata(); 169 } 170 } 171 172 // Update the metadata per the image type and param. 173 outData.initialize(imageType, param); 174 175 return outData; 176 } 177 178 public boolean canWriteRasters() { 179 return true; 180 } 181 182 public void write(IIOMetadata streamMetadata, 183 IIOImage image, 184 ImageWriteParam param) throws IOException { 185 clearAbortRequest(); 186 processImageStarted(0); 187 if (param == null) 188 param = getDefaultWriteParam(); 189 190 RenderedImage input = null; 191 Raster inputRaster = null; 192 boolean writeRaster = image.hasRaster(); 193 Rectangle sourceRegion = param.getSourceRegion(); 194 SampleModel sampleModel = null; 195 ColorModel colorModel = null; 196 197 if (writeRaster) { 198 inputRaster = image.getRaster(); 199 sampleModel = inputRaster.getSampleModel(); 200 if (sourceRegion == null) 201 sourceRegion = inputRaster.getBounds(); 202 else 203 sourceRegion = sourceRegion.intersection(inputRaster.getBounds()); 204 } else { 205 input = image.getRenderedImage(); 206 sampleModel = input.getSampleModel(); 207 colorModel = input.getColorModel(); 208 Rectangle rect = new Rectangle(input.getMinX(), input.getMinY(), 209 input.getWidth(), input.getHeight()); 210 if (sourceRegion == null) 211 sourceRegion = rect; 212 else 213 sourceRegion = sourceRegion.intersection(rect); 214 } 215 216 if (sourceRegion.isEmpty()) 217 throw new RuntimeException(I18N.getString("PNMImageWrite1")); 218 219 ImageUtil.canEncodeImage(this, colorModel, sampleModel); 220 221 int scaleX = param.getSourceXSubsampling(); 222 int scaleY = param.getSourceYSubsampling(); 223 int xOffset = param.getSubsamplingXOffset(); 224 int yOffset = param.getSubsamplingYOffset(); 225 226 sourceRegion.translate(xOffset, yOffset); 227 sourceRegion.width -= xOffset; 228 sourceRegion.height -= yOffset; 229 230 int minX = sourceRegion.x / scaleX; 231 int minY = sourceRegion.y / scaleY; 232 int w = (sourceRegion.width + scaleX - 1) / scaleX; 233 int h = (sourceRegion.height + scaleY - 1) / scaleY; 234 235 Rectangle destinationRegion = new Rectangle(minX, minY, w, h); 236 237 int tileHeight = sampleModel.getHeight(); 238 int tileWidth = sampleModel.getWidth(); 239 240 // Raw data can only handle bytes, everything greater must be ASCII. 241 int[] sampleSize = sampleModel.getSampleSize(); 242 int[] sourceBands = param.getSourceBands(); 243 boolean noSubband = true; 244 int numBands = sampleModel.getNumBands(); 245 246 if (sourceBands != null) { 247 sampleModel = sampleModel.createSubsetSampleModel(sourceBands); 248 colorModel = null; 249 noSubband = false; 250 numBands = sampleModel.getNumBands(); 251 } else { 252 sourceBands = new int[numBands]; 253 for (int i = 0; i < numBands; i++) 254 sourceBands[i] = i; 255 } 256 257 // Colormap populated for non-bilevel IndexColorModel only. 258 byte[] reds = null; 259 byte[] greens = null; 260 byte[] blues = null; 261 262 // Flag indicating that PB data should be inverted before writing. 263 boolean isPBMInverted = false; 264 265 if (numBands == 1) { 266 if (colorModel instanceof IndexColorModel) { 267 IndexColorModel icm = (IndexColorModel)colorModel; 268 269 int mapSize = icm.getMapSize(); 270 if (mapSize < (1 << sampleSize[0])) 271 throw new RuntimeException(I18N.getString("PNMImageWrite2")); 272 273 if(sampleSize[0] == 1) { 274 variant = PBM_RAW; 275 276 // Set PBM inversion flag if 1 maps to a higher color 277 // value than 0: PBM expects white-is-zero so if this 278 // does not obtain then inversion needs to occur. 279 isPBMInverted = icm.getRed(1) > icm.getRed(0); 280 } else { 281 variant = PPM_RAW; 282 283 reds = new byte[mapSize]; 284 greens = new byte[mapSize]; 285 blues = new byte[mapSize]; 286 287 icm.getReds(reds); 288 icm.getGreens(greens); 289 icm.getBlues(blues); 290 } 291 } else if (sampleSize[0] == 1) { 292 variant = PBM_RAW; 293 } else if (sampleSize[0] <= 8) { 294 variant = PGM_RAW; 295 } else { 296 variant = PGM_ASCII; 297 } 298 } else if (numBands == 3) { 299 if (sampleSize[0] <= 8 && sampleSize[1] <= 8 && 300 sampleSize[2] <= 8) { // all 3 bands must be <= 8 301 variant = PPM_RAW; 302 } else { 303 variant = PPM_ASCII; 304 } 305 } else { 306 throw new RuntimeException(I18N.getString("PNMImageWrite3")); 307 } 308 309 IIOMetadata inputMetadata = image.getMetadata(); 310 ImageTypeSpecifier imageType; 311 if(colorModel != null) { 312 imageType = new ImageTypeSpecifier(colorModel, sampleModel); 313 } else { 314 int dataType = sampleModel.getDataType(); 315 switch(numBands) { 316 case 1: 317 imageType = 318 ImageTypeSpecifier.createGrayscale(sampleSize[0], dataType, 319 false); 320 break; 321 case 3: 322 ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); 323 imageType = 324 ImageTypeSpecifier.createInterleaved(cs, 325 new int[] {0, 1, 2}, 326 dataType, 327 false, false); 328 break; 329 default: 330 throw new IIOException("Cannot encode image with "+ 331 numBands+" bands!"); 332 } 333 } 334 335 PNMMetadata metadata; 336 if(inputMetadata != null) { 337 // Convert metadata. 338 metadata = (PNMMetadata)convertImageMetadata(inputMetadata, 339 imageType, param); 340 } else { 341 // Use default. 342 metadata = (PNMMetadata)getDefaultImageMetadata(imageType, param); 343 } 344 345 // Read parameters 346 boolean isRawPNM; 347 if(param instanceof PNMImageWriteParam) { 348 isRawPNM = ((PNMImageWriteParam)param).getRaw(); 349 } else { 350 isRawPNM = metadata.isRaw(); 351 } 352 353 maxValue = metadata.getMaxValue(); 354 for (int i = 0; i < sampleSize.length; i++) { 355 int v = (1 << sampleSize[i]) - 1; 356 if (v > maxValue) { 357 maxValue = v; 358 } 359 } 360 361 if (isRawPNM) { 362 // Raw output is desired. 363 int maxBitDepth = metadata.getMaxBitDepth(); 364 if (!isRaw(variant) && maxBitDepth <= 8) { 365 // Current variant is ASCII and the bit depth is acceptable 366 // so convert to RAW variant by adding '3' to variant. 367 variant += 0x3; 368 } else if(isRaw(variant) && maxBitDepth > 8) { 369 // Current variant is RAW and the bit depth it too large for 370 // RAW so convert to ASCII. 371 variant -= 0x3; 372 } 373 // Omitted cases are (variant == RAW && max <= 8) and 374 // (variant == ASCII && max > 8) neither of which requires action. 375 } else if(isRaw(variant)) { 376 // Raw output is NOT desired so convert to ASCII 377 variant -= 0x3; 378 } 379 380 // Write PNM file. 381 stream.writeByte('P'); // magic value: 'P' 382 stream.writeByte(variant); 383 384 stream.write(lineSeparator); 385 stream.write(COMMENT.getBytes()); // comment line 386 387 // Write the comments provided in the metadata 388 Iterator comments = metadata.getComments(); 389 if(comments != null) { 390 while(comments.hasNext()) { 391 stream.write(lineSeparator); 392 String comment = "# " + (String)comments.next(); 393 stream.write(comment.getBytes()); 394 } 395 } 396 397 stream.write(lineSeparator); 398 writeInteger(stream, w); // width 399 stream.write(SPACE); 400 writeInteger(stream, h); // height 401 402 // Write sample max value for non-binary images 403 if ((variant != PBM_RAW) && (variant != PBM_ASCII)) { 404 stream.write(lineSeparator); 405 writeInteger(stream, maxValue); 406 } 407 408 // The spec allows a single character between the 409 // last header value and the start of the raw data. 410 if (variant == PBM_RAW || 411 variant == PGM_RAW || 412 variant == PPM_RAW) { 413 stream.write('\n'); 414 } 415 416 // Set flag for optimal image writing case: row-packed data with 417 // correct band order if applicable. 418 boolean writeOptimal = false; 419 if (variant == PBM_RAW && 420 sampleModel.getTransferType() == DataBuffer.TYPE_BYTE && 421 sampleModel instanceof MultiPixelPackedSampleModel) { 422 423 MultiPixelPackedSampleModel mppsm = 424 (MultiPixelPackedSampleModel)sampleModel; 425 426 int originX = 0; 427 if (writeRaster) 428 originX = inputRaster.getMinX(); 429 else 430 originX = input.getMinX(); 431 432 // Must have left-aligned bytes with unity bit stride. 433 if(mppsm.getBitOffset((sourceRegion.x - originX) % tileWidth) == 0 && 434 mppsm.getPixelBitStride() == 1 && scaleX == 1) 435 writeOptimal = true; 436 } else if ((variant == PGM_RAW || variant == PPM_RAW) && 437 sampleModel instanceof ComponentSampleModel && 438 !(colorModel instanceof IndexColorModel)) { 439 440 ComponentSampleModel csm = 441 (ComponentSampleModel)sampleModel; 442 443 // Pixel stride must equal band count. 444 if(csm.getPixelStride() == numBands && scaleX == 1) { 445 writeOptimal = true; 446 447 // Band offsets must equal band indices. 448 if(variant == PPM_RAW) { 449 int[] bandOffsets = csm.getBandOffsets(); 450 for(int b = 0; b < numBands; b++) { 451 if(bandOffsets[b] != b) { 452 writeOptimal = false; 453 break; 454 } 455 } 456 } 457 } 458 } 459 460 // Write using an optimal approach if possible. 461 if(writeOptimal) { 462 int bytesPerRow = variant == PBM_RAW ? 463 (w + 7)/8 : w * sampleModel.getNumBands(); 464 byte[] bdata = null; 465 byte[] invertedData = new byte[bytesPerRow]; 466 467 // Loop over tiles to minimize cobbling. 468 for(int j = 0; j < sourceRegion.height; j++) { 469 if (abortRequested()) 470 break; 471 Raster lineRaster = null; 472 if (writeRaster) { 473 lineRaster = inputRaster.createChild(sourceRegion.x, 474 j, 475 sourceRegion.width, 476 1, 0, 0, null); 477 } else { 478 lineRaster = 479 input.getData(new Rectangle(sourceRegion.x, 480 sourceRegion.y + j, 481 w, 1)); 482 lineRaster = lineRaster.createTranslatedChild(0, 0); 483 } 484 485 bdata = ((DataBufferByte)lineRaster.getDataBuffer()).getData(); 486 487 sampleModel = lineRaster.getSampleModel(); 488 int offset = 0; 489 if (sampleModel instanceof ComponentSampleModel) { 490 offset = 491 ((ComponentSampleModel)sampleModel).getOffset(lineRaster.getMinX()-lineRaster.getSampleModelTranslateX(), 492 lineRaster.getMinY()-lineRaster.getSampleModelTranslateY()); 493 } else if (sampleModel instanceof MultiPixelPackedSampleModel) { 494 offset = ((MultiPixelPackedSampleModel)sampleModel).getOffset(lineRaster.getMinX() - 495 lineRaster.getSampleModelTranslateX(), 496 lineRaster.getMinX()-lineRaster.getSampleModelTranslateY()); 497 } 498 499 if (isPBMInverted) { 500 for(int k = offset, m = 0; m < bytesPerRow; k++, m++) 501 invertedData[m] = (byte)~bdata[k]; 502 bdata = invertedData; 503 offset = 0; 504 } 505 506 stream.write(bdata, offset, bytesPerRow); 507 processImageProgress(100.0F * j / sourceRegion.height); 508 } 509 510 // Write all buffered bytes and return. 511 stream.flush(); 512 if (abortRequested()) 513 processWriteAborted(); 514 else 515 processImageComplete(); 516 return; 517 } 518 519 // Buffer for 1 rows of original pixels 520 int size = sourceRegion.width * numBands; 521 522 int[] pixels = new int[size]; 523 524 // Also allocate a buffer to hold the data to be written to the file, 525 // so we can use array writes. 526 byte[] bpixels = 527 reds == null ? new byte[w * numBands] : new byte[w * 3]; 528 529 // The index of the sample being written, used to 530 // place a line separator after every 16th sample in 531 // ASCII mode. Not used in raw mode. 532 int count = 0; 533 534 // Process line by line 535 int lastRow = sourceRegion.y + sourceRegion.height; 536 537 for (int row = sourceRegion.y; row < lastRow; row += scaleY) { 538 if (abortRequested()) 539 break; 540 // Grab the pixels 541 Raster src = null; 542 543 if (writeRaster) 544 src = inputRaster.createChild(sourceRegion.x, 545 row, 546 sourceRegion.width, 1, 547 sourceRegion.x, row, sourceBands); 548 else 549 src = input.getData(new Rectangle(sourceRegion.x, row, 550 sourceRegion.width, 1)); 551 src.getPixels(sourceRegion.x, row, sourceRegion.width, 1, pixels); 552 553 if (isPBMInverted) 554 for (int i = 0; i < size; i += scaleX) 555 bpixels[i] ^= 1; 556 557 switch (variant) { 558 case PBM_ASCII: 559 case PGM_ASCII: 560 for (int i = 0; i < size; i += scaleX) { 561 if ((count++ % 16) == 0) 562 stream.write(lineSeparator); 563 else 564 stream.write(SPACE); 565 566 writeInteger(stream, pixels[i]); 567 } 568 stream.write(lineSeparator); 569 break; 570 571 case PPM_ASCII: 572 if (reds == null) { // no need to expand 573 for (int i = 0; i < size; i += scaleX * numBands) { 574 for (int j = 0; j < numBands; j++) { 575 if ((count++ % 16) == 0) 576 stream.write(lineSeparator); 577 else 578 stream.write(SPACE); 579 580 writeInteger(stream, pixels[i + j]); 581 } 582 } 583 } else { 584 for (int i = 0; i < size; i += scaleX) { 585 if ((count++ % 5) == 0) 586 stream.write(lineSeparator); 587 else 588 stream.write(SPACE); 589 590 writeInteger(stream, (reds[pixels[i]] & 0xFF)); 591 stream.write(SPACE); 592 writeInteger(stream, (greens[pixels[i]] & 0xFF)); 593 stream.write(SPACE); 594 writeInteger(stream, (blues[pixels[i]] & 0xFF)); 595 } 596 } 597 stream.write(lineSeparator); 598 break; 599 600 case PBM_RAW: 601 // 8 pixels packed into 1 byte, the leftovers are padded. 602 int kdst = 0; 603 int ksrc = 0; 604 int b = 0; 605 int pos = 7; 606 for (int i = 0; i < size; i += scaleX) { 607 b |= pixels[i] << pos; 608 pos--; 609 if (pos == -1) { 610 bpixels[kdst++] = (byte)b; 611 b = 0; 612 pos = 7; 613 } 614 } 615 616 if (pos != 7) 617 bpixels[kdst++] = (byte)b; 618 619 stream.write(bpixels, 0, kdst); 620 break; 621 622 case PGM_RAW: 623 for (int i = 0, j = 0; i < size; i += scaleX) { 624 bpixels[j++] = (byte)(pixels[i]); 625 } 626 stream.write(bpixels, 0, w); 627 break; 628 629 case PPM_RAW: 630 if (reds == null) { // no need to expand 631 for (int i = 0, k = 0; i < size; i += scaleX * numBands) { 632 for (int j = 0; j < numBands; j++) 633 bpixels[k++] = (byte)(pixels[i + j] & 0xFF); 634 } 635 } else { 636 for (int i = 0, j = 0; i < size; i += scaleX) { 637 bpixels[j++] = reds[pixels[i]]; 638 bpixels[j++] = greens[pixels[i]]; 639 bpixels[j++] = blues[pixels[i]]; 640 } 641 } 642 stream.write(bpixels, 0, bpixels.length); 643 break; 644 } 645 646 processImageProgress(100.0F * (row - sourceRegion.y) / 647 sourceRegion.height); 648 } 649 650 // Force all buffered bytes to be written out. 651 stream.flush(); 652 653 if (abortRequested()) 654 processWriteAborted(); 655 else 656 processImageComplete(); 657 } 658 659 public void reset() { 660 super.reset(); 661 stream = null; 662 } 663 664 /** Writes an integer to the output in ASCII format. */ 665 private void writeInteger(ImageOutputStream output, int i) throws IOException { 666 output.write(Integer.toString(i).getBytes()); 667 } 668 669 /** Writes a byte to the output in ASCII format. */ 670 private void writeByte(ImageOutputStream output, byte b) throws IOException { 671 output.write(Byte.toString(b).getBytes()); 672 } 673 674 /** Returns true if file variant is raw format, false if ASCII. */ 675 private boolean isRaw(int v) { 676 return (v >= PBM_RAW); 677 } 678}