JpegImageParser.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.imaging.formats.jpeg;
import static org.apache.commons.imaging.common.BinaryFunctions.remainingBytes;
import static org.apache.commons.imaging.common.BinaryFunctions.startsWith;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.imaging.AbstractImageParser;
import org.apache.commons.imaging.ImageFormat;
import org.apache.commons.imaging.ImageFormats;
import org.apache.commons.imaging.ImageInfo;
import org.apache.commons.imaging.ImagingException;
import org.apache.commons.imaging.bytesource.ByteSource;
import org.apache.commons.imaging.common.Allocator;
import org.apache.commons.imaging.common.ImageMetadata;
import org.apache.commons.imaging.common.XmpEmbeddable;
import org.apache.commons.imaging.common.XmpImagingParameters;
import org.apache.commons.imaging.formats.jpeg.decoder.JpegDecoder;
import org.apache.commons.imaging.formats.jpeg.iptc.IptcParser;
import org.apache.commons.imaging.formats.jpeg.iptc.PhotoshopApp13Data;
import org.apache.commons.imaging.formats.jpeg.segments.AbstractSegment;
import org.apache.commons.imaging.formats.jpeg.segments.App13Segment;
import org.apache.commons.imaging.formats.jpeg.segments.App14Segment;
import org.apache.commons.imaging.formats.jpeg.segments.App2Segment;
import org.apache.commons.imaging.formats.jpeg.segments.ComSegment;
import org.apache.commons.imaging.formats.jpeg.segments.DqtSegment;
import org.apache.commons.imaging.formats.jpeg.segments.GenericSegment;
import org.apache.commons.imaging.formats.jpeg.segments.JfifSegment;
import org.apache.commons.imaging.formats.jpeg.segments.SofnSegment;
import org.apache.commons.imaging.formats.jpeg.segments.UnknownSegment;
import org.apache.commons.imaging.formats.jpeg.xmp.JpegXmpParser;
import org.apache.commons.imaging.formats.tiff.TiffField;
import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
import org.apache.commons.imaging.formats.tiff.TiffImageParser;
import org.apache.commons.imaging.formats.tiff.TiffImagingParameters;
import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
import org.apache.commons.imaging.internal.Debug;
public class JpegImageParser extends AbstractImageParser<JpegImagingParameters> implements XmpEmbeddable<JpegImagingParameters> {
private static final Logger LOGGER = Logger.getLogger(JpegImageParser.class.getName());
private static final String DEFAULT_EXTENSION = ImageFormats.JPEG.getDefaultExtension();
private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.JPEG.getExtensions();
public static boolean isExifApp1Segment(final GenericSegment segment) {
return startsWith(segment.getSegmentData(), JpegConstants.EXIF_IDENTIFIER_CODE);
}
private byte[] assembleSegments(final List<App2Segment> segments) throws ImagingException {
try {
return assembleSegments(segments, false);
} catch (final ImagingException e) {
return assembleSegments(segments, true);
}
}
private byte[] assembleSegments(final List<App2Segment> segments, final boolean startWithZero) throws ImagingException {
if (segments.isEmpty()) {
throw new ImagingException("No App2 Segments Found.");
}
final int markerCount = segments.get(0).numMarkers;
if (segments.size() != markerCount) {
throw new ImagingException("App2 Segments Missing. Found: " + segments.size() + ", Expected: " + markerCount + ".");
}
segments.sort(null);
final int offset = startWithZero ? 0 : 1;
int total = 0;
for (int i = 0; i < segments.size(); i++) {
final App2Segment segment = segments.get(i);
if (i + offset != segment.curMarker) {
dumpSegments(segments);
throw new ImagingException("Incoherent App2 Segment Ordering. i: " + i + ", segment[" + i + "].curMarker: " + segment.curMarker + ".");
}
if (markerCount != segment.numMarkers) {
dumpSegments(segments);
throw new ImagingException(
"Inconsistent App2 Segment Count info. markerCount: " + markerCount + ", segment[" + i + "].numMarkers: " + segment.numMarkers + ".");
}
if (segment.getIccBytes() != null) {
total += segment.getIccBytes().length;
}
}
final byte[] result = Allocator.byteArray(total);
int progress = 0;
for (final App2Segment segment : segments) {
System.arraycopy(segment.getIccBytes(), 0, result, progress, segment.getIccBytes().length);
progress += segment.getIccBytes().length;
}
return result;
}
@Override
public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
pw.println("jpeg.dumpImageFile");
{
final ImageInfo imageInfo = getImageInfo(byteSource);
if (imageInfo == null) {
return false;
}
imageInfo.toString(pw, "");
}
pw.println("");
{
final List<AbstractSegment> abstractSegments = readSegments(byteSource, null, false);
if (abstractSegments == null) {
throw new ImagingException("No Segments Found.");
}
for (int d = 0; d < abstractSegments.size(); d++) {
final AbstractSegment abstractSegment = abstractSegments.get(d);
final NumberFormat nf = NumberFormat.getIntegerInstance();
// this.debugNumber("found, marker: ", marker, 4);
pw.println(d + ": marker: " + Integer.toHexString(abstractSegment.marker) + ", " + abstractSegment.getDescription() + " (length: "
+ nf.format(abstractSegment.length) + ")");
abstractSegment.dump(pw);
}
pw.println("");
}
return true;
}
private void dumpSegments(final List<? extends AbstractSegment> v) {
Debug.debug();
Debug.debug("dumpSegments: " + v.size());
for (int i = 0; i < v.size(); i++) {
final App2Segment segment = (App2Segment) v.get(i);
Debug.debug(i + ": " + segment.curMarker + " / " + segment.numMarkers);
}
Debug.debug();
}
private List<AbstractSegment> filterApp1Segments(final List<AbstractSegment> abstractSegments) {
final List<AbstractSegment> result = new ArrayList<>();
for (final AbstractSegment s : abstractSegments) {
final GenericSegment segment = (GenericSegment) s;
if (isExifApp1Segment(segment)) {
result.add(segment);
}
}
return result;
}
@Override
protected String[] getAcceptedExtensions() {
return ACCEPTED_EXTENSIONS;
}
@Override
protected ImageFormat[] getAcceptedTypes() {
return new ImageFormat[] { ImageFormats.JPEG, //
};
}
@Override
public final BufferedImage getBufferedImage(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException {
final JpegDecoder jpegDecoder = new JpegDecoder();
return jpegDecoder.decode(byteSource);
}
@Override
public String getDefaultExtension() {
return DEFAULT_EXTENSION;
}
@Override
public JpegImagingParameters getDefaultParameters() {
return new JpegImagingParameters();
}
public TiffImageMetadata getExifMetadata(final ByteSource byteSource, TiffImagingParameters params) throws ImagingException, IOException {
final byte[] bytes = getExifRawData(byteSource);
if (null == bytes) {
return null;
}
if (params == null) {
params = new TiffImagingParameters();
}
params.setReadThumbnails(Boolean.TRUE);
return (TiffImageMetadata) new TiffImageParser().getMetadata(bytes, params);
}
public byte[] getExifRawData(final ByteSource byteSource) throws ImagingException, IOException {
final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP1_MARKER, }, false);
if (abstractSegments == null || abstractSegments.isEmpty()) {
return null;
}
final List<AbstractSegment> exifSegments = filterApp1Segments(abstractSegments);
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("exifSegments.size()" + ": " + exifSegments.size());
}
// Debug.debug("segments", segments);
// Debug.debug("exifSegments", exifSegments);
// TODO: concatenate if multiple segments, need example.
if (exifSegments.isEmpty()) {
return null;
}
if (exifSegments.size() > 1) {
throw new ImagingException(
"Imaging currently can't parse EXIF metadata split across multiple APP1 segments. " + "Please send this image to the Imaging project.");
}
final GenericSegment segment = (GenericSegment) exifSegments.get(0);
final byte[] bytes = segment.getSegmentData();
// byte[] head = readBytearray("exif head", bytes, 0, 6);
//
// Debug.debug("head", head);
return remainingBytes("trimmed exif bytes", bytes, 6);
}
@Override
public byte[] getIccProfileBytes(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException {
final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP2_MARKER, }, false);
final List<App2Segment> filtered = new ArrayList<>();
if (abstractSegments != null) {
// throw away non-icc profile app2 segments.
for (final AbstractSegment s : abstractSegments) {
final App2Segment segment = (App2Segment) s;
if (segment.getIccBytes() != null) {
filtered.add(segment);
}
}
}
if (filtered.isEmpty()) {
return null;
}
final byte[] bytes = assembleSegments(filtered);
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("bytes" + ": " + bytes.length);
}
return bytes;
}
@Override
public ImageInfo getImageInfo(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException {
// List allSegments = readSegments(byteSource, null, false);
final List<AbstractSegment> SOF_segments = readSegments(byteSource, new int[] {
// kJFIFMarker,
JpegConstants.SOF0_MARKER, JpegConstants.SOF1_MARKER, JpegConstants.SOF2_MARKER, JpegConstants.SOF3_MARKER, JpegConstants.SOF5_MARKER,
JpegConstants.SOF6_MARKER, JpegConstants.SOF7_MARKER, JpegConstants.SOF9_MARKER, JpegConstants.SOF10_MARKER, JpegConstants.SOF11_MARKER,
JpegConstants.SOF13_MARKER, JpegConstants.SOF14_MARKER, JpegConstants.SOF15_MARKER,
}, false);
if (SOF_segments == null) {
throw new ImagingException("No SOFN Data Found.");
}
// if (SOF_segments.size() != 1)
// System.out.println("Incoherent SOFN Data Found: "
// + SOF_segments.size());
final List<AbstractSegment> jfifSegments = readSegments(byteSource, new int[] { JpegConstants.JFIF_MARKER, }, true);
final SofnSegment fSOFNSegment = (SofnSegment) SOF_segments.get(0);
// SofnSegment fSOFNSegment = (SofnSegment) findSegment(segments,
// SOFNmarkers);
if (fSOFNSegment == null) {
throw new ImagingException("No SOFN Data Found.");
}
final int width = fSOFNSegment.width;
final int height = fSOFNSegment.height;
JfifSegment jfifSegment = null;
if (jfifSegments != null && !jfifSegments.isEmpty()) {
jfifSegment = (JfifSegment) jfifSegments.get(0);
}
final List<AbstractSegment> app14Segments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP14_MARKER }, true);
App14Segment app14Segment = null;
if (app14Segments != null && !app14Segments.isEmpty()) {
app14Segment = (App14Segment) app14Segments.get(0);
}
// JfifSegment fTheJFIFSegment = (JfifSegment) findSegment(segments,
// kJFIFMarker);
double xDensity = -1.0;
double yDensity = -1.0;
double unitsPerInch = -1.0;
// int JFIF_major_version;
// int JFIF_minor_version;
String formatDetails;
if (jfifSegment != null) {
xDensity = jfifSegment.xDensity;
yDensity = jfifSegment.yDensity;
final int densityUnits = jfifSegment.densityUnits;
// JFIF_major_version = fTheJFIFSegment.JFIF_major_version;
// JFIF_minor_version = fTheJFIFSegment.JFIF_minor_version;
formatDetails = "Jpeg/JFIF v." + jfifSegment.jfifMajorVersion + "." + jfifSegment.jfifMinorVersion;
switch (densityUnits) {
case 0:
break;
case 1: // inches
unitsPerInch = 1.0;
break;
case 2: // cms
unitsPerInch = 2.54;
break;
default:
break;
}
} else {
final JpegImageMetadata metadata = (JpegImageMetadata) getMetadata(byteSource, params);
if (metadata != null) {
{
final TiffField field = metadata.findExifValue(TiffTagConstants.TIFF_TAG_XRESOLUTION);
if (field != null) {
xDensity = ((Number) field.getValue()).doubleValue();
}
}
{
final TiffField field = metadata.findExifValue(TiffTagConstants.TIFF_TAG_YRESOLUTION);
if (field != null) {
yDensity = ((Number) field.getValue()).doubleValue();
}
}
{
final TiffField field = metadata.findExifValue(TiffTagConstants.TIFF_TAG_RESOLUTION_UNIT);
if (field != null) {
final int densityUnits = ((Number) field.getValue()).intValue();
switch (densityUnits) {
case 1:
break;
case 2: // inches
unitsPerInch = 1.0;
break;
case 3: // cms
unitsPerInch = 2.54;
break;
default:
break;
}
}
}
}
formatDetails = "Jpeg/DCM";
}
int physicalHeightDpi = -1;
float physicalHeightInch = -1;
int physicalWidthDpi = -1;
float physicalWidthInch = -1;
if (unitsPerInch > 0) {
physicalWidthDpi = (int) Math.round(xDensity * unitsPerInch);
physicalWidthInch = (float) (width / (xDensity * unitsPerInch));
physicalHeightDpi = (int) Math.round(yDensity * unitsPerInch);
physicalHeightInch = (float) (height / (yDensity * unitsPerInch));
}
final List<AbstractSegment> commentSegments = readSegments(byteSource, new int[] { JpegConstants.COM_MARKER }, false);
final List<String> comments = Allocator.arrayList(commentSegments.size());
for (final AbstractSegment commentSegment : commentSegments) {
final ComSegment comSegment = (ComSegment) commentSegment;
comments.add(new String(comSegment.getComment(), StandardCharsets.UTF_8));
}
final int numberOfComponents = fSOFNSegment.numberOfComponents;
final int precision = fSOFNSegment.precision;
final int bitsPerPixel = numberOfComponents * precision;
final ImageFormat format = ImageFormats.JPEG;
final String formatName = "JPEG (Joint Photographic Experts Group) Format";
final String mimeType = "image/jpeg";
// TODO: we ought to count images, but don't yet.
final int numberOfImages = 1;
// not accurate ... only reflects first
final boolean progressive = fSOFNSegment.marker == JpegConstants.SOF2_MARKER;
boolean transparent = false;
final boolean usesPalette = false; // TODO: inaccurate.
// See https://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/doc-files/jpeg_metadata.html#color
ImageInfo.ColorType colorType = ImageInfo.ColorType.UNKNOWN;
// Some images have both JFIF/APP0 and APP14.
// JFIF is meant to win but in them APP14 is clearly right, so make it win.
if (app14Segment != null && app14Segment.isAdobeJpegSegment()) {
final int colorTransform = app14Segment.getAdobeColorTransform();
switch (colorTransform) {
case App14Segment.ADOBE_COLOR_TRANSFORM_UNKNOWN:
if (numberOfComponents == 3) {
colorType = ImageInfo.ColorType.RGB;
} else if (numberOfComponents == 4) {
colorType = ImageInfo.ColorType.CMYK;
}
break;
case App14Segment.ADOBE_COLOR_TRANSFORM_YCbCr:
colorType = ImageInfo.ColorType.YCbCr;
break;
case App14Segment.ADOBE_COLOR_TRANSFORM_YCCK:
colorType = ImageInfo.ColorType.YCCK;
break;
default:
break;
}
} else if (jfifSegment != null) {
if (numberOfComponents == 1) {
colorType = ImageInfo.ColorType.GRAYSCALE;
} else if (numberOfComponents == 3) {
colorType = ImageInfo.ColorType.YCbCr;
}
} else {
switch (numberOfComponents) {
case 1:
colorType = ImageInfo.ColorType.GRAYSCALE;
break;
case 2:
colorType = ImageInfo.ColorType.GRAYSCALE;
transparent = true;
break;
case 3:
case 4:
boolean have1 = false;
boolean have2 = false;
boolean have3 = false;
boolean have4 = false;
boolean haveOther = false;
for (final SofnSegment.Component component : fSOFNSegment.getComponents()) {
final int id = component.componentIdentifier;
switch (id) {
case 1:
have1 = true;
break;
case 2:
have2 = true;
break;
case 3:
have3 = true;
break;
case 4:
have4 = true;
break;
default:
haveOther = true;
break;
}
}
if (numberOfComponents == 3 && have1 && have2 && have3 && !have4 && !haveOther) {
colorType = ImageInfo.ColorType.YCbCr;
} else if (numberOfComponents == 4 && have1 && have2 && have3 && have4 && !haveOther) {
colorType = ImageInfo.ColorType.YCbCr;
transparent = true;
} else {
boolean haveR = false;
boolean haveG = false;
boolean haveB = false;
boolean haveA = false;
boolean haveC = false;
boolean havec = false;
boolean haveY = false;
for (final SofnSegment.Component component : fSOFNSegment.getComponents()) {
final int id = component.componentIdentifier;
switch (id) {
case 'R':
haveR = true;
break;
case 'G':
haveG = true;
break;
case 'B':
haveB = true;
break;
case 'A':
haveA = true;
break;
case 'C':
haveC = true;
break;
case 'c':
havec = true;
break;
case 'Y':
haveY = true;
break;
default:
break;
}
}
if (haveR && haveG && haveB && !haveA && !haveC && !havec && !haveY) {
colorType = ImageInfo.ColorType.RGB;
} else if (haveR && haveG && haveB && haveA && !haveC && !havec && !haveY) {
colorType = ImageInfo.ColorType.RGB;
transparent = true;
} else if (haveY && haveC && havec && !haveR && !haveG && !haveB && !haveA) {
colorType = ImageInfo.ColorType.YCC;
} else if (haveY && haveC && havec && haveA && !haveR && !haveG && !haveB) {
colorType = ImageInfo.ColorType.YCC;
transparent = true;
} else {
int minHorizontalSamplingFactor = Integer.MAX_VALUE;
int maxHorizontalSmaplingFactor = Integer.MIN_VALUE;
int minVerticalSamplingFactor = Integer.MAX_VALUE;
int maxVerticalSamplingFactor = Integer.MIN_VALUE;
for (final SofnSegment.Component component : fSOFNSegment.getComponents()) {
if (minHorizontalSamplingFactor > component.horizontalSamplingFactor) {
minHorizontalSamplingFactor = component.horizontalSamplingFactor;
}
if (maxHorizontalSmaplingFactor < component.horizontalSamplingFactor) {
maxHorizontalSmaplingFactor = component.horizontalSamplingFactor;
}
if (minVerticalSamplingFactor > component.verticalSamplingFactor) {
minVerticalSamplingFactor = component.verticalSamplingFactor;
}
if (maxVerticalSamplingFactor < component.verticalSamplingFactor) {
maxVerticalSamplingFactor = component.verticalSamplingFactor;
}
}
final boolean isSubsampled = minHorizontalSamplingFactor != maxHorizontalSmaplingFactor
|| minVerticalSamplingFactor != maxVerticalSamplingFactor;
if (numberOfComponents == 3) {
if (isSubsampled) {
colorType = ImageInfo.ColorType.YCbCr;
} else {
colorType = ImageInfo.ColorType.RGB;
}
} else if (numberOfComponents == 4) {
if (isSubsampled) {
colorType = ImageInfo.ColorType.YCCK;
} else {
colorType = ImageInfo.ColorType.CMYK;
}
}
}
}
break;
default:
break;
}
}
final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.JPEG;
return new ImageInfo(formatDetails, bitsPerPixel, comments, format, formatName, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch,
physicalWidthDpi, physicalWidthInch, width, progressive, transparent, usesPalette, colorType, compressionAlgorithm);
}
@Override
public Dimension getImageSize(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException {
final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] {
// kJFIFMarker,
JpegConstants.SOF0_MARKER, JpegConstants.SOF1_MARKER, JpegConstants.SOF2_MARKER, JpegConstants.SOF3_MARKER, JpegConstants.SOF5_MARKER,
JpegConstants.SOF6_MARKER, JpegConstants.SOF7_MARKER, JpegConstants.SOF9_MARKER, JpegConstants.SOF10_MARKER, JpegConstants.SOF11_MARKER,
JpegConstants.SOF13_MARKER, JpegConstants.SOF14_MARKER, JpegConstants.SOF15_MARKER,
}, true);
if (abstractSegments == null || abstractSegments.isEmpty()) {
throw new ImagingException("No JFIF Data Found.");
}
if (abstractSegments.size() > 1) {
throw new ImagingException("Redundant JFIF Data Found.");
}
final SofnSegment fSOFNSegment = (SofnSegment) abstractSegments.get(0);
return new Dimension(fSOFNSegment.width, fSOFNSegment.height);
}
@Override
public ImageMetadata getMetadata(final ByteSource byteSource, JpegImagingParameters params) throws ImagingException, IOException {
if (params == null) {
params = new JpegImagingParameters();
}
final TiffImageMetadata exif = getExifMetadata(byteSource, new TiffImagingParameters());
final JpegPhotoshopMetadata photoshop = getPhotoshopMetadata(byteSource, params);
if (null == exif && null == photoshop) {
return null;
}
return new JpegImageMetadata(photoshop, exif);
}
@Override
public String getName() {
return "Jpeg-Custom";
}
public JpegPhotoshopMetadata getPhotoshopMetadata(final ByteSource byteSource, final JpegImagingParameters params) throws ImagingException, IOException {
final List<AbstractSegment> abstractSegments = readSegments(byteSource, new int[] { JpegConstants.JPEG_APP13_MARKER, }, false);
if (abstractSegments == null || abstractSegments.isEmpty()) {
return null;
}
PhotoshopApp13Data photoshopApp13Data = null;
for (final AbstractSegment s : abstractSegments) {
final App13Segment segment = (App13Segment) s;
final PhotoshopApp13Data data = segment.parsePhotoshopSegment(params);
if (data != null) {
if (photoshopApp13Data != null) {
throw new ImagingException("JPEG contains more than one Photoshop App13 segment.");
}
photoshopApp13Data = data;
}
}
if (null == photoshopApp13Data) {
return null;
}
return new JpegPhotoshopMetadata(photoshopApp13Data);
}
/**
* Extracts embedded XML metadata as XML string.
* <p>
*
* @param byteSource File containing image data.
* @param params Map of optional parameters, defined in ImagingConstants.
* @return Xmp Xml as String, if present. Otherwise, returns null.
*/
@Override
public String getXmpXml(final ByteSource byteSource, final XmpImagingParameters<JpegImagingParameters> params) throws ImagingException, IOException {
final List<String> result = new ArrayList<>();
final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
// return false to exit before reading image data.
@Override
public boolean beginSos() {
return false;
}
// return false to exit traversal.
@Override
public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
final byte[] segmentData) throws ImagingException {
if (marker == 0xffd9) {
return false;
}
if (marker == JpegConstants.JPEG_APP1_MARKER) {
if (new JpegXmpParser().isXmpJpegSegment(segmentData)) {
result.add(new JpegXmpParser().parseXmpJpegSegment(segmentData));
return false;
}
}
return true;
}
@Override
public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
// don't need image data
}
};
new JpegUtils().traverseJfif(byteSource, visitor);
if (result.isEmpty()) {
return null;
}
if (result.size() > 1) {
throw new ImagingException("JPEG file contains more than one XMP segment.");
}
return result.get(0);
}
public boolean hasExifSegment(final ByteSource byteSource) throws ImagingException, IOException {
final boolean[] result = { false, };
final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
// return false to exit before reading image data.
@Override
public boolean beginSos() {
return false;
}
// return false to exit traversal.
@Override
public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
final byte[] segmentData) {
if (marker == 0xffd9) {
return false;
}
if (marker == JpegConstants.JPEG_APP1_MARKER) {
if (startsWith(segmentData, JpegConstants.EXIF_IDENTIFIER_CODE)) {
result[0] = true;
return false;
}
}
return true;
}
@Override
public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
// don't need image data
}
};
new JpegUtils().traverseJfif(byteSource, visitor);
return result[0];
}
public boolean hasIptcSegment(final ByteSource byteSource) throws ImagingException, IOException {
final boolean[] result = { false, };
final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
// return false to exit before reading image data.
@Override
public boolean beginSos() {
return false;
}
// return false to exit traversal.
@Override
public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
final byte[] segmentData) {
if (marker == 0xffd9) {
return false;
}
if (marker == JpegConstants.JPEG_APP13_MARKER) {
if (new IptcParser().isPhotoshopJpegSegment(segmentData)) {
result[0] = true;
return false;
}
}
return true;
}
@Override
public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
// don't need image data
}
};
new JpegUtils().traverseJfif(byteSource, visitor);
return result[0];
}
public boolean hasXmpSegment(final ByteSource byteSource) throws ImagingException, IOException {
final boolean[] result = { false, };
final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
// return false to exit before reading image data.
@Override
public boolean beginSos() {
return false;
}
// return false to exit traversal.
@Override
public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
final byte[] segmentData) {
if (marker == 0xffd9) {
return false;
}
if (marker == JpegConstants.JPEG_APP1_MARKER) {
if (new JpegXmpParser().isXmpJpegSegment(segmentData)) {
result[0] = true;
return false;
}
}
return true;
}
@Override
public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
// don't need image data
}
};
new JpegUtils().traverseJfif(byteSource, visitor);
return result[0];
}
private boolean keepMarker(final int marker, final int[] markers) {
if (markers == null) {
return true;
}
for (final int marker2 : markers) {
if (marker2 == marker) {
return true;
}
}
return false;
}
public List<AbstractSegment> readSegments(final ByteSource byteSource, final int[] markers, final boolean returnAfterFirst)
throws ImagingException, IOException {
final List<AbstractSegment> result = new ArrayList<>();
final int[] sofnSegments = {
// kJFIFMarker,
JpegConstants.SOF0_MARKER, JpegConstants.SOF1_MARKER, JpegConstants.SOF2_MARKER, JpegConstants.SOF3_MARKER, JpegConstants.SOF5_MARKER,
JpegConstants.SOF6_MARKER, JpegConstants.SOF7_MARKER, JpegConstants.SOF9_MARKER, JpegConstants.SOF10_MARKER, JpegConstants.SOF11_MARKER,
JpegConstants.SOF13_MARKER, JpegConstants.SOF14_MARKER, JpegConstants.SOF15_MARKER, };
final JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
// return false to exit before reading image data.
@Override
public boolean beginSos() {
return false;
}
// return false to exit traversal.
@Override
public boolean visitSegment(final int marker, final byte[] markerBytes, final int markerLength, final byte[] markerLengthBytes,
final byte[] segmentData) throws ImagingException, IOException {
if (marker == JpegConstants.EOI_MARKER) {
return false;
}
// Debug.debug("visitSegment marker", marker);
// // Debug.debug("visitSegment keepMarker(marker, markers)",
// keepMarker(marker, markers));
// Debug.debug("visitSegment keepMarker(marker, markers)",
// keepMarker(marker, markers));
if (!keepMarker(marker, markers)) {
return true;
}
switch (marker) {
case JpegConstants.JPEG_APP13_MARKER:
// Debug.debug("app 13 segment data", segmentData.length);
result.add(new App13Segment(marker, segmentData));
break;
case JpegConstants.JPEG_APP14_MARKER:
result.add(new App14Segment(marker, segmentData));
break;
case JpegConstants.JPEG_APP2_MARKER:
result.add(new App2Segment(marker, segmentData));
break;
case JpegConstants.JFIF_MARKER:
result.add(new JfifSegment(marker, segmentData));
break;
default:
if (Arrays.binarySearch(sofnSegments, marker) >= 0) {
result.add(new SofnSegment(marker, segmentData));
} else if (marker == JpegConstants.DQT_MARKER) {
result.add(new DqtSegment(marker, segmentData));
} else if (marker >= JpegConstants.JPEG_APP1_MARKER && marker <= JpegConstants.JPEG_APP15_MARKER) {
result.add(new UnknownSegment(marker, segmentData));
} else if (marker == JpegConstants.COM_MARKER) {
result.add(new ComSegment(marker, segmentData));
}
break;
}
return !returnAfterFirst;
}
@Override
public void visitSos(final int marker, final byte[] markerBytes, final byte[] imageData) {
// don't need image data
}
};
new JpegUtils().traverseJfif(byteSource, visitor);
return result;
}
}