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