001/* 002 * $RCSfile: FileFormatReader.java,v $ 003 * $Revision: 1.2 $ 004 * $Date: 2005/04/28 01:25:38 $ 005 * $State: Exp $ 006 * 007 * Class: FileFormatReader 008 * 009 * Description: Read J2K file stream 010 * 011 * COPYRIGHT: 012 * 013 * This software module was originally developed by Raphaël Grosbois and 014 * Diego Santa Cruz (Swiss Federal Institute of Technology-EPFL); Joel 015 * Askelöf (Ericsson Radio Systems AB); and Bertrand Berthelot, David 016 * Bouchard, Félix Henry, Gerard Mozelle and Patrice Onno (Canon Research 017 * Centre France S.A) in the course of development of the JPEG2000 018 * standard as specified by ISO/IEC 15444 (JPEG 2000 Standard). This 019 * software module is an implementation of a part of the JPEG 2000 020 * Standard. Swiss Federal Institute of Technology-EPFL, Ericsson Radio 021 * Systems AB and Canon Research Centre France S.A (collectively JJ2000 022 * Partners) agree not to assert against ISO/IEC and users of the JPEG 023 * 2000 Standard (Users) any of their rights under the copyright, not 024 * including other intellectual property rights, for this software module 025 * with respect to the usage by ISO/IEC and Users of this software module 026 * or modifications thereof for use in hardware or software products 027 * claiming conformance to the JPEG 2000 Standard. Those intending to use 028 * this software module in hardware or software products are advised that 029 * their use may infringe existing patents. The original developers of 030 * this software module, JJ2000 Partners and ISO/IEC assume no liability 031 * for use of this software module or modifications thereof. No license 032 * or right to this software module is granted for non JPEG 2000 Standard 033 * conforming products. JJ2000 Partners have full right to use this 034 * software module for his/her own purpose, assign or donate this 035 * software module to any third party and to inhibit third parties from 036 * using this software module for non JPEG 2000 Standard conforming 037 * products. This copyright notice must be included in all copies or 038 * derivative works of this software module. 039 * 040 * Copyright (c) 1999/2000 JJ2000 Partners. 041 * */ 042package jj2000.j2k.fileformat.reader; 043 044import java.awt.Transparency; 045import java.awt.color.ColorSpace; 046import java.awt.color.ICC_ColorSpace; 047import java.awt.color.ICC_Profile; 048import java.awt.image.ColorModel; 049import java.awt.image.ComponentColorModel; 050import java.awt.image.DataBuffer; 051import java.awt.image.IndexColorModel; 052import java.io.EOFException; 053import java.io.IOException; 054import java.util.Vector; 055 056import jj2000.j2k.codestream.Markers; 057import jj2000.j2k.fileformat.FileFormatBoxes; 058import jj2000.j2k.io.RandomAccessIO; 059 060import com.github.jaiimageio.jpeg2000.impl.BitsPerComponentBox; 061import com.github.jaiimageio.jpeg2000.impl.Box; 062import com.github.jaiimageio.jpeg2000.impl.ChannelDefinitionBox; 063import com.github.jaiimageio.jpeg2000.impl.ColorSpecificationBox; 064import com.github.jaiimageio.jpeg2000.impl.ComponentMappingBox; 065import com.github.jaiimageio.jpeg2000.impl.DataEntryURLBox; 066import com.github.jaiimageio.jpeg2000.impl.FileTypeBox; 067import com.github.jaiimageio.jpeg2000.impl.HeaderBox; 068import com.github.jaiimageio.jpeg2000.impl.J2KMetadata; 069import com.github.jaiimageio.jpeg2000.impl.PaletteBox; 070import com.github.jaiimageio.jpeg2000.impl.ResolutionBox; 071import com.github.jaiimageio.jpeg2000.impl.SignatureBox; 072import com.github.jaiimageio.jpeg2000.impl.UUIDBox; 073import com.github.jaiimageio.jpeg2000.impl.UUIDListBox; 074import com.github.jaiimageio.jpeg2000.impl.XMLBox; 075 076/** 077 * This class reads the file format wrapper that may or may not exist around a 078 * valid JPEG 2000 codestream. Since no information from the file format is 079 * used in the actual decoding, this class simply goes through the file and 080 * finds the first valid codestream. 081 * 082 * @see jj2000.j2k.fileformat.writer.FileFormatWriter 083 * */ 084public class FileFormatReader implements FileFormatBoxes{ 085 086 /** The random access from which the file format boxes are read */ 087 private RandomAccessIO in; 088 089 /** The positions of the codestreams in the fileformat*/ 090 private Vector codeStreamPos; 091 092 /** The lengths of the codestreams in the fileformat*/ 093 private Vector codeStreamLength; 094 095 /** Create a IndexColorModel from the palette box if there is one */ 096 private ColorModel colorModel = null; 097 098 /** The meta data */ 099 private J2KMetadata metadata; 100 101 /** Parameters in header box */ 102 private int width; 103 private int height; 104 private int numComp; 105 private int bitDepth; 106 private int compressionType; 107 private int unknownColor; 108 private int intelProp; 109 110 /** Various bit depth */ 111 private byte[] bitDepths; 112 113 /** The lut in the palette box */ 114 private byte[][] lut; 115 private byte[] compSize; 116 117 /** Component mapping */ 118 private short[] comps; 119 private byte[] type; 120 private byte[] maps; 121 122 /** Channel definitions */ 123 private short[] channels ; 124 private short[] cType; 125 private short[] associations; 126 127 /** Color specification */ 128 private int colorSpaceType; 129 130 /** ICC profile */ 131 private ICC_Profile profile; 132 133 /** 134 * The constructor of the FileFormatReader 135 * 136 * @param in The RandomAccessIO from which to read the file format 137 * */ 138 public FileFormatReader(RandomAccessIO in, J2KMetadata metadata){ 139 this.in = in; 140 this.metadata = metadata; 141 } 142 143 144 /** 145 * This method checks whether the given RandomAccessIO is a valid JP2 file 146 * and if so finds the first codestream in the file. Currently, the 147 * information in the codestream is not used 148 * 149 * @param in The RandomAccessIO from which to read the file format 150 * 151 * @exception java.io.IOException If an I/O error ocurred. 152 * 153 * @exception java.io.EOFException If end of file is reached 154 * */ 155 public void readFileFormat() throws IOException, EOFException { 156 157 int foundCodeStreamBoxes=0; 158 int box; 159 int length; 160 long longLength=0; 161 int pos; 162 short marker; 163 boolean jp2HeaderBoxFound=false; 164 boolean lastBoxFound = false; 165 166 try{ 167 168 // Go through the randomaccessio and find the first 169 // contiguous codestream box. Check also that the File Format is 170 // correct 171 172 pos = in.getPos(); 173 174 // Make sure that the first 12 bytes is the JP2_SIGNATURE_BOX 175 // or if not that the first 2 bytes is the SOC marker 176 if(in.readInt() != 0x0000000c || 177 in.readInt() != JP2_SIGNATURE_BOX || 178 in.readInt() != 0x0d0a870a){ // Not a JP2 file 179 in.seek(pos); 180 181 marker = (short)in.readShort(); 182 if(marker != Markers.SOC) //Standard syntax marker found 183 throw new Error("File is neither valid JP2 file nor "+ 184 "valid JPEG 2000 codestream"); 185 in.seek(pos); 186 if(codeStreamPos == null) 187 codeStreamPos = new Vector(); 188 codeStreamPos.addElement(new Integer(pos)); 189 return; 190 } 191 192 if (metadata != null) 193 metadata.addNode(new SignatureBox()); 194 195 // Read all remaining boxes 196 while(!lastBoxFound){ 197 pos = in.getPos(); 198 length = in.readInt(); 199 if((pos+length) == in.length()) 200 lastBoxFound = true; 201 202 box = in.readInt(); 203 if (length == 0) { 204 lastBoxFound = true; 205 length = in.length()-in.getPos(); 206 } else if(length == 1) { 207 longLength = in.readLong(); 208 throw new IOException("File too long."); 209 } else longLength = (long) 0; 210 211 pos = in.getPos(); 212 length -= 8; 213 214 switch(box){ 215 case FILE_TYPE_BOX: 216 readFileTypeBox(length + 8, longLength); 217 break; 218 case CONTIGUOUS_CODESTREAM_BOX: 219 if(!jp2HeaderBoxFound) 220 throw new Error("Invalid JP2 file: JP2Header box not "+ 221 "found before Contiguous codestream "+ 222 "box "); 223 readContiguousCodeStreamBox(length + 8, longLength); 224 break; 225 case JP2_HEADER_BOX: 226 if(jp2HeaderBoxFound) 227 throw new Error("Invalid JP2 file: Multiple "+ 228 "JP2Header boxes found"); 229 readJP2HeaderBox(length + 8); 230 jp2HeaderBoxFound = true; 231 length = 0; 232 break; 233 case IMAGE_HEADER_BOX: 234 readImageHeaderBox(length); 235 break; 236 case INTELLECTUAL_PROPERTY_BOX: 237 readIntPropertyBox(length); 238 break; 239 case XML_BOX: 240 readXMLBox(length); 241 break; 242 case UUID_INFO_BOX: 243 length = 0; 244 break; 245 case UUID_BOX: 246 readUUIDBox(length); 247 break; 248 case UUID_LIST_BOX: 249 readUUIDListBox(length); 250 break; 251 case URL_BOX: 252 readURLBox(length); 253 break; 254 case PALETTE_BOX: 255 readPaletteBox(length + 8); 256 break; 257 case BITS_PER_COMPONENT_BOX: 258 readBitsPerComponentBox(length); 259 break; 260 case COMPONENT_MAPPING_BOX: 261 readComponentMappingBox(length); 262 break; 263 case COLOUR_SPECIFICATION_BOX: 264 readColourSpecificationBox(length); 265 break; 266 case CHANNEL_DEFINITION_BOX: 267 readChannelDefinitionBox(length); 268 break; 269 case RESOLUTION_BOX: 270 length = 0; 271 break; 272 case CAPTURE_RESOLUTION_BOX: 273 case DEFAULT_DISPLAY_RESOLUTION_BOX: 274 readResolutionBox(box, length); 275 break; 276 default: 277 if (metadata != null) { 278 byte[] data = new byte[length]; 279 in.readFully(data, 0, length); 280 metadata.addNode(new Box(length + 8, 281 box, 282 longLength, 283 data)); 284 } 285 } 286 if(!lastBoxFound) 287 in.seek(pos+length); 288 } 289 }catch( EOFException e ){ 290 throw new Error("EOF reached before finding Contiguous "+ 291 "Codestream Box"); 292 } 293 294 if(codeStreamPos.size() == 0){ 295 // Not a valid JP2 file or codestream 296 throw new Error("Invalid JP2 file: Contiguous codestream box "+ 297 "missing"); 298 } 299 300 return; 301 } 302 303 /** 304 * This method reads the File Type box 305 * 306 * @return false if the File Type box was not found or invalid else true 307 * 308 * @exception java.io.IOException If an I/O error ocurred. 309 * 310 * @exception java.io.EOFException If the end of file was reached 311 * */ 312 public boolean readFileTypeBox(int length, long longLength) 313 throws IOException, EOFException { 314 int nComp; 315 boolean foundComp=false; 316 317 // Check for XLBox 318 if(length == 1) { // Box has 8 byte length; 319 longLength = in.readLong(); 320 throw new IOException("File too long."); 321 } 322 323 // Check that this is a correct DBox value 324 // Read Brand field 325 if(in.readInt() != FT_BR) 326 return false; 327 328 // Read MinV field 329 int minorVersion = in.readInt(); 330 331 // Check that there is at least one FT_BR entry in in 332 // compatibility list 333 nComp = (length - 16)/4; // Number of compatibilities. 334 int[] comp = new int[nComp]; 335 for(int i=0; i < nComp; i++){ 336 if((comp[i] = in.readInt()) == FT_BR) 337 foundComp = true; 338 } 339 if(!foundComp) 340 return false; 341 342 if (metadata != null) 343 metadata.addNode(new FileTypeBox(FT_BR, minorVersion, comp)); 344 345 return true; 346 } 347 348 /** 349 * This method reads the JP2Header box 350 * 351 * @param pos The position in the file 352 * 353 * @param length The length of the JP2Header box 354 * 355 * @param long length The length of the JP2Header box if greater than 356 * 1<<32 357 * 358 * @return false if the JP2Header box was not found or invalid else true 359 * 360 * @exception java.io.IOException If an I/O error ocurred. 361 * 362 * @exception java.io.EOFException If the end of file was reached 363 * */ 364 public boolean readJP2HeaderBox(int length) 365 throws IOException, EOFException { 366 367 if(length == 0) // This can not be last box 368 throw new Error("Zero-length of JP2Header Box"); 369 370 // Here the JP2Header data (DBox) would be read if we were to use it 371 return true; 372 } 373 374 /** 375 * This method reads the Image Header box 376 * @param length The length of the JP2Header box 377 * 378 * @return false if the JP2Header box was not found or invalid else true 379 * 380 * @exception java.io.IOException If an I/O error ocurred. 381 * 382 * @exception java.io.EOFException If the end of file was reached 383 * */ 384 public boolean readImageHeaderBox(int length) 385 throws IOException, EOFException { 386 387 if(length == 0) // This can not be last box 388 throw new Error("Zero-length of JP2Header Box"); 389 390 // Here the JP2Header data (DBox) would be read if we were to use it 391 392 height = in.readInt(); 393 width = in.readInt(); 394 numComp = in.readShort(); 395 bitDepth = in.readByte(); 396 397 compressionType = in.readByte(); 398 unknownColor = in.readByte(); 399 intelProp = in.readByte(); 400 401 if (metadata != null) { 402 403 metadata.addNode(new HeaderBox(height, width, numComp, bitDepth, 404 compressionType, unknownColor, 405 intelProp)); 406 } 407 return true; 408 } 409 410 /** 411 * This method skips the Contiguous codestream box and adds position 412 * of contiguous codestream to a vector 413 * 414 * @param pos The position in the file 415 * 416 * @param length The length of the JP2Header box 417 * 418 * @param long length The length of the JP2Header box if greater than 1<<32 419 * 420 * @return false if the Contiguous codestream box was not found or invalid 421 * else true 422 * 423 * @exception java.io.IOException If an I/O error ocurred. 424 * 425 * @exception java.io.EOFException If the end of file was reached 426 * */ 427 public boolean readContiguousCodeStreamBox(int length, 428 long longLength) 429 throws IOException, EOFException { 430 431 // Add new codestream position to position vector 432 int ccpos = in.getPos(); 433 434 if(codeStreamPos == null) 435 codeStreamPos = new Vector(); 436 codeStreamPos.addElement(new Integer(ccpos)); 437 438 // Add new codestream length to length vector 439 if(codeStreamLength == null) 440 codeStreamLength = new Vector(); 441 codeStreamLength.addElement(new Integer(length)); 442 443 return true; 444 } 445 446 /** 447 * This method reads the contents of the Intellectual property box 448 * */ 449 public void readIntPropertyBox(int length) throws IOException { 450 if (metadata != null) { 451 byte[] data = new byte[length]; 452 in.readFully(data, 0, length); 453 metadata.addNode(new Box(length + 8, 0x6A703269, data)); 454 } 455 } 456 457 /** 458 * This method reads the contents of the XML box 459 */ 460 public void readXMLBox(int length) throws IOException { 461 if (metadata != null) { 462 byte[] data = new byte[length]; 463 in.readFully(data, 0, length); 464 metadata.addNode(new XMLBox(data)); 465 } 466 } 467 468 /** 469 * This method reads the contents of the XML box 470 */ 471 public void readURLBox(int length) throws IOException { 472 if (metadata != null) { 473 byte[] data = new byte[length]; 474 in.readFully(data, 0, length); 475 metadata.addNode(new DataEntryURLBox(data)); 476 } 477 } 478 479 /** 480 * This method reads the contents of the Intellectual property box 481 */ 482 public void readUUIDBox(int length) throws IOException { 483 if (metadata != null) { 484 byte[] data = new byte[length]; 485 in.readFully(data, 0, length); 486 metadata.addNode(new UUIDBox(data)); 487 } 488 } 489 490 /** 491 * This method reads the contents of the UUID List box 492 * */ 493 public void readUUIDListBox(int length) throws IOException { 494 if (metadata != null) { 495 byte[] data = new byte[length]; 496 in.readFully(data, 0, length); 497 metadata.addNode(new UUIDListBox(data)); 498 } 499 } 500 501 /** This method reads the content of the palette box */ 502 public void readPaletteBox(int length) throws IOException { 503 // Get current position in file 504 int pos = in.getPos(); 505 506 int lutSize = in.readShort(); 507 int numComp = in.readByte(); 508 compSize = new byte[numComp]; 509 510 for (int i = 0; i < numComp; i++) { 511 compSize[i] = (byte)in.readByte(); 512 } 513 514 lut = new byte[numComp][lutSize]; 515 516 for (int n=0; n < lutSize; n++) { 517 for (int c = 0; c < numComp; c++) { 518 int depth = 1 + (compSize[c] & 0x7F); 519 if (depth > 32) 520 depth = 32; 521 int numBytes = (depth + 7)>>3; 522 int mask = (1 << depth) - 1; 523 byte[] buf = new byte[numBytes]; 524 in.readFully(buf, 0, numBytes); 525 526 int val = 0; 527 528 for (int k = 0; k < numBytes; k++) { 529 val = buf[k] + (val << 8); 530 } 531 lut[c][n] = (byte)val; 532 } 533 } 534 if (metadata != null) { 535 metadata.addNode(new PaletteBox(length, compSize, lut)); 536 } 537 } 538 539 /** Read the component mapping channel. 540 */ 541 public void readComponentMappingBox(int length)throws IOException { 542 int num = length / 4; 543 544 comps = new short[num]; 545 type = new byte[num]; 546 maps = new byte[num]; 547 548 for (int i = 0; i < num; i++) { 549 comps[i] = in.readShort(); 550 type[i] = in.readByte(); 551 maps[i] = in.readByte(); 552 } 553 554 if (metadata != null) { 555 metadata.addNode(new ComponentMappingBox(comps, type, maps)); 556 } 557 } 558 559 /** 560 * This method reads the Channel Definition box 561 * 562 * @exception java.io.IOException If an I/O error ocurred. 563 * 564 */ 565 public void readChannelDefinitionBox(int length)throws IOException { 566 int num = in.readShort(); 567 channels = new short[num]; 568 cType = new short[num]; 569 associations = new short[num]; 570 571 for (int i = 0; i < num; i++) { 572 channels[i] = in.readShort(); 573 cType[i] = in.readShort(); 574 associations[i] = in.readShort(); 575 } 576 if (metadata != null) { 577 metadata.addNode(new ChannelDefinitionBox(channels, cType, associations)); 578 } 579 } 580 581 /** Read the bits per component. 582 */ 583 public void readBitsPerComponentBox(int length)throws IOException { 584 bitDepths = new byte[length]; 585 in.readFully(bitDepths, 0, length); 586 587 if (metadata != null) { 588 metadata.addNode(new BitsPerComponentBox(bitDepths)); 589 } 590 } 591 592 /** Read the color specifications. 593 */ 594 public void readColourSpecificationBox(int length)throws IOException { 595 // read METHOD field 596 byte method = (byte)in.readByte(); 597 598 // read PREC field 599 byte prec = (byte)in.readByte(); 600 601 // read APPROX field 602 byte approx = (byte)in.readByte(); 603 604 if (method == 2) { 605 byte[] data = new byte[length - 3]; 606 in.readFully(data, 0, data.length); 607 profile = ICC_Profile.getInstance(data); 608 } else // read EnumCS field 609 colorSpaceType = in.readInt(); 610 611 if (metadata != null) { 612 metadata.addNode(new ColorSpecificationBox(method, prec, approx, 613 colorSpaceType, 614 profile)); 615 } 616 } 617 618 /** Read the resolution. 619 */ 620 public void readResolutionBox(int type, int length)throws IOException { 621 byte[] data = new byte[length]; 622 in.readFully(data, 0, length); 623 if (metadata != null) { 624 metadata.addNode(new ResolutionBox(type, data)); 625 } 626 } 627 628 /** 629 * This method creates and returns an array of positions to contiguous 630 * codestreams in the file 631 * 632 * @return The positions of the contiguous codestreams in the file 633 * */ 634 public long[] getCodeStreamPos(){ 635 int size = codeStreamPos.size(); 636 long[] pos = new long[size]; 637 for(int i=0 ; i<size ; i++) 638 pos[i]=((Integer)(codeStreamPos.elementAt(i))).longValue(); 639 return pos; 640 } 641 642 /** 643 * This method returns the position of the first contiguous codestreams in 644 * the file 645 * 646 * @return The position of the first contiguous codestream in the file 647 * */ 648 public int getFirstCodeStreamPos(){ 649 return ((Integer)(codeStreamPos.elementAt(0))).intValue(); 650 } 651 652 /** 653 * This method returns the length of the first contiguous codestreams in 654 * the file 655 * 656 * @return The length of the first contiguous codestream in the file 657 * */ 658 public int getFirstCodeStreamLength(){ 659 return ((Integer)(codeStreamLength.elementAt(0))).intValue(); 660 } 661 662 /** 663 * Returns the color model created from the palette box. 664 */ 665 public ColorModel getColorModel() { 666 // Check 'numComp' instance variable here in case there is an 667 // embedded palette such as in the pngsuite images pp0n2c16.png 668 // and pp0n6a08.png. 669 if (lut != null && numComp == 1) { 670 int numComp = lut.length; 671 672 int maxDepth = 1 + (bitDepth & 0x7F); 673 674 if (maps == null) { 675 maps = new byte[numComp]; 676 for (int i = 0; i < numComp; i++) 677 maps[i] = (byte)i; 678 } 679 if (numComp == 3) 680 colorModel = new IndexColorModel(maxDepth, lut[0].length, 681 lut[maps[0]], 682 lut[maps[1]], 683 lut[maps[2]]); 684 else if (numComp == 4) 685 colorModel = new IndexColorModel(maxDepth, lut[0].length, 686 lut[maps[0]], 687 lut[maps[1]], 688 lut[maps[2]], 689 lut[maps[3]]); 690 } else if (channels != null){ 691 boolean hasAlpha = false; 692 int alphaChannel = numComp - 1; 693 694 for (int i = 0; i < channels.length; i++) { 695 if (cType[i] == 1 && channels[i] == alphaChannel) 696 hasAlpha = true; 697 } 698 699 boolean[] isPremultiplied = new boolean[] {false}; 700 701 if (hasAlpha) { 702 isPremultiplied = new boolean[alphaChannel]; 703 704 for (int i = 0; i < alphaChannel; i++) 705 isPremultiplied[i] = false; 706 707 for (int i = 0; i < channels.length; i++) { 708 if (cType[i] == 2) 709 isPremultiplied[associations[i] - 1] = true; 710 } 711 712 for (int i = 1; i < alphaChannel; i++) 713 isPremultiplied[0] &= isPremultiplied[i]; 714 } 715 716 ColorSpace cs = null; 717 if (profile != null) 718 cs = new ICC_ColorSpace(profile); 719 else if (colorSpaceType == CSB_ENUM_SRGB) 720 cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); 721 else if (colorSpaceType == CSB_ENUM_GREY) 722 cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); 723 else if (colorSpaceType == CSB_ENUM_YCC) 724 cs = ColorSpace.getInstance(ColorSpace.CS_PYCC); 725 726 int[] bits = new int[numComp]; 727 for (int i = 0; i < numComp; i++) 728 if (bitDepths != null) 729 bits[i] = (bitDepths[i] & 0x7F) + 1; 730 else 731 bits[i] = (bitDepth &0x7F) + 1; 732 733 int maxBitDepth = 1 + (bitDepth & 0x7F); 734 boolean isSigned = (bitDepth & 0x80) == 0x80; 735 if (bitDepths != null) 736 isSigned = (bitDepths[0] & 0x80) == 0x80; 737 738 if (bitDepths != null) 739 for (int i = 0; i < numComp; i++) 740 if (bits[i] > maxBitDepth) 741 maxBitDepth = bits[i]; 742 743 int type = -1; 744 745 if (maxBitDepth <= 8) 746 type = DataBuffer.TYPE_BYTE; 747 else if (maxBitDepth <= 16) 748 type = isSigned ? DataBuffer.TYPE_SHORT : DataBuffer.TYPE_USHORT; 749 else if (maxBitDepth <= 32) 750 type = DataBuffer.TYPE_INT; 751 752 if (type == -1) 753 return null; 754 755 if (cs != null) { 756 colorModel = new ComponentColorModel(cs, 757 bits, 758 hasAlpha, 759 isPremultiplied[0], 760 hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE , 761 type); 762 } 763 } 764 return colorModel; 765 } 766}