1. /*
  2. * @(#)JFIFMarkerSegment.java 1.9 03/12/19
  3. *
  4. * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package com.sun.imageio.plugins.jpeg;
  8. import javax.imageio.IIOException;
  9. import javax.imageio.IIOImage;
  10. import javax.imageio.ImageTypeSpecifier;
  11. import javax.imageio.ImageReader;
  12. import javax.imageio.metadata.IIOInvalidTreeException;
  13. import javax.imageio.metadata.IIOMetadataNode;
  14. import javax.imageio.metadata.IIOMetadata;
  15. import javax.imageio.stream.ImageInputStream;
  16. import javax.imageio.stream.ImageOutputStream;
  17. import javax.imageio.stream.MemoryCacheImageOutputStream;
  18. import javax.imageio.event.IIOReadProgressListener;
  19. import java.awt.Graphics;
  20. import java.awt.color.ICC_Profile;
  21. import java.awt.color.ICC_ColorSpace;
  22. import java.awt.color.ColorSpace;
  23. import java.awt.image.ColorModel;
  24. import java.awt.image.SampleModel;
  25. import java.awt.image.IndexColorModel;
  26. import java.awt.image.ComponentColorModel;
  27. import java.awt.image.BufferedImage;
  28. import java.awt.image.DataBuffer;
  29. import java.awt.image.DataBufferByte;
  30. import java.awt.image.Raster;
  31. import java.awt.image.WritableRaster;
  32. import java.io.IOException;
  33. import java.io.ByteArrayOutputStream;
  34. import java.util.List;
  35. import java.util.ArrayList;
  36. import java.util.Iterator;
  37. import org.w3c.dom.Node;
  38. import org.w3c.dom.NodeList;
  39. import org.w3c.dom.NamedNodeMap;
  40. /**
  41. * A JFIF (JPEG File Interchange Format) APP0 (Application-Specific)
  42. * marker segment. Inner classes are included for JFXX extension
  43. * marker segments, for different varieties of thumbnails, and for
  44. * ICC Profile APP2 marker segments. Any of these secondary types
  45. * that occur are kept as members of a single JFIFMarkerSegment object.
  46. */
  47. class JFIFMarkerSegment extends MarkerSegment {
  48. int majorVersion;
  49. int minorVersion;
  50. int resUnits;
  51. int Xdensity;
  52. int Ydensity;
  53. int thumbWidth;
  54. int thumbHeight;
  55. JFIFThumbRGB thumb = null; // If present
  56. ArrayList extSegments = new ArrayList();
  57. ICCMarkerSegment iccSegment = null; // optional ICC
  58. private static final int THUMB_JPEG = 0x10;
  59. private static final int THUMB_PALETTE = 0x11;
  60. private static final int THUMB_UNASSIGNED = 0x12;
  61. private static final int THUMB_RGB = 0x13;
  62. private static final int DATA_SIZE = 14;
  63. private static final int ID_SIZE = 5;
  64. private final int MAX_THUMB_WIDTH = 255;
  65. private final int MAX_THUMB_HEIGHT = 255;
  66. private final boolean debug = false;
  67. /**
  68. * Set to <code>true</code> when reading the chunks of an
  69. * ICC profile. All chunks are consolidated to create a single
  70. * "segment" containing all the chunks. This flag is a state
  71. * variable identifying whether to construct a new segment or
  72. * append to an old one.
  73. */
  74. private boolean inICC = false;
  75. /**
  76. * A placeholder for an ICC profile marker segment under
  77. * construction. The segment is not added to the list
  78. * until all chunks have been read.
  79. */
  80. private ICCMarkerSegment tempICCSegment = null;
  81. /**
  82. * Default constructor. Used to create a default JFIF header
  83. */
  84. JFIFMarkerSegment() {
  85. super(JPEG.APP0);
  86. majorVersion = 1;
  87. minorVersion = 2;
  88. resUnits = JPEG.DENSITY_UNIT_ASPECT_RATIO;
  89. Xdensity = 1;
  90. Ydensity = 1;
  91. thumbWidth = 0;
  92. thumbHeight = 0;
  93. }
  94. /**
  95. * Constructs a JFIF header by reading from a stream wrapped
  96. * in a JPEGBuffer.
  97. */
  98. JFIFMarkerSegment(JPEGBuffer buffer) throws IOException {
  99. super(buffer);
  100. buffer.bufPtr += ID_SIZE; // skip the id, we already checked it
  101. majorVersion = buffer.buf[buffer.bufPtr++];
  102. minorVersion = buffer.buf[buffer.bufPtr++];
  103. resUnits = buffer.buf[buffer.bufPtr++];
  104. Xdensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
  105. Xdensity |= buffer.buf[buffer.bufPtr++] & 0xff;
  106. Ydensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
  107. Ydensity |= buffer.buf[buffer.bufPtr++] & 0xff;
  108. thumbWidth = buffer.buf[buffer.bufPtr++] & 0xff;
  109. thumbHeight = buffer.buf[buffer.bufPtr++] & 0xff;
  110. buffer.bufAvail -= DATA_SIZE;
  111. if (thumbWidth > 0) {
  112. thumb = new JFIFThumbRGB(buffer, thumbWidth, thumbHeight);
  113. }
  114. }
  115. /**
  116. * Constructs a JFIF header from a DOM Node.
  117. */
  118. JFIFMarkerSegment(Node node) throws IIOInvalidTreeException {
  119. this();
  120. updateFromNativeNode(node, true);
  121. }
  122. /**
  123. * Returns a deep-copy clone of this object.
  124. */
  125. protected Object clone() {
  126. JFIFMarkerSegment newGuy = (JFIFMarkerSegment) super.clone();
  127. if (!extSegments.isEmpty()) { // Clone the list with a deep copy
  128. newGuy.extSegments = new ArrayList();
  129. for (Iterator iter = extSegments.iterator(); iter.hasNext();) {
  130. JFIFExtensionMarkerSegment jfxx =
  131. (JFIFExtensionMarkerSegment) iter.next();
  132. newGuy.extSegments.add(jfxx.clone());
  133. }
  134. }
  135. if (iccSegment != null) {
  136. newGuy.iccSegment = (ICCMarkerSegment) iccSegment.clone();
  137. }
  138. return newGuy;
  139. }
  140. /**
  141. * Add an JFXX extension marker segment from the stream wrapped
  142. * in the JPEGBuffer to the list of extension segments.
  143. */
  144. void addJFXX(JPEGBuffer buffer, JPEGImageReader reader)
  145. throws IOException {
  146. extSegments.add(new JFIFExtensionMarkerSegment(buffer, reader));
  147. }
  148. /**
  149. * Adds an ICC Profile APP2 segment from the stream wrapped
  150. * in the JPEGBuffer.
  151. */
  152. void addICC(JPEGBuffer buffer) throws IOException {
  153. if (inICC == false) {
  154. if (iccSegment != null) {
  155. throw new IIOException
  156. ("> 1 ICC APP2 Marker Segment not supported");
  157. }
  158. tempICCSegment = new ICCMarkerSegment(buffer);
  159. if (inICC == false) { // Just one chunk
  160. iccSegment = tempICCSegment;
  161. tempICCSegment = null;
  162. }
  163. } else {
  164. if (tempICCSegment.addData(buffer) == true) {
  165. iccSegment = tempICCSegment;
  166. tempICCSegment = null;
  167. }
  168. }
  169. }
  170. /**
  171. * Add an ICC Profile APP2 segment by constructing it from
  172. * the given ICC_ColorSpace object.
  173. */
  174. void addICC(ICC_ColorSpace cs) throws IOException {
  175. if (iccSegment != null) {
  176. throw new IIOException
  177. ("> 1 ICC APP2 Marker Segment not supported");
  178. }
  179. iccSegment = new ICCMarkerSegment(cs);
  180. }
  181. /**
  182. * Returns a tree of DOM nodes representing this object and any
  183. * subordinate JFXX extension or ICC Profile segments.
  184. */
  185. IIOMetadataNode getNativeNode() {
  186. IIOMetadataNode node = new IIOMetadataNode("app0JFIF");
  187. node.setAttribute("majorVersion", Integer.toString(majorVersion));
  188. node.setAttribute("minorVersion", Integer.toString(minorVersion));
  189. node.setAttribute("resUnits", Integer.toString(resUnits));
  190. node.setAttribute("Xdensity", Integer.toString(Xdensity));
  191. node.setAttribute("Ydensity", Integer.toString(Ydensity));
  192. node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
  193. node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
  194. if (!extSegments.isEmpty()) {
  195. IIOMetadataNode JFXXnode = new IIOMetadataNode("JFXX");
  196. node.appendChild(JFXXnode);
  197. for (Iterator iter = extSegments.iterator(); iter.hasNext();) {
  198. JFIFExtensionMarkerSegment seg =
  199. (JFIFExtensionMarkerSegment) iter.next();
  200. JFXXnode.appendChild(seg.getNativeNode());
  201. }
  202. }
  203. if (iccSegment != null) {
  204. node.appendChild(iccSegment.getNativeNode());
  205. }
  206. return node;
  207. }
  208. /**
  209. * Updates the data in this object from the given DOM Node tree.
  210. * If fromScratch is true, this object is being constructed.
  211. * Otherwise an existing object is being modified.
  212. * Throws an IIOInvalidTreeException if the tree is invalid in
  213. * any way.
  214. */
  215. void updateFromNativeNode(Node node, boolean fromScratch)
  216. throws IIOInvalidTreeException {
  217. // none of the attributes are required
  218. NamedNodeMap attrs = node.getAttributes();
  219. if (attrs.getLength() > 0) {
  220. int value = getAttributeValue(node, attrs, "majorVersion",
  221. 0, 255, false);
  222. majorVersion = (value != -1) ? value : majorVersion;
  223. value = getAttributeValue(node, attrs, "minorVersion",
  224. 0, 255, false);
  225. minorVersion = (value != -1) ? value : minorVersion;
  226. value = getAttributeValue(node, attrs, "resUnits", 0, 2, false);
  227. resUnits = (value != -1) ? value : resUnits;
  228. value = getAttributeValue(node, attrs, "Xdensity", 1, 65535, false);
  229. Xdensity = (value != -1) ? value : Xdensity;
  230. value = getAttributeValue(node, attrs, "Ydensity", 1, 65535, false);
  231. Ydensity = (value != -1) ? value : Ydensity;
  232. value = getAttributeValue(node, attrs, "thumbWidth", 0, 255, false);
  233. thumbWidth = (value != -1) ? value : thumbWidth;
  234. value = getAttributeValue(node, attrs, "thumbHeight", 0, 255, false);
  235. thumbHeight = (value != -1) ? value : thumbHeight;
  236. }
  237. if (node.hasChildNodes()) {
  238. NodeList children = node.getChildNodes();
  239. int count = children.getLength();
  240. if (count > 2) {
  241. throw new IIOInvalidTreeException
  242. ("app0JFIF node cannot have > 2 children", node);
  243. }
  244. for (int i = 0; i < count; i++) {
  245. Node child = children.item(i);
  246. String name = child.getNodeName();
  247. if (name.equals("JFXX")) {
  248. if ((!extSegments.isEmpty()) && fromScratch) {
  249. throw new IIOInvalidTreeException
  250. ("app0JFIF node cannot have > 1 JFXX node", node);
  251. }
  252. NodeList exts = child.getChildNodes();
  253. int extCount = exts.getLength();
  254. for (int j = 0; j < extCount; j++) {
  255. Node ext = exts.item(j);
  256. extSegments.add(new JFIFExtensionMarkerSegment(ext));
  257. }
  258. }
  259. if (name.equals("app2ICC")) {
  260. if ((iccSegment != null) && fromScratch) {
  261. throw new IIOInvalidTreeException
  262. ("> 1 ICC APP2 Marker Segment not supported", node);
  263. }
  264. iccSegment = new ICCMarkerSegment(child);
  265. }
  266. }
  267. }
  268. }
  269. int getThumbnailWidth(int index) {
  270. if (thumb != null) {
  271. if (index == 0) {
  272. return thumb.getWidth();
  273. }
  274. index--;
  275. }
  276. JFIFExtensionMarkerSegment jfxx =
  277. (JFIFExtensionMarkerSegment) extSegments.get(index);
  278. return jfxx.thumb.getWidth();
  279. }
  280. int getThumbnailHeight(int index) {
  281. if (thumb != null) {
  282. if (index == 0) {
  283. return thumb.getHeight();
  284. }
  285. index--;
  286. }
  287. JFIFExtensionMarkerSegment jfxx =
  288. (JFIFExtensionMarkerSegment) extSegments.get(index);
  289. return jfxx.thumb.getHeight();
  290. }
  291. BufferedImage getThumbnail(ImageInputStream iis,
  292. int index,
  293. JPEGImageReader reader) throws IOException {
  294. reader.thumbnailStarted(index);
  295. BufferedImage ret = null;
  296. if ((thumb != null) && (index == 0)) {
  297. ret = thumb.getThumbnail(iis, reader);
  298. } else {
  299. if (thumb != null) {
  300. index--;
  301. }
  302. JFIFExtensionMarkerSegment jfxx =
  303. (JFIFExtensionMarkerSegment) extSegments.get(index);
  304. ret = jfxx.thumb.getThumbnail(iis, reader);
  305. }
  306. reader.thumbnailComplete();
  307. return ret;
  308. }
  309. /**
  310. * Writes the data for this segment to the stream in
  311. * valid JPEG format. Assumes that there will be no thumbnail.
  312. */
  313. void write(ImageOutputStream ios,
  314. JPEGImageWriter writer) throws IOException {
  315. // No thumbnail
  316. write(ios, null, writer);
  317. }
  318. /**
  319. * Writes the data for this segment to the stream in
  320. * valid JPEG format. The length written takes the thumbnail
  321. * width and height into account. If necessary, the thumbnail
  322. * is clipped to 255 x 255 and a warning is sent to the writer
  323. * argument. Progress updates are sent to the writer argument.
  324. */
  325. void write(ImageOutputStream ios,
  326. BufferedImage thumb,
  327. JPEGImageWriter writer) throws IOException {
  328. int thumbWidth = 0;
  329. int thumbHeight = 0;
  330. int thumbLength = 0;
  331. int [] thumbData = null;
  332. if (thumb != null) {
  333. // Clip if necessary and get the data in thumbData
  334. thumbWidth = thumb.getWidth();
  335. thumbHeight = thumb.getHeight();
  336. if ((thumbWidth > MAX_THUMB_WIDTH)
  337. || (thumbHeight > MAX_THUMB_HEIGHT)) {
  338. writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
  339. }
  340. thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
  341. thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
  342. thumbData = thumb.getRaster().getPixels(0, 0,
  343. thumbWidth, thumbHeight,
  344. (int []) null);
  345. thumbLength = thumbData.length;
  346. }
  347. length = DATA_SIZE + LENGTH_SIZE + thumbLength;
  348. writeTag(ios);
  349. byte [] id = {0x4A, 0x46, 0x49, 0x46, 0x00};
  350. ios.write(id);
  351. ios.write(majorVersion);
  352. ios.write(minorVersion);
  353. ios.write(resUnits);
  354. write2bytes(ios, Xdensity);
  355. write2bytes(ios, Ydensity);
  356. ios.write(thumbWidth);
  357. ios.write(thumbHeight);
  358. if (thumbData != null) {
  359. writer.thumbnailStarted(0);
  360. writeThumbnailData(ios, thumbData, writer);
  361. writer.thumbnailComplete();
  362. }
  363. }
  364. /*
  365. * Write out the values in the integer array as a sequence of bytes,
  366. * reporting progress to the writer argument.
  367. */
  368. void writeThumbnailData(ImageOutputStream ios,
  369. int [] thumbData,
  370. JPEGImageWriter writer) throws IOException {
  371. int progInterval = thumbData.length / 20; // approx. every 5%
  372. if (progInterval == 0) {
  373. progInterval = 1;
  374. }
  375. for (int i = 0; i < thumbData.length; i++) {
  376. ios.write(thumbData[i]);
  377. if ((i > progInterval) && (i % progInterval == 0)) {
  378. writer.thumbnailProgress
  379. (((float) i * 100) / ((float) thumbData.length));
  380. }
  381. }
  382. }
  383. /**
  384. * Write out this JFIF Marker Segment, including a thumbnail or
  385. * appending a series of JFXX Marker Segments, as appropriate.
  386. * Warnings and progress reports are sent to the writer argument.
  387. * The list of thumbnails is matched to the list of JFXX extension
  388. * segments, if any, in order to determine how to encode the
  389. * thumbnails. If there are more thumbnails than metadata segments,
  390. * default encoding is used for the extra thumbnails.
  391. */
  392. void writeWithThumbs(ImageOutputStream ios,
  393. List thumbnails,
  394. JPEGImageWriter writer) throws IOException {
  395. if (thumbnails != null) {
  396. JFIFExtensionMarkerSegment jfxx = null;
  397. if (thumbnails.size() == 1) {
  398. if (!extSegments.isEmpty()) {
  399. jfxx = (JFIFExtensionMarkerSegment) extSegments.get(0);
  400. }
  401. writeThumb(ios,
  402. (BufferedImage) thumbnails.get(0),
  403. jfxx,
  404. 0,
  405. true,
  406. writer);
  407. } else {
  408. // All others write as separate JFXX segments
  409. write(ios, writer); // Just the header without any thumbnail
  410. for (int i = 0; i < thumbnails.size(); i++) {
  411. jfxx = null;
  412. if (i < extSegments.size()) {
  413. jfxx = (JFIFExtensionMarkerSegment) extSegments.get(i);
  414. }
  415. writeThumb(ios,
  416. (BufferedImage) thumbnails.get(i),
  417. jfxx,
  418. i,
  419. false,
  420. writer);
  421. }
  422. }
  423. } else { // No thumbnails
  424. write(ios, writer);
  425. }
  426. }
  427. private void writeThumb(ImageOutputStream ios,
  428. BufferedImage thumb,
  429. JFIFExtensionMarkerSegment jfxx,
  430. int index,
  431. boolean onlyOne,
  432. JPEGImageWriter writer) throws IOException {
  433. ColorModel cm = thumb.getColorModel();
  434. ColorSpace cs = cm.getColorSpace();
  435. if (cm instanceof IndexColorModel) {
  436. // We never write a palette image into the header
  437. // So if it's the only one, we need to write the header first
  438. if (onlyOne) {
  439. write(ios, writer);
  440. }
  441. if ((jfxx == null)
  442. || (jfxx.code == THUMB_PALETTE)) {
  443. writeJFXXSegment(index, thumb, ios, writer); // default
  444. } else {
  445. // Expand to RGB
  446. BufferedImage thumbRGB =
  447. ((IndexColorModel) cm).convertToIntDiscrete
  448. (thumb.getRaster(), false);
  449. jfxx.setThumbnail(thumbRGB);
  450. writer.thumbnailStarted(index);
  451. jfxx.write(ios, writer); // Handles clipping if needed
  452. writer.thumbnailComplete();
  453. }
  454. } else if (cs.getType() == ColorSpace.TYPE_RGB) {
  455. if (jfxx == null) {
  456. if (onlyOne) {
  457. write(ios, thumb, writer); // As part of the header
  458. } else {
  459. writeJFXXSegment(index, thumb, ios, writer); // default
  460. }
  461. } else {
  462. // If this is the only one, write the header first
  463. if (onlyOne) {
  464. write(ios, writer);
  465. }
  466. if (jfxx.code == THUMB_PALETTE) {
  467. writeJFXXSegment(index, thumb, ios, writer); // default
  468. writer.warningOccurred
  469. (JPEGImageWriter.WARNING_NO_RGB_THUMB_AS_INDEXED);
  470. } else {
  471. jfxx.setThumbnail(thumb);
  472. writer.thumbnailStarted(index);
  473. jfxx.write(ios, writer); // Handles clipping if needed
  474. writer.thumbnailComplete();
  475. }
  476. }
  477. } else if (cs.getType() == ColorSpace.TYPE_GRAY) {
  478. if (jfxx == null) {
  479. if (onlyOne) {
  480. BufferedImage thumbRGB = expandGrayThumb(thumb);
  481. write(ios, thumbRGB, writer); // As part of the header
  482. } else {
  483. writeJFXXSegment(index, thumb, ios, writer); // default
  484. }
  485. } else {
  486. // If this is the only one, write the header first
  487. if (onlyOne) {
  488. write(ios, writer);
  489. }
  490. if (jfxx.code == THUMB_RGB) {
  491. BufferedImage thumbRGB = expandGrayThumb(thumb);
  492. writeJFXXSegment(index, thumbRGB, ios, writer);
  493. } else if (jfxx.code == THUMB_JPEG) {
  494. jfxx.setThumbnail(thumb);
  495. writer.thumbnailStarted(index);
  496. jfxx.write(ios, writer); // Handles clipping if needed
  497. writer.thumbnailComplete();
  498. } else if (jfxx.code == THUMB_PALETTE) {
  499. writeJFXXSegment(index, thumb, ios, writer); // default
  500. writer.warningOccurred
  501. (JPEGImageWriter.WARNING_NO_GRAY_THUMB_AS_INDEXED);
  502. }
  503. }
  504. } else {
  505. writer.warningOccurred
  506. (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL);
  507. }
  508. }
  509. // Could put reason codes in here to be parsed in writeJFXXSegment
  510. // in order to provide more meaningful warnings.
  511. private class IllegalThumbException extends Exception {}
  512. /**
  513. * Writes out a new JFXX extension segment, without saving it.
  514. */
  515. private void writeJFXXSegment(int index,
  516. BufferedImage thumbnail,
  517. ImageOutputStream ios,
  518. JPEGImageWriter writer) throws IOException {
  519. JFIFExtensionMarkerSegment jfxx = null;
  520. try {
  521. jfxx = new JFIFExtensionMarkerSegment(thumbnail);
  522. } catch (IllegalThumbException e) {
  523. writer.warningOccurred
  524. (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL);
  525. return;
  526. }
  527. writer.thumbnailStarted(index);
  528. jfxx.write(ios, writer);
  529. writer.thumbnailComplete();
  530. }
  531. /**
  532. * Return an RGB image that is the expansion of the given grayscale
  533. * image.
  534. */
  535. private static BufferedImage expandGrayThumb(BufferedImage thumb) {
  536. BufferedImage ret = new BufferedImage(thumb.getWidth(),
  537. thumb.getHeight(),
  538. BufferedImage.TYPE_INT_RGB);
  539. Graphics g = ret.getGraphics();
  540. g.drawImage(thumb, 0, 0, null);
  541. return ret;
  542. }
  543. /**
  544. * Writes out a default JFIF marker segment to the given
  545. * output stream. If <code>thumbnails</code> is not <code>null</code>,
  546. * writes out the set of thumbnail images as JFXX marker segments, or
  547. * incorporated into the JFIF segment if appropriate.
  548. * If <code>iccProfile</code> is not <code>null</code>,
  549. * writes out the profile after the JFIF segment using as many APP2
  550. * marker segments as necessary.
  551. */
  552. static void writeDefaultJFIF(ImageOutputStream ios,
  553. List thumbnails,
  554. ICC_Profile iccProfile,
  555. JPEGImageWriter writer)
  556. throws IOException {
  557. JFIFMarkerSegment jfif = new JFIFMarkerSegment();
  558. jfif.writeWithThumbs(ios, thumbnails, writer);
  559. if (iccProfile != null) {
  560. writeICC(iccProfile, ios);
  561. }
  562. }
  563. /**
  564. * Prints out the contents of this object to System.out for debugging.
  565. */
  566. void print() {
  567. printTag("JFIF");
  568. System.out.print("Version ");
  569. System.out.print(majorVersion);
  570. System.out.println(".0"
  571. + Integer.toString(minorVersion));
  572. System.out.print("Resolution units: ");
  573. System.out.println(resUnits);
  574. System.out.print("X density: ");
  575. System.out.println(Xdensity);
  576. System.out.print("Y density: ");
  577. System.out.println(Ydensity);
  578. System.out.print("Thumbnail Width: ");
  579. System.out.println(thumbWidth);
  580. System.out.print("Thumbnail Height: ");
  581. System.out.println(thumbHeight);
  582. if (!extSegments.isEmpty()) {
  583. for (Iterator iter = extSegments.iterator(); iter.hasNext();) {
  584. JFIFExtensionMarkerSegment extSegment =
  585. (JFIFExtensionMarkerSegment) iter.next();
  586. extSegment.print();
  587. }
  588. }
  589. if (iccSegment != null) {
  590. iccSegment.print();
  591. }
  592. }
  593. /**
  594. * A JFIF extension APP0 marker segment.
  595. */
  596. class JFIFExtensionMarkerSegment extends MarkerSegment {
  597. int code;
  598. JFIFThumb thumb;
  599. private static final int DATA_SIZE = 6;
  600. private static final int ID_SIZE = 5;
  601. JFIFExtensionMarkerSegment(JPEGBuffer buffer, JPEGImageReader reader)
  602. throws IOException {
  603. super(buffer);
  604. buffer.bufPtr += ID_SIZE; // skip the id, we already checked it
  605. code = buffer.buf[buffer.bufPtr++] & 0xff;
  606. buffer.bufAvail -= DATA_SIZE;
  607. if (code == THUMB_JPEG) {
  608. thumb = new JFIFThumbJPEG(buffer, length, reader);
  609. } else {
  610. buffer.loadBuf(2);
  611. int thumbX = buffer.buf[buffer.bufPtr++] & 0xff;
  612. int thumbY = buffer.buf[buffer.bufPtr++] & 0xff;
  613. buffer.bufAvail -= 2;
  614. // following constructors handle bufAvail
  615. if (code == THUMB_PALETTE) {
  616. thumb = new JFIFThumbPalette(buffer, thumbX, thumbY);
  617. } else {
  618. thumb = new JFIFThumbRGB(buffer, thumbX, thumbY);
  619. }
  620. }
  621. }
  622. JFIFExtensionMarkerSegment(Node node) throws IIOInvalidTreeException {
  623. super(JPEG.APP0);
  624. NamedNodeMap attrs = node.getAttributes();
  625. if (attrs.getLength() > 0) {
  626. code = getAttributeValue(node,
  627. attrs,
  628. "extensionCode",
  629. THUMB_JPEG,
  630. THUMB_RGB,
  631. false);
  632. if (code == THUMB_UNASSIGNED) {
  633. throw new IIOInvalidTreeException
  634. ("invalid extensionCode attribute value", node);
  635. }
  636. } else {
  637. code = THUMB_UNASSIGNED;
  638. }
  639. // Now the child
  640. if (node.getChildNodes().getLength() != 1) {
  641. throw new IIOInvalidTreeException
  642. ("app0JFXX node must have exactly 1 child", node);
  643. }
  644. Node child = node.getFirstChild();
  645. String name = child.getNodeName();
  646. if (name.equals("JFIFthumbJPEG")) {
  647. if (code == THUMB_UNASSIGNED) {
  648. code = THUMB_JPEG;
  649. }
  650. thumb = new JFIFThumbJPEG(child);
  651. } else if (name.equals("JFIFthumbPalette")) {
  652. if (code == THUMB_UNASSIGNED) {
  653. code = THUMB_PALETTE;
  654. }
  655. thumb = new JFIFThumbPalette(child);
  656. } else if (name.equals("JFIFthumbRGB")) {
  657. if (code == THUMB_UNASSIGNED) {
  658. code = THUMB_RGB;
  659. }
  660. thumb = new JFIFThumbRGB(child);
  661. } else {
  662. throw new IIOInvalidTreeException
  663. ("unrecognized app0JFXX child node", node);
  664. }
  665. }
  666. JFIFExtensionMarkerSegment(BufferedImage thumbnail)
  667. throws IllegalThumbException {
  668. super(JPEG.APP0);
  669. ColorModel cm = thumbnail.getColorModel();
  670. int csType = cm.getColorSpace().getType();
  671. if (cm.hasAlpha()) {
  672. throw new IllegalThumbException();
  673. }
  674. if (cm instanceof IndexColorModel) {
  675. code = THUMB_PALETTE;
  676. thumb = new JFIFThumbPalette(thumbnail);
  677. } else if (csType == ColorSpace.TYPE_RGB) {
  678. code = THUMB_RGB;
  679. thumb = new JFIFThumbRGB(thumbnail);
  680. } else if (csType == ColorSpace.TYPE_GRAY) {
  681. code = THUMB_JPEG;
  682. thumb = new JFIFThumbJPEG(thumbnail);
  683. } else {
  684. throw new IllegalThumbException();
  685. }
  686. }
  687. void setThumbnail(BufferedImage thumbnail) {
  688. try {
  689. switch (code) {
  690. case THUMB_PALETTE:
  691. thumb = new JFIFThumbPalette(thumbnail);
  692. break;
  693. case THUMB_RGB:
  694. thumb = new JFIFThumbRGB(thumbnail);
  695. break;
  696. case THUMB_JPEG:
  697. thumb = new JFIFThumbJPEG(thumbnail);
  698. break;
  699. }
  700. } catch (IllegalThumbException e) {
  701. // Should never happen
  702. throw new InternalError("Illegal thumb in setThumbnail!");
  703. }
  704. }
  705. protected Object clone() {
  706. JFIFExtensionMarkerSegment newGuy =
  707. (JFIFExtensionMarkerSegment) super.clone();
  708. if (thumb != null) {
  709. newGuy.thumb = (JFIFThumb) thumb.clone();
  710. }
  711. return newGuy;
  712. }
  713. IIOMetadataNode getNativeNode() {
  714. IIOMetadataNode node = new IIOMetadataNode("app0JFXX");
  715. node.setAttribute("extensionCode", Integer.toString(code));
  716. node.appendChild(thumb.getNativeNode());
  717. return node;
  718. }
  719. void write(ImageOutputStream ios,
  720. JPEGImageWriter writer) throws IOException {
  721. length = LENGTH_SIZE + DATA_SIZE + thumb.getLength();
  722. writeTag(ios);
  723. byte [] id = {0x4A, 0x46, 0x58, 0x58, 0x00};
  724. ios.write(id);
  725. ios.write(code);
  726. thumb.write(ios, writer);
  727. }
  728. void print() {
  729. printTag("JFXX");
  730. thumb.print();
  731. }
  732. }
  733. /**
  734. * A superclass for the varieties of thumbnails that can
  735. * be stored in a JFIF extension marker segment.
  736. */
  737. abstract class JFIFThumb implements Cloneable {
  738. long streamPos = -1L; // Save the thumbnail pos when reading
  739. abstract int getLength(); // When writing
  740. abstract int getWidth();
  741. abstract int getHeight();
  742. abstract BufferedImage getThumbnail(ImageInputStream iis,
  743. JPEGImageReader reader)
  744. throws IOException;
  745. protected JFIFThumb() {}
  746. protected JFIFThumb(JPEGBuffer buffer) throws IOException{
  747. // Save the stream position for reading the thumbnail later
  748. streamPos = buffer.getStreamPosition();
  749. }
  750. abstract void print();
  751. abstract IIOMetadataNode getNativeNode();
  752. abstract void write(ImageOutputStream ios,
  753. JPEGImageWriter writer) throws IOException;
  754. protected Object clone() {
  755. try {
  756. return super.clone();
  757. } catch (CloneNotSupportedException e) {} // won't happen
  758. return null;
  759. }
  760. }
  761. abstract class JFIFThumbUncompressed extends JFIFThumb {
  762. BufferedImage thumbnail = null;
  763. int thumbWidth;
  764. int thumbHeight;
  765. String name;
  766. JFIFThumbUncompressed(JPEGBuffer buffer,
  767. int width,
  768. int height,
  769. int skip,
  770. String name)
  771. throws IOException {
  772. super(buffer);
  773. thumbWidth = width;
  774. thumbHeight = height;
  775. // Now skip the thumbnail data
  776. buffer.skipData(skip);
  777. this.name = name;
  778. }
  779. JFIFThumbUncompressed(Node node, String name)
  780. throws IIOInvalidTreeException {
  781. thumbWidth = 0;
  782. thumbHeight = 0;
  783. this.name = name;
  784. NamedNodeMap attrs = node.getAttributes();
  785. int count = attrs.getLength();
  786. if (count > 2) {
  787. throw new IIOInvalidTreeException
  788. (name +" node cannot have > 2 attributes", node);
  789. }
  790. if (count != 0) {
  791. int value = getAttributeValue(node, attrs, "thumbWidth",
  792. 0, 255, false);
  793. thumbWidth = (value != -1) ? value : thumbWidth;
  794. value = getAttributeValue(node, attrs, "thumbHeight",
  795. 0, 255, false);
  796. thumbHeight = (value != -1) ? value : thumbHeight;
  797. }
  798. }
  799. JFIFThumbUncompressed(BufferedImage thumb) {
  800. thumbnail = thumb;
  801. thumbWidth = thumb.getWidth();
  802. thumbHeight = thumb.getHeight();
  803. name = null; // not used when writing
  804. }
  805. void readByteBuffer(ImageInputStream iis,
  806. byte [] data,
  807. JPEGImageReader reader,
  808. float workPortion,
  809. float workOffset) throws IOException {
  810. int progInterval = Math.max((int)(data.length20workPortion),
  811. 1);
  812. for (int offset = 0;
  813. offset < data.length;) {
  814. int len = Math.min(progInterval, data.length-offset);
  815. iis.read(data, offset, len);
  816. offset += progInterval;
  817. float percentDone = ((float) offset* 100)
  818. / data.length
  819. * workPortion + workOffset;
  820. if (percentDone > 100.0F) {
  821. percentDone = 100.0F;
  822. }
  823. reader.thumbnailProgress (percentDone);
  824. }
  825. }
  826. int getWidth() {
  827. return thumbWidth;
  828. }
  829. int getHeight() {
  830. return thumbHeight;
  831. }
  832. IIOMetadataNode getNativeNode() {
  833. IIOMetadataNode node = new IIOMetadataNode(name);
  834. node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
  835. node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
  836. return node;
  837. }
  838. void write(ImageOutputStream ios,
  839. JPEGImageWriter writer) throws IOException {
  840. if ((thumbWidth > MAX_THUMB_WIDTH)
  841. || (thumbHeight > MAX_THUMB_HEIGHT)) {
  842. writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
  843. }
  844. thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
  845. thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
  846. ios.write(thumbWidth);
  847. ios.write(thumbHeight);
  848. }
  849. void writePixels(ImageOutputStream ios,
  850. JPEGImageWriter writer) throws IOException {
  851. if ((thumbWidth > MAX_THUMB_WIDTH)
  852. || (thumbHeight > MAX_THUMB_HEIGHT)) {
  853. writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
  854. }
  855. thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
  856. thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
  857. int [] data = thumbnail.getRaster().getPixels(0, 0,
  858. thumbWidth,
  859. thumbHeight,
  860. (int []) null);
  861. writeThumbnailData(ios, data, writer);
  862. }
  863. void print() {
  864. System.out.print(name + " width: ");
  865. System.out.println(thumbWidth);
  866. System.out.print(name + " height: ");
  867. System.out.println(thumbHeight);
  868. }
  869. }
  870. /**
  871. * A JFIF thumbnail stored as RGB, one byte per channel,
  872. * interleaved.
  873. */
  874. class JFIFThumbRGB extends JFIFThumbUncompressed {
  875. JFIFThumbRGB(JPEGBuffer buffer, int width, int height)
  876. throws IOException {
  877. super(buffer, width, height, width*height*3, "JFIFthumbRGB");
  878. }
  879. JFIFThumbRGB(Node node) throws IIOInvalidTreeException {
  880. super(node, "JFIFthumbRGB");
  881. }
  882. JFIFThumbRGB(BufferedImage thumb) throws IllegalThumbException {
  883. super(thumb);
  884. }
  885. int getLength() {
  886. return (thumbWidth*thumbHeight*3);
  887. }
  888. BufferedImage getThumbnail(ImageInputStream iis,
  889. JPEGImageReader reader)
  890. throws IOException {
  891. iis.mark();
  892. iis.seek(streamPos);
  893. DataBufferByte buffer = new DataBufferByte(getLength());
  894. readByteBuffer(iis,
  895. buffer.getData(),
  896. reader,
  897. 1.0F,
  898. 0.0F);
  899. iis.reset();
  900. WritableRaster raster =
  901. Raster.createInterleavedRaster(buffer,
  902. thumbWidth,
  903. thumbHeight,
  904. thumbWidth*3,
  905. 3,
  906. new int [] {0, 1, 2},
  907. null);
  908. ColorModel cm = new ComponentColorModel(JPEG.sRGB,
  909. false,
  910. false,
  911. ColorModel.OPAQUE,
  912. DataBuffer.TYPE_BYTE);
  913. return new BufferedImage(cm,
  914. raster,
  915. false,
  916. null);
  917. }
  918. void write(ImageOutputStream ios,
  919. JPEGImageWriter writer) throws IOException {
  920. super.write(ios, writer); // width and height
  921. writePixels(ios, writer);
  922. }
  923. }
  924. /**
  925. * A JFIF thumbnail stored as an indexed palette image
  926. * using an RGB palette.
  927. */
  928. class JFIFThumbPalette extends JFIFThumbUncompressed {
  929. private static final int PALETTE_SIZE = 768;
  930. JFIFThumbPalette(JPEGBuffer buffer, int width, int height)
  931. throws IOException {
  932. super(buffer,
  933. width,
  934. height,
  935. PALETTE_SIZE + width * height,
  936. "JFIFThumbPalette");
  937. }
  938. JFIFThumbPalette(Node node) throws IIOInvalidTreeException {
  939. super(node, "JFIFThumbPalette");
  940. }
  941. JFIFThumbPalette(BufferedImage thumb) throws IllegalThumbException {
  942. super(thumb);
  943. IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel();
  944. if (icm.getMapSize() > 256) {
  945. throw new IllegalThumbException();
  946. }
  947. }
  948. int getLength() {
  949. return (thumbWidth*thumbHeight + PALETTE_SIZE);
  950. }
  951. BufferedImage getThumbnail(ImageInputStream iis,
  952. JPEGImageReader reader)
  953. throws IOException {
  954. iis.mark();
  955. iis.seek(streamPos);
  956. // read the palette
  957. byte [] palette = new byte [PALETTE_SIZE];
  958. float palettePart = ((float) PALETTE_SIZE) / getLength();
  959. readByteBuffer(iis,
  960. palette,
  961. reader,
  962. palettePart,
  963. 0.0F);
  964. DataBufferByte buffer = new DataBufferByte(thumbWidth*thumbHeight);
  965. readByteBuffer(iis,
  966. buffer.getData(),
  967. reader,
  968. 1.0F-palettePart,
  969. palettePart);
  970. iis.read();
  971. iis.reset();
  972. IndexColorModel cm = new IndexColorModel(8,
  973. 256,
  974. palette,
  975. 0,
  976. false);
  977. SampleModel sm = cm.createCompatibleSampleModel(thumbWidth,
  978. thumbHeight);
  979. WritableRaster raster =
  980. Raster.createWritableRaster(sm, buffer, null);
  981. return new BufferedImage(cm,
  982. raster,
  983. false,
  984. null);
  985. }
  986. void write(ImageOutputStream ios,
  987. JPEGImageWriter writer) throws IOException {
  988. super.write(ios, writer); // width and height
  989. // Write the palette (must be 768 bytes)
  990. byte [] palette = new byte[768];
  991. IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel();
  992. byte [] reds = new byte [256];
  993. byte [] greens = new byte [256];
  994. byte [] blues = new byte [256];
  995. icm.getReds(reds);
  996. icm.getGreens(greens);
  997. icm.getBlues(blues);
  998. for (int i = 0; i < 256; i++) {
  999. palette[i*3] = reds[i];
  1000. palette[i*3+1] = greens[i];
  1001. palette[i*3+2] = blues[i];
  1002. }
  1003. ios.write(palette);
  1004. writePixels(ios, writer);
  1005. }
  1006. }
  1007. /**
  1008. * A JFIF thumbnail stored as a JPEG stream. No JFIF or
  1009. * JFIF extension markers are permitted. There is no need
  1010. * to clip these, but the entire image must fit into a
  1011. * single JFXX marker segment.
  1012. */
  1013. class JFIFThumbJPEG extends JFIFThumb {
  1014. JPEGMetadata thumbMetadata = null;
  1015. byte [] data = null; // Compressed image data, for writing
  1016. private static final int PREAMBLE_SIZE = 6;
  1017. JFIFThumbJPEG(JPEGBuffer buffer,
  1018. int length,
  1019. JPEGImageReader reader) throws IOException {
  1020. super(buffer);
  1021. // Compute the final stream position
  1022. long finalPos = streamPos + (length - PREAMBLE_SIZE);
  1023. // Set the stream back to the start of the thumbnail
  1024. // and read its metadata (but don't decode the image)
  1025. buffer.iis.seek(streamPos);
  1026. thumbMetadata = new JPEGMetadata(false, true, buffer.iis, reader);
  1027. // Set the stream to the computed final position
  1028. buffer.iis.seek(finalPos);
  1029. // Clear the now invalid buffer
  1030. buffer.bufAvail = 0;
  1031. buffer.bufPtr = 0;
  1032. }
  1033. JFIFThumbJPEG(Node node) throws IIOInvalidTreeException {
  1034. if (node.getChildNodes().getLength() > 1) {
  1035. throw new IIOInvalidTreeException
  1036. ("JFIFThumbJPEG node must have 0 or 1 child", node);
  1037. }
  1038. Node child = node.getFirstChild();
  1039. if (child != null) {
  1040. String name = child.getNodeName();
  1041. if (!name.equals("markerSequence")) {
  1042. throw new IIOInvalidTreeException
  1043. ("JFIFThumbJPEG child must be a markerSequence node",
  1044. node);
  1045. }
  1046. thumbMetadata = new JPEGMetadata(false, true);
  1047. thumbMetadata.setFromMarkerSequenceNode(child);
  1048. }
  1049. }
  1050. JFIFThumbJPEG(BufferedImage thumb) throws IllegalThumbException {
  1051. int INITIAL_BUFSIZE = 4096;
  1052. int MAZ_BUFSIZE = 65535 - 2 - PREAMBLE_SIZE;
  1053. try {
  1054. ByteArrayOutputStream baos =
  1055. new ByteArrayOutputStream(INITIAL_BUFSIZE);
  1056. MemoryCacheImageOutputStream mos =
  1057. new MemoryCacheImageOutputStream(baos);
  1058. JPEGImageWriter thumbWriter = new JPEGImageWriter(null);
  1059. thumbWriter.setOutput(mos);
  1060. // get default metadata for the thumb
  1061. JPEGMetadata metadata =
  1062. (JPEGMetadata) thumbWriter.getDefaultImageMetadata
  1063. (new ImageTypeSpecifier(thumb), null);
  1064. // Remove the jfif segment, which should be there.
  1065. MarkerSegment jfif = metadata.findMarkerSegment
  1066. (JFIFMarkerSegment.class, true);
  1067. if (jfif == null) {
  1068. throw new IllegalThumbException();
  1069. }
  1070. metadata.markerSequence.remove(jfif);
  1071. /* Use this if removing leaves a hole and causes trouble
  1072. // Get the tree
  1073. String format = metadata.getNativeMetadataFormatName();
  1074. IIOMetadataNode tree =
  1075. (IIOMetadataNode) metadata.getAsTree(format);
  1076. // If there is no app0jfif node, the image is bad
  1077. NodeList jfifs = tree.getElementsByTagName("app0JFIF");
  1078. if (jfifs.getLength() == 0) {
  1079. throw new IllegalThumbException();
  1080. }
  1081. // remove the app0jfif node
  1082. Node jfif = jfifs.item(0);
  1083. Node parent = jfif.getParentNode();
  1084. parent.removeChild(jfif);
  1085. metadata.setFromTree(format, tree);
  1086. */
  1087. thumbWriter.write(new IIOImage(thumb, null, metadata));
  1088. thumbWriter.dispose();
  1089. // Now check that the size is OK
  1090. if (baos.size() > MAZ_BUFSIZE) {
  1091. throw new IllegalThumbException();
  1092. }
  1093. data = baos.toByteArray();
  1094. } catch (IOException e) {
  1095. throw new IllegalThumbException();
  1096. }
  1097. }
  1098. int getWidth() {
  1099. int retval = 0;
  1100. SOFMarkerSegment sof =
  1101. (SOFMarkerSegment) thumbMetadata.findMarkerSegment
  1102. (SOFMarkerSegment.class, true);
  1103. if (sof != null) {
  1104. retval = sof.samplesPerLine;
  1105. }
  1106. return retval;
  1107. }
  1108. int getHeight() {
  1109. int retval = 0;
  1110. SOFMarkerSegment sof =
  1111. (SOFMarkerSegment) thumbMetadata.findMarkerSegment
  1112. (SOFMarkerSegment.class, true);
  1113. if (sof != null) {
  1114. retval = sof.numLines;
  1115. }
  1116. return retval;
  1117. }
  1118. private class ThumbnailReadListener
  1119. implements IIOReadProgressListener {
  1120. JPEGImageReader reader = null;
  1121. ThumbnailReadListener (JPEGImageReader reader) {
  1122. this.reader = reader;
  1123. }
  1124. public void sequenceStarted(ImageReader source, int minIndex) {}
  1125. public void sequenceComplete(ImageReader source) {}
  1126. public void imageStarted(ImageReader source, int imageIndex) {}
  1127. public void imageProgress(ImageReader source,
  1128. float percentageDone) {
  1129. reader.thumbnailProgress(percentageDone);
  1130. }
  1131. public void imageComplete(ImageReader source) {}
  1132. public void thumbnailStarted(ImageReader source,
  1133. int imageIndex, int thumbnailIndex) {}
  1134. public void thumbnailProgress(ImageReader source, float percentageDone) {}
  1135. public void thumbnailComplete(ImageReader source) {}
  1136. public void readAborted(ImageReader source) {}
  1137. }
  1138. BufferedImage getThumbnail(ImageInputStream iis,
  1139. JPEGImageReader reader)
  1140. throws IOException {
  1141. iis.mark();
  1142. iis.seek(streamPos);
  1143. JPEGImageReader thumbReader = new JPEGImageReader(null);
  1144. thumbReader.setInput(iis);
  1145. thumbReader.addIIOReadProgressListener
  1146. (new ThumbnailReadListener(reader));
  1147. BufferedImage ret = thumbReader.read(0, null);
  1148. thumbReader.dispose();
  1149. iis.reset();
  1150. return ret;
  1151. }
  1152. protected Object clone() {
  1153. JFIFThumbJPEG newGuy = (JFIFThumbJPEG) super.clone();
  1154. if (thumbMetadata != null) {
  1155. newGuy.thumbMetadata = (JPEGMetadata) thumbMetadata.clone();
  1156. }
  1157. return newGuy;
  1158. }
  1159. IIOMetadataNode getNativeNode() {
  1160. IIOMetadataNode node = new IIOMetadataNode("JFIFthumbJPEG");
  1161. if (thumbMetadata != null) {
  1162. node.appendChild(thumbMetadata.getNativeTree());
  1163. }
  1164. return node;
  1165. }
  1166. int getLength() {
  1167. if (data == null) {
  1168. return 0;
  1169. } else {
  1170. return data.length;
  1171. }
  1172. }
  1173. void write(ImageOutputStream ios,
  1174. JPEGImageWriter writer) throws IOException {
  1175. int progInterval = data.length / 20; // approx. every 5%
  1176. if (progInterval == 0) {
  1177. progInterval = 1;
  1178. }
  1179. for (int offset = 0;
  1180. offset < data.length;) {
  1181. int len = Math.min(progInterval, data.length-offset);
  1182. ios.write(data, offset, len);
  1183. offset += progInterval;
  1184. float percentDone = ((float) offset * 100) / data.length;
  1185. if (percentDone > 100.0F) {
  1186. percentDone = 100.0F;
  1187. }
  1188. writer.thumbnailProgress (percentDone);
  1189. }
  1190. }
  1191. void print () {
  1192. System.out.println("JFIF thumbnail stored as JPEG");
  1193. }
  1194. }
  1195. /**
  1196. * Write out the given profile to the stream, embedded in
  1197. * the necessary number of APP2 segments, per the ICC spec.
  1198. * This is the only mechanism for writing an ICC profile
  1199. * to a stream.
  1200. */
  1201. static void writeICC(ICC_Profile profile, ImageOutputStream ios)
  1202. throws IOException {
  1203. int LENGTH_LENGTH = 2;
  1204. final String ID = "ICC_PROFILE";
  1205. int ID_LENGTH = ID.length()+1; // spec says it's null-terminated
  1206. int COUNTS_LENGTH = 2;
  1207. int MAX_ICC_CHUNK_SIZE =
  1208. 65535 - LENGTH_LENGTH - ID_LENGTH - COUNTS_LENGTH;
  1209. byte [] data = profile.getData();
  1210. int numChunks = data.length / MAX_ICC_CHUNK_SIZE;
  1211. if ((data.length % MAX_ICC_CHUNK_SIZE) != 0) {
  1212. numChunks++;
  1213. }
  1214. int chunkNum = 1;
  1215. int offset = 0;
  1216. for (int i = 0; i < numChunks; i++) {
  1217. int dataLength = Math.min(data.length-offset, MAX_ICC_CHUNK_SIZE);
  1218. int segLength = dataLength+COUNTS_LENGTH+ID_LENGTH+LENGTH_LENGTH;
  1219. ios.write(0xff);
  1220. ios.write(JPEG.APP2);
  1221. MarkerSegment.write2bytes(ios, segLength);
  1222. byte [] id = ID.getBytes("US-ASCII");
  1223. ios.write(id);
  1224. ios.write(0); // Null-terminate the string
  1225. ios.write(chunkNum++);
  1226. ios.write(numChunks);
  1227. ios.write(data, offset, dataLength);
  1228. offset += dataLength;
  1229. }
  1230. }
  1231. /**
  1232. * An APP2 marker segment containing an ICC profile. In the stream
  1233. * a profile larger than 64K is broken up into a series of chunks.
  1234. * This inner class represents the complete profile as a single objec,
  1235. * combining chunks as necessary.
  1236. */
  1237. class ICCMarkerSegment extends MarkerSegment {
  1238. ArrayList chunks = null;
  1239. byte [] profile = null; // The complete profile when it's fully read
  1240. // May remain null when writing
  1241. private static final int ID_SIZE = 12;
  1242. int chunksRead;
  1243. int numChunks;
  1244. ICCMarkerSegment(ICC_ColorSpace cs) {
  1245. super(JPEG.APP2);
  1246. chunks = null;
  1247. chunksRead = 0;
  1248. numChunks = 0;
  1249. profile = cs.getProfile().getData();
  1250. }
  1251. ICCMarkerSegment(JPEGBuffer buffer) throws IOException {
  1252. super(buffer); // gets whole segment or fills the buffer
  1253. if (debug) {
  1254. System.out.println("Creating new ICC segment");
  1255. }
  1256. buffer.bufPtr += ID_SIZE; // Skip the id
  1257. buffer.bufAvail -= ID_SIZE;
  1258. /*
  1259. * Reduce the stored length by the id size. The stored
  1260. * length is used to store the length of the profile
  1261. * data only.
  1262. */
  1263. length -= ID_SIZE;
  1264. // get the chunk number
  1265. int chunkNum = buffer.buf[buffer.bufPtr] & 0xff;
  1266. // get the total number of chunks
  1267. numChunks = buffer.buf[buffer.bufPtr+1] & 0xff;
  1268. if (chunkNum > numChunks) {
  1269. throw new IIOException
  1270. ("Image format Error; chunk num > num chunks");
  1271. }
  1272. // if there are no more chunks, set up the data
  1273. if (numChunks == 1) {
  1274. // reduce the stored length by the two chunk numbering bytes
  1275. length -= 2;
  1276. profile = new byte[length];
  1277. buffer.bufPtr += 2;
  1278. buffer.bufAvail-=2;
  1279. buffer.readData(profile);
  1280. inICC = false;
  1281. } else {
  1282. // If we store them away, include the chunk numbering bytes
  1283. byte [] profileData = new byte[length];
  1284. // Now reduce the stored length by the
  1285. // two chunk numbering bytes
  1286. length -= 2;
  1287. buffer.readData(profileData);
  1288. chunks = new ArrayList();
  1289. chunks.add(profileData);
  1290. chunksRead = 1;
  1291. inICC = true;
  1292. }
  1293. }
  1294. ICCMarkerSegment(Node node) throws IIOInvalidTreeException {
  1295. super(JPEG.APP2);
  1296. if (node instanceof IIOMetadataNode) {
  1297. IIOMetadataNode ourNode = (IIOMetadataNode) node;
  1298. ICC_Profile prof = (ICC_Profile) ourNode.getUserObject();
  1299. if (prof != null) { // May be null
  1300. profile = prof.getData();
  1301. }
  1302. }
  1303. }
  1304. protected Object clone () {
  1305. ICCMarkerSegment newGuy = (ICCMarkerSegment) super.clone();
  1306. if (profile != null) {
  1307. newGuy.profile = (byte[]) profile.clone();
  1308. }
  1309. return newGuy;
  1310. }
  1311. boolean addData(JPEGBuffer buffer) throws IOException {
  1312. if (debug) {
  1313. System.out.println("Adding to ICC segment");
  1314. }
  1315. // skip the tag
  1316. buffer.bufPtr++;
  1317. buffer.bufAvail--;
  1318. // Get the length, but not in length
  1319. int dataLen = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
  1320. dataLen |= buffer.buf[buffer.bufPtr++] & 0xff;
  1321. buffer.bufAvail -= 2;
  1322. // Don't include length itself
  1323. dataLen -= 2;
  1324. // skip the id
  1325. buffer.bufPtr += ID_SIZE; // Skip the id
  1326. buffer.bufAvail -= ID_SIZE;
  1327. /*
  1328. * Reduce the stored length by the id size. The stored
  1329. * length is used to store the length of the profile
  1330. * data only.
  1331. */
  1332. dataLen -= ID_SIZE;
  1333. // get the chunk number
  1334. int chunkNum = buffer.buf[buffer.bufPtr] & 0xff;
  1335. if (chunkNum > numChunks) {
  1336. throw new IIOException
  1337. ("Image format Error; chunk num > num chunks");
  1338. }
  1339. // get the number of chunks, which should match
  1340. int newNumChunks = buffer.buf[buffer.bufPtr+1] & 0xff;
  1341. if (numChunks != newNumChunks) {
  1342. throw new IIOException
  1343. ("Image format Error; icc num chunks mismatch");
  1344. }
  1345. dataLen -= 2;
  1346. if (debug) {
  1347. System.out.println("chunkNum: " + chunkNum
  1348. + ", numChunks: " + numChunks
  1349. + ", dataLen: " + dataLen);
  1350. }
  1351. boolean retval = false;
  1352. byte [] profileData = new byte[dataLen];
  1353. buffer.readData(profileData);
  1354. chunks.add(profileData);
  1355. length += dataLen;
  1356. chunksRead++;
  1357. if (chunksRead < numChunks) {
  1358. inICC = true;
  1359. } else {
  1360. if (debug) {
  1361. System.out.println("Completing profile; total length is "
  1362. + length);
  1363. }
  1364. // create an array for the whole thing
  1365. profile = new byte[length];
  1366. // copy the existing chunks, releasing them
  1367. // Note that they may be out of order
  1368. int index = 0;
  1369. for (int i = 1; i <= numChunks; i++) {
  1370. boolean foundIt = false;
  1371. for (int chunk = 0; chunk < chunks.size(); chunk++) {
  1372. byte [] chunkData = (byte []) chunks.get(chunk);
  1373. if (chunkData[0] == i) { // Right one
  1374. System.arraycopy(chunkData, 2,
  1375. profile, index,
  1376. chunkData.length-2);
  1377. index += chunkData.length-2;
  1378. foundIt = true;
  1379. }
  1380. }
  1381. if (foundIt == false) {
  1382. throw new IIOException
  1383. ("Image Format Error: Missing ICC chunk num " + i);
  1384. }
  1385. }
  1386. chunks = null;
  1387. chunksRead = 0;
  1388. numChunks = 0;
  1389. inICC = false;
  1390. retval = true;
  1391. }
  1392. return retval;
  1393. }
  1394. IIOMetadataNode getNativeNode() {
  1395. IIOMetadataNode node = new IIOMetadataNode("app2ICC");
  1396. if (profile != null) {
  1397. node.setUserObject(ICC_Profile.getInstance(profile));
  1398. }
  1399. return node;
  1400. }
  1401. /**
  1402. * No-op. Profiles are never written from metadata.
  1403. * They are written from the ColorSpace of the image.
  1404. */
  1405. void write(ImageOutputStream ios) throws IOException {
  1406. // No-op
  1407. }
  1408. void print () {
  1409. printTag("ICC Profile APP2");
  1410. }
  1411. }
  1412. }