001 /* 002 * PCDCodec 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.IOException; 011 import java.io.RandomAccessFile; 012 import net.sourceforge.jiu.codecs.ImageCodec; 013 import net.sourceforge.jiu.codecs.InvalidFileStructureException; 014 import net.sourceforge.jiu.codecs.UnsupportedTypeException; 015 import net.sourceforge.jiu.codecs.WrongFileFormatException; 016 import net.sourceforge.jiu.color.YCbCrIndex; 017 import net.sourceforge.jiu.color.conversion.PCDYCbCrConversion; 018 import net.sourceforge.jiu.data.Gray8Image; 019 import net.sourceforge.jiu.data.IntegerImage; 020 import net.sourceforge.jiu.data.MemoryGray8Image; 021 import net.sourceforge.jiu.data.MemoryRGB24Image; 022 import net.sourceforge.jiu.data.RGB24Image; 023 import net.sourceforge.jiu.ops.MissingParameterException; 024 import net.sourceforge.jiu.ops.OperationFailedException; 025 import net.sourceforge.jiu.util.ArrayRotation; 026 import net.sourceforge.jiu.util.ArrayScaling; 027 028 /** 029 * A codec to read Kodak Photo-CD (image pac) image files. 030 * Typical file extension is <code>.pcd</code>. 031 * PCD is designed to store the same image in several resolutions. 032 * Not all resolutions are always present in a file. 033 * Typically, the first five resolutions are available and the file size 034 * is between four and six megabytes. 035 * Lossless compression (Huffman encoding) is used to store the higher resolution images. 036 * All images are in 24 bit YCbCr colorspace, with a component subsampling of 4:1:1 (Y:Cb:Cr) 037 * in both horizontal and vertical direction. 038 * <h3>Limitations</h3> 039 * Only the lowest three resolutions are supported by this codec. 040 * <h3>Sample PCD files</h3> 041 * You can download sample PCD image files from 042 * <a href="http://www.kodak.com/digitalImaging/samples/imageIntro.shtml">Kodak's 043 * website</a>. 044 * 045 * @author Marco Schmidt 046 */ 047 public class PCDCodec extends ImageCodec implements YCbCrIndex 048 { 049 /** 050 * Base/16, the minimum pixel resolution, 192 x 128 pixels. 051 */ 052 public static final int PCD_RESOLUTION_1 = 0; 053 054 /** 055 * Base/4, the second pixel resolution, 384 x 256 pixels. 056 */ 057 public static final int PCD_RESOLUTION_2 = 1; 058 059 /** 060 * Base, the third pixel resolution, 768 x 512 pixels. 061 */ 062 public static final int PCD_RESOLUTION_3 = 2; 063 064 /** 065 * Base*4, the fourth pixel resolution, 1536 x 1024 pixels. <em>Unsupported</em> 066 */ 067 public static final int PCD_RESOLUTION_4 = 3; 068 069 /** 070 * Base*16, the fifth pixel resolution, 3072 x 2048 pixels. <em>Unsupported</em> 071 */ 072 public static final int PCD_RESOLUTION_5 = 4; 073 074 /** 075 * Base*64, the sixth pixel resolution, 6144 x 4096 pixels. <em>Unsupported</em> 076 */ 077 public static final int PCD_RESOLUTION_6 = 5; 078 079 /** 080 * Index for the default resolution , Base ({@link #PCD_RESOLUTION_3}). 081 */ 082 public static final int PCD_RESOLUTION_DEFAULT = PCD_RESOLUTION_3; 083 084 /** 085 * This two-dimensional int array holds all possible pixel resolutions for 086 * a PCD file. Use one of the PCD resolution constants (e.g. 087 * {@link #PCD_RESOLUTION_3} as first index. 088 * The second index must be 0 or 1 and leads to either width or 089 * height. 090 * Example: <code>PCD_RESOLUTION[PCD_RESOLUTION_3][1]</code> will evalute 091 * as 512, which can be width or height, depending on the image being 092 * in landscape or portrait mode. 093 * You may want to use these resolution values in your program 094 * to prompt the user which resolution to load from the file. 095 */ 096 public static final int[][] PCD_RESOLUTIONS = 097 {{192, 128}, {384, 256}, {768, 512}, 098 {1536, 1024}, {3072, 2048}, {6144, 4096}}; 099 // offsets into the file for the three uncompressed resolutions 100 private static final long[] PCD_FILE_OFFSETS = 101 {0x2000, 0xb800, 0x30000}; 102 private static final long[] PCD_BASE_LENGTH = 103 {0x2000, 0xb800, 0x30000}; 104 // some constants to understand the orientation of an image 105 private static final int NO_ROTATION = 0; 106 private static final int ROTATE_90_LEFT = 1; 107 private static final int ROTATE_180 = 2; 108 private static final int ROTATE_90_RIGHT = 3; 109 // 2048 bytes 110 private static final int SECTOR_SIZE = 0x800; 111 // "PCD_IPI" 112 private static final byte[] MAGIC = 113 {0x50, 0x43, 0x44, 0x5f, 0x49, 0x50, 0x49}; 114 private boolean performColorConversion; 115 private boolean monochrome; 116 private int numChannels; 117 private int resolutionIndex; 118 private RandomAccessFile in; 119 private byte[][] data; 120 121 /** 122 * This constructor chooses the default settings for PCD image loading: 123 * <ul> 124 * <li>load color image (all channels, not only luminance)</li> 125 * <li>perform color conversion from PCD's native YCbCr color space to RGB</li> 126 * <li>load the image in the default resolution 127 * {@link #PCD_RESOLUTION_DEFAULT}, 768 x 512 pixels (or vice versa)</li> 128 * </ul> 129 */ 130 public PCDCodec() 131 { 132 super(); 133 setColorConversion(true); 134 setMonochrome(false); 135 setResolutionIndex(PCD_RESOLUTION_DEFAULT); 136 } 137 138 private byte[][] allocateMemory() 139 { 140 int numPixels = PCD_RESOLUTIONS[resolutionIndex][0] * 141 PCD_RESOLUTIONS[resolutionIndex][1]; 142 byte[][] result = new byte[numChannels][]; 143 for (int i = 0; i < numChannels; i++) 144 { 145 result[i] = new byte[numPixels]; 146 } 147 return result; 148 } 149 150 private void checkByteArray( 151 byte[][] data, 152 int numPixels) throws IllegalArgumentException 153 { 154 // check if array is non-null 155 if (data == null) 156 { 157 throw new IllegalArgumentException("Error: Image channel array is not initialized."); 158 } 159 // check if array has enough entries 160 int channels; 161 if (monochrome) 162 { 163 channels = 1; 164 if (data.length < 1) 165 { 166 throw new IllegalArgumentException("Error: Image channel " + 167 "array must have at least one channel for monochrome " + 168 "images."); 169 } 170 } 171 else 172 { 173 channels = 3; 174 if (data.length < 3) 175 { 176 throw new IllegalArgumentException("Error: Image channel " + 177 "array must have at least three channels for color images."); 178 } 179 } 180 // check if each channel has enough entries for the samples 181 for (int i = 0; i < channels; i++) 182 { 183 if (data[i].length < numPixels) 184 { 185 throw new IllegalArgumentException("Error: Image channel #" + i + 186 " is not large enough (" + numPixels + " entries required, " + 187 data[i].length + " found)."); 188 } 189 } 190 } 191 192 private void convertToRgb(int width, int height) 193 { 194 byte[] red = new byte[width]; 195 byte[] green = new byte[width]; 196 byte[] blue = new byte[width]; 197 int offset = 0; 198 for (int y = 0; y < height; y++) 199 { 200 PCDYCbCrConversion.convertYccToRgb( 201 data[INDEX_Y], 202 data[INDEX_CB], 203 data[INDEX_CR], 204 offset, 205 red, 206 green, 207 blue, 208 0, 209 width); 210 System.arraycopy(red, 0, data[0], offset, width); 211 System.arraycopy(green, 0, data[1], offset, width); 212 System.arraycopy(blue, 0, data[2], offset, width); 213 offset += width; 214 } 215 } 216 217 private IntegerImage createImage(int width, int height) 218 { 219 if (monochrome) 220 { 221 Gray8Image image = new MemoryGray8Image(width, height); 222 int offset = 0; 223 for (int y = 0; y < height; y++) 224 { 225 for (int x = 0; x < width; x++) 226 { 227 image.putByteSample(0, x, y, data[0][offset++]); 228 } 229 } 230 return image; 231 } 232 else 233 if (performColorConversion) 234 { 235 RGB24Image image = new MemoryRGB24Image(width, height); 236 int offset = 0; 237 for (int y = 0; y < height; y++) 238 { 239 for (int x = 0; x < width; x++) 240 { 241 image.putByteSample(RGB24Image.INDEX_RED, x, y, data[0][offset]); 242 image.putByteSample(RGB24Image.INDEX_GREEN, x, y, data[1][offset]); 243 image.putByteSample(RGB24Image.INDEX_BLUE, x, y, data[2][offset]); 244 offset++; 245 } 246 } 247 return image; 248 } 249 else 250 { 251 return null; 252 } 253 } 254 255 public String[] getFileExtensions() 256 { 257 return new String[] {".pcd"}; 258 } 259 260 public String getFormatName() 261 { 262 return "Kodak Photo-CD (PCD)"; 263 } 264 265 public String[] getMimeTypes() 266 { 267 return new String[] {"image/x-pcd"}; 268 } 269 270 public boolean isLoadingSupported() 271 { 272 return true; 273 } 274 275 public boolean isSavingSupported() 276 { 277 return false; 278 } 279 280 /** 281 * Attempts to load an image. 282 * The codec must have been given an input stream, all other 283 * parameters (do not convert color to RGB, load monochrome channel 284 * only, load other resolution than default) can optionally be 285 * chosen by calling the corresponding methods. 286 * 287 * @return loaded image 288 * @throws IOException if there were reading errors 289 * @throws OutOfMemoryException if there was not enough free memory 290 * available 291 * @throws InvalidFileStructureException if the file seems to be a PCD 292 * stream but has logical errors in it 293 * @throws WrongFileFormatException if this is not a PCD file 294 */ 295 private void load() throws 296 InvalidFileStructureException, 297 IOException, 298 UnsupportedTypeException, 299 WrongFileFormatException 300 { 301 if (resolutionIndex != PCD_RESOLUTION_1 && 302 resolutionIndex != PCD_RESOLUTION_2 && 303 resolutionIndex != PCD_RESOLUTION_3) 304 { 305 throw new UnsupportedTypeException("Error reading PCD input " + 306 "stream. Only the three lowest resolutions are supported."); 307 } 308 if (in == null) 309 { 310 throw new IllegalArgumentException("Input file is missing " + 311 "(use PCDCodec.setInput(RandomAccessFile)."); 312 } 313 if (in.length() < 16 * 1024) 314 { 315 throw new WrongFileFormatException("Not a PCD file."); 316 } 317 byte[] sector = new byte[SECTOR_SIZE]; 318 // read first sector; first 7 bytes must be 0xff 319 in.readFully(sector); 320 for (int i = 0; i < 7; i++) 321 { 322 if (sector[i] != -1) 323 { 324 throw new WrongFileFormatException("Input is not a valid PCD " + 325 "file (wrong magic byte sequence)."); 326 } 327 } 328 // read second sector and check more magic bytes 329 in.readFully(sector); 330 for (int i = 0; i < MAGIC.length; i++) 331 { 332 if (sector[i] != MAGIC[i]) 333 { 334 throw new WrongFileFormatException("Input is not a valid PCD " + 335 "file (wrong magic byte sequence)."); 336 } 337 } 338 // get image orientation and resolution 339 int rotationAngle = sector[0x602] & 0x03; 340 int width = PCD_RESOLUTIONS[resolutionIndex][0]; 341 int height = PCD_RESOLUTIONS[resolutionIndex][1]; 342 int realWidth = width; 343 int realHeight = height; 344 if (rotationAngle == ROTATE_90_LEFT || rotationAngle == ROTATE_90_RIGHT) 345 { 346 realWidth = height; 347 realHeight = width; 348 } 349 if (!hasBounds()) 350 { 351 setBounds(0, 0, realWidth - 1, realHeight - 1); 352 } 353 // determine which uncompressed image will be loaded 354 int uncompressedResolution = resolutionIndex; 355 if (resolutionIndex > PCD_RESOLUTION_3) 356 { 357 uncompressedResolution = PCD_RESOLUTION_3; 358 } 359 // load uncompressed image 360 data = allocateMemory(); 361 loadUncompressedImage(uncompressedResolution); 362 // reverse color subsampling if necessary 363 if (!monochrome) 364 { 365 ArrayScaling.scaleUp200Percent(data[INDEX_CB], 366 PCD_RESOLUTIONS[uncompressedResolution][0] / 2, 367 PCD_RESOLUTIONS[uncompressedResolution][1] / 2); 368 ArrayScaling.scaleUp200Percent(data[INDEX_CR], 369 PCD_RESOLUTIONS[uncompressedResolution][0] / 2, 370 PCD_RESOLUTIONS[uncompressedResolution][1] / 2); 371 } 372 // TODO load higher resolution by decoding differences to uncompressed image 373 // ... 374 // convert to RGB color space if possible and desired 375 if ((!monochrome) && performColorConversion) 376 { 377 convertToRgb(width, height); 378 } 379 // rotate the image if necessary 380 rotateArrays(rotationAngle, width, height); 381 // adjust width and height 382 if (rotationAngle == ROTATE_90_LEFT || rotationAngle == ROTATE_90_RIGHT) 383 { 384 int temp = width; 385 width = height; 386 height = temp; 387 } 388 setImage(createImage(width, height)); 389 } 390 391 /** 392 * Loads one of the three lowest resolution images from the file. 393 * First skips as many bytes as there are between the current 394 * stream offset and the offset of the image in the PCD file 395 * (first three images are at fixed positions). 396 * Then reads the pixels from in to data. 397 * <p> 398 * Note that there are <code>width</code> times <code>height</code> 399 * samples for Y, but only one fourth that many samples for each Cb and Cr 400 * (because of the 4:1:1 subsampling of the two chroma components). 401 * <p> 402 * @param resolution one of PCD_RESOLUTION_1, PCD_RESOLUTION_2 or PCD_RESOLUTION_3 403 * @throws an IOException if there were any reading errors 404 */ 405 private void loadUncompressedImage(int resolution) 406 throws IllegalArgumentException, IOException 407 { 408 if (resolution != PCD_RESOLUTION_1 && 409 resolution != PCD_RESOLUTION_2 && 410 resolution != PCD_RESOLUTION_3) 411 { 412 throw new IllegalArgumentException("Error loading " + 413 "PCD image, only the lowest three resolutions are " + 414 "uncompressed."); 415 } 416 in.seek(PCD_FILE_OFFSETS[resolution]); 417 int fullWidth = PCD_RESOLUTIONS[resolution][0]; 418 int fullHeight = PCD_RESOLUTIONS[resolution][1]; 419 int halfWidth = fullWidth / 2; 420 int halfHeight = fullHeight / 2; 421 int offset1 = 0; 422 int offset2 = 0; 423 for (int y = 0; y < halfHeight; y++) 424 { 425 // read two luminance rows 426 in.readFully(data[INDEX_Y], offset1, fullWidth * 2); 427 offset1 += (fullWidth * 2); 428 if (monochrome) 429 { 430 if (in.skipBytes(fullWidth) != fullWidth) 431 { 432 throw new IOException("Could not skip " + fullWidth + 433 " bytes."); 434 } 435 } 436 else 437 { 438 // read one row for each cb and cr 439 in.readFully(data[INDEX_CB], offset2, halfWidth); 440 in.readFully(data[INDEX_CR], offset2, halfWidth); 441 offset2 += halfWidth; 442 } 443 } 444 } 445 446 /** 447 * Checks the parameter and loads an image. 448 */ 449 public void process() throws 450 InvalidFileStructureException, 451 MissingParameterException, 452 OperationFailedException, 453 UnsupportedTypeException, 454 WrongFileFormatException 455 { 456 in = getRandomAccessFile(); 457 if (in == null) 458 { 459 throw new MissingParameterException("RandomAccessFile object needed in PCDCodec."); 460 } 461 if (getMode() != CodecMode.LOAD) 462 { 463 throw new UnsupportedTypeException("PCDCodec can only load images."); 464 } 465 try 466 { 467 load(); 468 } 469 catch (IOException ioe) 470 { 471 throw new OperationFailedException("I/O error: " + ioe.toString()); 472 } 473 } 474 475 private void rotateArrays(int rotationAngle, int width, int height) 476 { 477 if (rotationAngle == NO_ROTATION) 478 { 479 return; 480 } 481 int numPixels = width * height; 482 for (int i = 0; i < numChannels; i++) 483 { 484 byte[] dest = new byte[numPixels]; 485 switch(rotationAngle) 486 { 487 case(ROTATE_90_LEFT): 488 { 489 ArrayRotation.rotate90Left(width, height, data[i], 0, dest, 0); 490 break; 491 } 492 case(ROTATE_90_RIGHT): 493 { 494 ArrayRotation.rotate90Right(width, height, data[i], 0, dest, 0); 495 break; 496 } 497 case(ROTATE_180): 498 { 499 ArrayRotation.rotate180(width, height, data[i], 0, dest, 0); 500 break; 501 } 502 } 503 System.arraycopy(dest, 0, data[i], 0, numPixels); 504 } 505 } 506 507 private void scaleUp(int currentResolution) 508 { 509 int width = PCD_RESOLUTIONS[currentResolution][0]; 510 int height = PCD_RESOLUTIONS[currentResolution][1]; 511 ArrayScaling.scaleUp200Percent(data[INDEX_Y], width, height); 512 if (!monochrome) 513 { 514 ArrayScaling.scaleUp200Percent(data[INDEX_CB], width, height); 515 ArrayScaling.scaleUp200Percent(data[INDEX_CR], width, height); 516 } 517 } 518 519 /** 520 * Specify whether color is converted from PCD's YCbCr color space to 521 * RGB color space. 522 * The default is <code>true</code>, and you should only change this 523 * if you really know what you are doing. 524 * If you simply want the luminance (gray) channel, use 525 * {@link #setMonochrome(boolean)} with <code>true</code> as parameter. 526 * @param performColorConversion boolean that determines whether color conversion is applied 527 */ 528 public void setColorConversion(boolean performColorConversion) 529 { 530 this.performColorConversion = performColorConversion; 531 } 532 533 public void setFile(String fileName, CodecMode codecMode) throws 534 IOException, 535 UnsupportedCodecModeException 536 { 537 if (codecMode == CodecMode.LOAD) 538 { 539 setRandomAccessFile(new RandomAccessFile(fileName, "r"), CodecMode.LOAD); 540 } 541 else 542 { 543 throw new UnsupportedCodecModeException("This PCD codec can only load images."); 544 } 545 } 546 547 /** 548 * Specifies whether the image is to be loaded as gray or color image. 549 * If argument is true, only the gray channel is loaded. 550 */ 551 public void setMonochrome(boolean monochrome) 552 { 553 this.monochrome = monochrome; 554 if (monochrome) 555 { 556 numChannels = 1; 557 } 558 else 559 { 560 numChannels = 3; 561 } 562 } 563 564 public void setResolutionIndex(int resolutionIndex) 565 { 566 this.resolutionIndex = resolutionIndex; 567 } 568 }