Java Source Code: fi.faidon.jis.PNGImageSaver


   1: /*****************************************************************************
   2:  *
   3:  *                               PNGImageSaver.java
   4:  *
   5:  * Java source created by Kary FRAMLING 6/6/1998
   6:  *
   7:  * Copyright 1998-2003 Kary Fr´┐Żmling
   8:  * Source code distributed under GNU LESSER GENERAL PUBLIC LICENSE,
   9:  * included in the LICENSE.txt file in the topmost directory
  10:  *
  11:  *****************************************************************************/
  12: 
  13: package fi.faidon.jis;
  14: 
  15: import java.io.*;
  16: import java.awt.Image;
  17: import java.awt.image.ImageProducer;
  18: import java.awt.image.ImageConsumer;
  19: import java.awt.image.ImageObserver;
  20: import java.awt.image.ColorModel;
  21: import java.awt.image.IndexColorModel;
  22: import java.util.Hashtable;
  23: import java.awt.Toolkit;
  24: import java.io.Serializable;
  25: import java.util.zip.*;
  26: 
  27: import fi.faidon.util.ByteArrayConversion;
  28: 
  29: /**
  30:  * Class for saving an image in the PNG format. We always use 24-bit RGB
  31:  * true-color coding for the moment.
  32:  *
  33:  * @author Kary FRÄMLING
  34:  */
  35: public class PNGImageSaver extends ImageSaverInterface
  36:	  implements Serializable, ImageConsumer {
  37:    //--------------------------------------------------------------------------------------
  38:    // Public constants.
  39:    //--------------------------------------------------------------------------------------
  40:    public static final String    FORMAT_CODE = "PNG";
  41:    public static final String    FORMAT_COMPLETE_NAME = "Portable Network Graphics (PNG)";
  42:    public static final String    FORMAT_EXTENSION = "png";
  43:    
  44:    //--------------------------------------------------------------------------------------
  45:    // Private constants.
  46:    //--------------------------------------------------------------------------------------
  47:    private final boolean    OBLIGATORY_REGISTRATION = false;
  48:    private final int        DEFLATE_INPUT_SIZE = 512;
  49:    private final byte[]    PNG_IDENTIFICATION_BYTES = {(byte)137, (byte)80, (byte)78, (byte)71, (byte)13, (byte)10, (byte)26, (byte)10};
  50:    private final int        IHDR_DATA_SIZE = 13;
  51:    private final int        CHUNK_TYPE_SIZE = 4;
  52:    private final byte[]    IHDR_CHUNK_TYPE = {(byte)0x49, (byte)0x48, (byte)0x44, (byte)0x52};
  53:    private final byte[]    IDAT_CHUNK_TYPE = {(byte)0x49, (byte)0x44, (byte)0x41, (byte)0x54};
  54:    private final byte[]    IEND_CHUNK_TYPE = {(byte)0x49, (byte)0x45, (byte)0x4E, (byte)0x44};
  55:    private final byte        COLOR_TYPE_COLOR = 2;
  56:    private final byte        COLOR_TYPE_USE_ALPHA_CHANNEL = 4;
  57:    private final byte        DEFLATE_INFLATE_COMPRESSION = 0;
  58:    private final byte        NO_INTERLACE = 0;
  59:    private final byte        NO_FILTER = 0;
  60:    private final byte        SUB_FILTER = 1;
  61:    
  62:    //--------------------------------------------------------------------------------------
  63:    // Private fields.
  64:    //--------------------------------------------------------------------------------------
  65:    private FileOutputStream    writeFileHandle;
  66:    private int        width;
  67:    private int        height;
  68:    private int        saveStatus;
  69:    private int        byteCount;
  70:    private int        bytesPerPixel;
  71:    private CRC32    crcCounter;
  72:    private Deflater    pixmapDeflater;
  73:    private long    idatChunkSizeOffset;
  74:    private long    idatDataSize;
  75:    private boolean    headerIsWritten;
  76:    private boolean    imageCompleteDone;
  77:    private boolean includeAlphaChannel;
  78:    
  79:    //--------------------------------------------------------------------------------------
  80:    // Public methods.
  81:    //--------------------------------------------------------------------------------------
  82:    
  83:    //=============================================================================
  84:    // Constructor
  85:    //=============================================================================
  86:    /**
  87:     * @author Kary FRÄMLING 6/6/1998.
  88:     */
  89:    //=============================================================================
  90:	      public PNGImageSaver() {
  91:    // Registration check.
  92:        /*
  93:	          if ( OBLIGATORY_REGISTRATION ) {
  94:            fi.faidon.protection.SerialNumberManager sm = new fi.faidon.protection.SerialNumberManager();
  95:	              if ( !sm.verifyCurrentPackage() ) {
  96:                System.err.println(fi.faidon.protection.SerialNumberManager.STR_REG_VERIFICATION_FAILED);
  97:                System.err.println("Exiting...");
  98:                System.exit(1);
  99:            }
 100:        }
 101:         */
 102:    // We use the same CRC counter all the way.
 103:    crcCounter = new CRC32();
 104:    
 105:    // Nothing written yet.
 106:    headerIsWritten = false;
 107:    imageCompleteDone = false;
 108:    
 109:    // Three bytes per pixel by default.
 110:    bytesPerPixel = 3;
 111:    
 112:    // No alpha channel by default.
 113:    includeAlphaChannel = false;
 114:    }
 115:    
 116:    //=============================================================================
 117:    /**
 118:     * ImageSaverInterface method implementations.
 119:     */
 120:    //=============================================================================
 121:    public String getFormatCode() { return FORMAT_CODE; }
 122:    public String getFormatString() { return FORMAT_COMPLETE_NAME; }
 123:    public String getFormatExtension() { return FORMAT_EXTENSION; }
 124:    
 125:    //=============================================================================
 126:    // saveIt
 127:    //=============================================================================
 128:    /**
 129:     * Save the image.
 130:     */
 131:    //=============================================================================
 132:	      public boolean saveIt() {
 133:    ImageProducer    ip;
 134:    
 135:    // Verify that we have an image.
 136:    if ( saveImage == null ) return false;
 137:    
 138:    // No status information yet.
 139:    saveStatus = 0;
 140:    
 141:    // Open the file.
 142:	      try {
 143:        writeFileHandle = new FileOutputStream(savePath);
 144:    } catch ( IOException e ) { System.out.println("IOException occurred opening FileOutputStream : " + e); }
 145:    
 146:    // Return straight away if we couldn't get a handle.
 147:    if ( writeFileHandle == null ) return false;
 148:    
 149:    // Set us up as image consumer and start producing.
 150:    ip = saveImage.getSource();
 151:    if ( ip == null ) return false;
 152:    ip.startProduction(this);
 153:    
 154:    // Nothing more to do, just get data and close file at the end.
 155:    return true;
 156:    }
 157:    
 158:    //=============================================================================
 159:    // checkSave
 160:    //=============================================================================
 161:    /**
 162:     * Return ImageObserver constants for indicating the state of the image saving.
 163:     *
 164:     * @author Kary FRÄMLING 30/4/1998.
 165:     */
 166:    //=============================================================================
 167:	      public int checkSave() {
 168:    return saveStatus;
 169:    }
 170:    
 171:    //=============================================================================
 172:    // getUseAlphaChannel
 173:    //=============================================================================
 174:    /**
 175:     * Returns if an alpha channel is to be used or not.
 176:     *
 177:     * @author Kary FRÄMLING
 178:     */
 179:    //=============================================================================
 180:	      public boolean getUseAlphaChannel() {
 181:    return includeAlphaChannel;
 182:    }
 183:    
 184:    //=============================================================================
 185:    // setUseAlphaChannel
 186:    //=============================================================================
 187:    /**
 188:     * Indicate if an alpha channel should be used or not.
 189:     *
 190:     * @author Kary FRÄMLING
 191:     */
 192:    //=============================================================================
 193:	      public void setUseAlphaChannel(boolean use) {
 194:    includeAlphaChannel = use;
 195:    if ( includeAlphaChannel ) bytesPerPixel = 4;
 196:    else bytesPerPixel = 3;
 197:    }
 198:    
 199:    //=============================================================================
 200:    /**
 201:     * ImageConsumer method implementations.
 202:     */
 203:    //=============================================================================
 204:	      public void setProperties(Hashtable props) {
 205:    saveStatus |= ImageObserver.PROPERTIES;
 206:    }
 207:    
 208:    public void setHints(int hintflags) {}
 209:    
 210:    //=============================================================================
 211:    // setColorModel
 212:    //=============================================================================
 213:    /**
 214:     * If the default color model is an indexed color model and if it has a
 215:     * transparency pixel, then we put in an alpha channel.
 216:     */
 217:    //=============================================================================
 218:	      public void setColorModel(ColorModel model) {
 219:    // Change alpha channel mode only if the header isn't written yet.
 220:	      if ( !headerIsWritten && model instanceof IndexColorModel ) {
 221:	          if ( ((IndexColorModel) model).getTransparentPixel() != -1 ) {
 222:        includeAlphaChannel = true;
 223:        bytesPerPixel = 4;
 224:        }
 225:    }
 226:    }
 227:    
 228:    //=============================================================================
 229:    // setDimensions
 230:    //=============================================================================
 231:    /**
 232:     */
 233:    //=============================================================================
 234:	      public void setDimensions(int w, int h) {
 235:    // Store width and height.
 236:    width = w;
 237:    height = h;
 238:    
 239:    // Update save status.
 240:    saveStatus |= ImageObserver.WIDTH | ImageObserver.HEIGHT;
 241:    }
 242:    
 243:    //=============================================================================
 244:    // setPixels
 245:    //=============================================================================
 246:    /**
 247:     * Write the pixels into the file as RGB data. For this to work correctly,
 248:     * pixels should be delivered in topdownleftright order with complete
 249:     * scanlines. If we have several lines, the lines should be complete scanlines,
 250:     * otherwise the saving fails.
 251:     * @see ImageConsumer.
 252:     */
 253:    //=============================================================================
 254:    public void setPixels(int x, int y, int w, int h,
 255:    ColorModel model, byte pixels[], int off,
 256:	      int scansize) {
 257:    int        i, rgb;
 258:    int        transparent_pixel = -1;
 259:    int[]    int_pix_buf;
 260:    
 261:    // Change alpha channel mode only if the header isn't written yet.
 262:    // We only use an alpha channel if the model has a transparent color.
 263:    // This is allright if only one color model is used or if the first
 264:    // one used has a transparent color. Otherwise there will be no
 265:    // alpha channel.
 266:	      if ( !headerIsWritten && model instanceof IndexColorModel ) {
 267:	          if ( ((IndexColorModel) model).getTransparentPixel() != -1 ) {
 268:        includeAlphaChannel = true;
 269:        bytesPerPixel = 4;
 270:        }
 271:    }
 272:    
 273:    // If we are using an alpha channel, then get the transparent pixel
 274:    // index if any.
 275:	      if ( includeAlphaChannel && model instanceof IndexColorModel ) {
 276:        transparent_pixel = ((IndexColorModel) model).getTransparentPixel();
 277:    }
 278:    
 279:    // Transform the byte array into the corresponding RGB array.
 280:    int_pix_buf = new int[pixels.length];
 281:	      for ( i = off ; i < pixels.length ; i++ ) {
 282:        // Funny, we have to mask out the last byte for this to work correctly.
 283:        int_pix_buf[i] = model.getRGB(pixels[i]&0xFF);
 284:        
 285:        // Set alpha channel value if applicable.
 286:        if ( includeAlphaChannel && (pixels[i]&0xFF) == transparent_pixel )
 287:        int_pix_buf[i] &= 0xFFFFFF;
 288:    }
 289:    
 290:    // Call the int array method.
 291:    setPixels(x, y, w, h, ColorModel.getRGBdefault(), int_pix_buf, off, scansize);
 292:    }
 293:    
 294:    //=============================================================================
 295:    // setPixels
 296:    //=============================================================================
 297:    /**
 298:     * @see ImageConsumer.
 299:     */
 300:    //=============================================================================
 301:    public void setPixels(int x, int y, int w, int h,
 302:    ColorModel model, int pixels[], int off,
 303:	      int scansize) {
 304:    int        i, j, buf_off, rgb, deflate_size, in_byte_off;
 305:    byte[]    buf;
 306:    byte[]    deflate_buf = new byte[DEFLATE_INPUT_SIZE];
 307:    byte[]    four_byte_buf = new byte[4];
 308:    int        r, g, b, alpha;
 309:    int        old_r, old_g, old_b;
 310:    
 311:    // Write out the header if it is not written yet.
 312:    if ( !headerIsWritten ) writeHeader();
 313:    
 314:    // Fill the pixel buffer to pack. We suppose that we always get
 315:    // entire scanlines if "h" is bigger than 1. The "+ h" is because
 316:    // we insert a filtering indication byte at the beginning of each
 317:    // scanline.
 318:    buf = new byte[w*h*bytesPerPixel + h];
 319:    buf_off = 0;
 320:    old_r = old_g = old_b = 0;
 321:	      for ( i = 0 ; i < h ; i++ ) {
 322:        // Set the filtering byte in the beginning of the scanline.
 323:        if ( x == 0 ) buf[buf_off++] = NO_FILTER;
 324:        //            if ( x == 0 ) buf[buf_off++] = SUB_FILTER;
 325:        
 326:        // Treat the scanline.
 327:	          for ( j = 0 ; j < w ; j++ ) {
 328:        // Get the RGB value in one go.
 329:        rgb = model.getRGB(pixels[off + i*scansize + j]);
 330:        
 331:        // Set red, green and blue components.
 332:        r = ((rgb>>16)&0xFF);
 333:        g = ((rgb>>8)&0xFF);
 334:        b = (rgb&0xFF);
 335:        buf[buf_off++] = (byte) (r&0xFF);
 336:        buf[buf_off++] = (byte) (g&0xFF);
 337:        buf[buf_off++] = (byte) (b&0xFF);
 338:/*                buf[buf_off++] = (byte) ((r - old_r)&0xFF);
 339:                buf[buf_off++] = (byte) ((g - old_g)&0xFF);
 340:                buf[buf_off++] = (byte) ((b - old_b)&0xFF);
 341:                old_r = r;
 342:                old_g = g;
 343:                old_b = b; */
 344:        
 345:        // If we have an alpha channel to include, then add it after
 346:        // the RGB values.
 347:	          if ( includeAlphaChannel ) {
 348:            alpha = model.getAlpha(pixels[off + i*scansize + j]);
 349:            buf[buf_off++] = (byte) (alpha&0xFF);
 350:        }
 351:        }
 352:    }
 353:    
 354:    // Compress the data and write it out as it is compressed.
 355:    // We do this in 512 byte chunks, because it seems like
 356:    // doing one big setInput gives a stack overflow.
 357:	      for ( in_byte_off = 0 ; in_byte_off < buf.length ; in_byte_off += DEFLATE_INPUT_SIZE ) {
 358:        pixmapDeflater.setInput(buf, in_byte_off, (int) Math.min(DEFLATE_INPUT_SIZE, buf.length - in_byte_off));
 359:	          while ( !pixmapDeflater.needsInput() ) {
 360:        deflate_size = pixmapDeflater.deflate(deflate_buf);
 361:        try { writeFileHandle.write(deflate_buf, 0, deflate_size); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 362:        byteCount += deflate_size;
 363:        idatDataSize += deflate_size;
 364:        crcCounter.update(deflate_buf, 0, deflate_size);
 365:        }
 366:    }
 367:    
 368:    // Update save status.
 369:    saveStatus |= ImageObserver.SOMEBITS;
 370:    }
 371:    
 372:    //=============================================================================
 373:    // imageComplete
 374:    //=============================================================================
 375:    /**
 376:     * Get imageComplete message so that we can close the output file.
 377:     * @see ImageConsumer.
 378:     */
 379:    //=============================================================================
 380:	      public void imageComplete(int status) {
 381:    int        deflate_size;
 382:    byte[]    four_byte_buf = new byte[4];
 383:    byte[]    deflate_buf = new byte[DEFLATE_INPUT_SIZE];
 384:    
 385:    // Finish everything if it has not been done yet.
 386:	      if ( !imageCompleteDone ) {
 387:        // Write out the remaining pixel data and add the CRC of the IDAT chunk.
 388:        pixmapDeflater.finish();
 389:	          while ( !pixmapDeflater.finished() ) {
 390:        deflate_size = pixmapDeflater.deflate(deflate_buf);
 391:        try { writeFileHandle.write(deflate_buf, 0, deflate_size); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 392:        byteCount += deflate_size;
 393:        idatDataSize += deflate_size;
 394:        crcCounter.update(deflate_buf, 0, deflate_size);
 395:        }
 396:        ByteArrayConversion.ulongAsBytesBE(crcCounter.getValue(), four_byte_buf, 0, four_byte_buf.length);
 397:        try { writeFileHandle.write(four_byte_buf); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 398:        byteCount += four_byte_buf.length;
 399:        
 400:        // Data length. 0 for IEND.
 401:        ByteArrayConversion.uintAsBytesBE(0, four_byte_buf, 0, four_byte_buf.length);
 402:        try { writeFileHandle.write(four_byte_buf); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 403:        byteCount += four_byte_buf.length;
 404:        
 405:        // IEND type.
 406:        try { writeFileHandle.write(IEND_CHUNK_TYPE); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 407:        byteCount += IEND_CHUNK_TYPE.length;
 408:        
 409:        // Chunk CRC.
 410:        crcCounter.reset();
 411:        crcCounter.update(IEND_CHUNK_TYPE);
 412:        ByteArrayConversion.ulongAsBytesBE(crcCounter.getValue(), four_byte_buf, 0, four_byte_buf.length);
 413:        try { writeFileHandle.write(four_byte_buf); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 414:        byteCount += four_byte_buf.length;
 415:        
 416:        // Close the file and reopen in random access mode. This is just for setting
 417:        // the size of the IDAT chunk data.
 418:	          if ( writeFileHandle != null ) {
 419:        try { writeFileHandle.close(); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 420:        }
 421:	          try {
 422:        RandomAccessFile rf = new RandomAccessFile(savePath, "rw");
 423:        rf.seek(idatChunkSizeOffset);
 424:        ByteArrayConversion.ulongAsBytesBE(idatDataSize, four_byte_buf, 0, four_byte_buf.length);
 425:        rf.write(four_byte_buf);
 426:        rf.close();
 427:        } catch ( IOException e ) { System.out.println("IOException occurred opening RandomAccessFile : " + e); }
 428:        
 429:        // We are ready with imageComplete treatment.
 430:        imageCompleteDone = true;
 431:    }
 432:    
 433:    // Update save status.
 434:    saveStatus |= ImageObserver.ALLBITS;
 435:    }
 436:    
 437:    //--------------------------------------------------------------------------------------
 438:    // Private methods.
 439:    //--------------------------------------------------------------------------------------
 440:    
 441:    //=============================================================================
 442:    // writeHeader
 443:    //=============================================================================
 444:    /**
 445:     * Writes out the entire PNG header and the start of the IDAT chunk. It should
 446:     * be definitely known at this time if we use an alpha channel or not.
 447:     */
 448:    //=============================================================================
 449:	      private synchronized void writeHeader() {
 450:    int        i, off;
 451:    long    crc;
 452:    byte[]    buf;
 453:    byte[]    one_byte_buf = new byte[1];
 454:    byte[]    two_byte_buf = new byte[2];
 455:    byte[]    four_byte_buf = new byte[4];
 456:    
 457:    // No bytes written yet.
 458:    byteCount = 0;
 459:    
 460:    // Write the PNG identification byte array.
 461:    try { writeFileHandle.write(PNG_IDENTIFICATION_BYTES); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 462:    byteCount += PNG_IDENTIFICATION_BYTES.length;
 463:    
 464:    // Write out header chunk.
 465:    ByteArrayConversion.uintAsBytesBE(IHDR_DATA_SIZE, four_byte_buf, 0, 4);
 466:    // Data length.
 467:    try { writeFileHandle.write(four_byte_buf); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 468:    byteCount += four_byte_buf.length;
 469:    // IHDR type.
 470:    try { writeFileHandle.write(IHDR_CHUNK_TYPE); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 471:    byteCount += IHDR_CHUNK_TYPE.length;
 472:    // Chunk data.
 473:    buf = new byte[IHDR_DATA_SIZE];
 474:    off = 0;
 475:    ByteArrayConversion.uintAsBytesBE(width, buf, off, off + 4); off += 4;
 476:    ByteArrayConversion.uintAsBytesBE(height, buf, off, off + 4); off += 4;
 477:    buf[off++] = 8;
 478:    if ( includeAlphaChannel ) buf[off++] = COLOR_TYPE_COLOR + COLOR_TYPE_USE_ALPHA_CHANNEL;
 479:    else buf[off++] = COLOR_TYPE_COLOR;
 480:    buf[off++] = DEFLATE_INFLATE_COMPRESSION;
 481:    buf[off++] = 0;
 482:    buf[off++] = NO_INTERLACE;
 483:    try { writeFileHandle.write(buf); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 484:    byteCount += buf.length;
 485:    // Chunk CRC.
 486:    crcCounter.reset();
 487:    crcCounter.update(IHDR_CHUNK_TYPE);
 488:    crcCounter.update(buf);
 489:    ByteArrayConversion.ulongAsBytesBE(crcCounter.getValue(), four_byte_buf, 0, four_byte_buf.length);
 490:    try { writeFileHandle.write(four_byte_buf); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 491:    byteCount += four_byte_buf.length;
 492:    
 493:    // The header is written, so don't write it again if ever this method
 494:    // gets called a second time.
 495:    headerIsWritten = true;
 496:    
 497:    // Write the first IDAT chunk and set up the deflater. For the moment,
 498:    // we suppose that one chunk will be sufficient for storing any resonable
 499:    // image. This is mainly limited by the chunk size field (4 bytes),
 500:    // which means that the maximal size is 2^31 - 1 bytes
 501:    // if we take the maximal unsigned value.
 502:    pixmapDeflater = new Deflater();
 503:    
 504:    // Write out the data length. This is not known yet, so wa have to
 505:    // come back afterwards in direct access mode and set it.
 506:    idatChunkSizeOffset = byteCount;
 507:    ByteArrayConversion.uintAsBytesBE(0, four_byte_buf, 0, four_byte_buf.length);
 508:    try { writeFileHandle.write(four_byte_buf); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 509:    byteCount += four_byte_buf.length;
 510:    idatDataSize = 0;
 511:    
 512:    // IDAT type.
 513:    try { writeFileHandle.write(IDAT_CHUNK_TYPE); } catch ( IOException e ) { saveStatus = ImageObserver.ERROR; }
 514:    byteCount += IDAT_CHUNK_TYPE.length;
 515:    
 516:    // Then we reset the CRC counter and just add the type field.
 517:    crcCounter.reset();
 518:    crcCounter.update(IDAT_CHUNK_TYPE);
 519:    }
 520:    
 521:}
 522: