
com.davidjohnburrowes.format.jpeg.JpegData Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jpegfile Show documentation
Show all versions of jpegfile Show documentation
A java library to take apart and assemble jpeg files
/*
* Copyright 2014,2017 柏大衛
*
* Licensed 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 com.davidjohnburrowes.format.jpeg;
import com.davidjohnburrowes.format.jpeg.data.DataItem;
import com.davidjohnburrowes.format.jpeg.data.EntropyData;
import com.davidjohnburrowes.format.jpeg.data.ExtraFf;
import com.davidjohnburrowes.format.jpeg.data.Marker;
import com.davidjohnburrowes.format.jpeg.marker.AppNSegment;
import com.davidjohnburrowes.format.jpeg.marker.ComSegment;
import com.davidjohnburrowes.format.jpeg.marker.DacSegment;
import com.davidjohnburrowes.format.jpeg.marker.DhpSegment;
import com.davidjohnburrowes.format.jpeg.marker.DhtSegment;
import com.davidjohnburrowes.format.jpeg.marker.DnlSegment;
import com.davidjohnburrowes.format.jpeg.marker.DqtSegment;
import com.davidjohnburrowes.format.jpeg.marker.DriSegment;
import com.davidjohnburrowes.format.jpeg.marker.EoiMarker;
import com.davidjohnburrowes.format.jpeg.marker.ExpSegment;
import com.davidjohnburrowes.format.jpeg.marker.JfifSegment;
import com.davidjohnburrowes.format.jpeg.marker.JfxxSegment;
import com.davidjohnburrowes.format.jpeg.marker.JpgNSegment;
import com.davidjohnburrowes.format.jpeg.marker.JpgSegment;
import com.davidjohnburrowes.format.jpeg.marker.ResNSegment;
import com.davidjohnburrowes.format.jpeg.marker.RstMMarker;
import com.davidjohnburrowes.format.jpeg.marker.SofSegment;
import com.davidjohnburrowes.format.jpeg.marker.SoiMarker;
import com.davidjohnburrowes.format.jpeg.marker.SosSegment;
import com.davidjohnburrowes.format.jpeg.marker.TemMarker;
import com.davidjohnburrowes.format.jpeg.support.DataMode;
import com.davidjohnburrowes.format.jpeg.support.FrameMode;
import com.davidjohnburrowes.format.jpeg.support.InvalidJpegFormat;
import com.davidjohnburrowes.format.jpeg.support.MarkerId;
import com.davidjohnburrowes.format.jpeg.support.MarkerIdRange;
import com.davidjohnburrowes.format.jpeg.support.MarkerIdSet;
import com.davidjohnburrowes.format.jpeg.validate.NonHierarchicalValidator;
import com.davidjohnburrowes.format.jpeg.validate.Validator;
import com.davidjohnburrowes.util.Util;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* This class represents an entire Jpeg file. It can read and write a jpeg file
* from disk or from an input stream, as well have a set of JPEG markers and
* segments "hand added".
*
* Note that this has a special relationship with the FrameMode and HierarchicalMode
* properties. If there are no DHP or SOF segments in the JPEG file, then the
* specified FrameMode and HierarchicalMode is applied to all markers. However,
* if there are DHP and FrameModes, then their own properties will affect the
* respective modes of the markers in the file. Specifically, if DHP is present,
* all markers will have HierarchicalMode set to true. All markers after one
* frame (or the start of the file) and the next SOF segment as well as markers
* within the frame defined by that SOF segment get the FrameMode of that segment.
*/
public class JpegData extends DataItem implements Iterable {
/**
* The list of DataItems this is managing
*/
private List dataItems = new ArrayList();
/**
* The set of marker classes that this will use when read()'ing
*/
private List> markerTypes =
new ArrayList>();
/**
* The validator that this will use when reading
*/
private Validator validator = new NonHierarchicalValidator();
/**
* Create a JpegData instance, with the set of all marker types
* defined in the JPEG and JFIF standards.
*/
public JpegData() {
markerTypes.add(SoiMarker.class);
markerTypes.add(EoiMarker.class);
markerTypes.add(SofSegment.class);
markerTypes.add(SosSegment.class);
markerTypes.add(RstMMarker.class);
markerTypes.add(ComSegment.class);
markerTypes.add(JfifSegment.class);
markerTypes.add(JfxxSegment.class);
markerTypes.add(DqtSegment.class);
markerTypes.add(DhtSegment.class);
markerTypes.add(DacSegment.class);
markerTypes.add(DnlSegment.class);
markerTypes.add(DriSegment.class);
markerTypes.add(DhpSegment.class);
markerTypes.add(ExpSegment.class);
markerTypes.add(TemMarker.class);
markerTypes.add(AppNSegment.class);
markerTypes.add(JpgNSegment.class);
markerTypes.add(JpgSegment.class);
markerTypes.add(ResNSegment.class);
}
/**
* Replace the set of marker types that this instance will use
* when parsing a Jpeg file. This is mainly useful if you have your own
* types that you want this to use. Note that the read() routines will
* try to find a marker type based on the order of this list. Thus, if you
* have two marker types that both could be used (e.g. JfifSegment,
* JfxxSegment and AppNSegment all could match a 0xE0 marker, so in the
* default list they are provided in that order, so Jfif and Jfxx will be
* matched before the more generic AppNSegment)
*
* @param markerTypes The list of marker types to use.
*/
public void setMarkerTypes(List> markerTypes) {
this.markerTypes = markerTypes;
}
/*
* @return the set of marker types this is using
*/
public List> getMarkerTypes() {
return markerTypes;
}
/**
* @param validator The validator to use with this this JpegData instance
*/
public void setValidator(Validator validator) {
this.validator = validator;
}
/**
* @return The validator being used by this JpegData instance
*/
public Validator getValidator() {
return this.validator;
}
/*
* @return the number of segments.
*/
public int getItemCount() {
return dataItems.size();
}
/**
* @param index The index to retrieve an item from
* @return the DataItem at the specified index
*/
public DataItem getItem(int index) {
return dataItems.get(index);
}
/**
* @param item The Dataitem to add to the end of the list of data items
*/
public void addItem(DataItem item) {
this.insertItem(dataItems.size(), item);
}
/**
* Inserts the specified DataItem into this. note that the modes of the
* DataItem will be updated
* @param index The index to add the DataItem at
* @param item The DataItem to be added
*/
public void insertItem(int index, DataItem item) {
dataItems.add(index, item);
item.setDataMode(getDataMode());
setModes();
}
/**
* @param index The index of the DataItem to remove
* @return the removed DataItem
*/
public DataItem deleteItem(int index) {
DataItem item = dataItems.remove(index);
setModes();
return item;
}
/**
* @return An iterator that will iterate over all the segments in the file
*/
@Override
public Iterator iterator() {
return dataItems.listIterator();
}
/**
* {@inheritDoc}
*/
@Override
public int getSizeOnDisk() {
int size = super.getSizeOnDisk();
for (DataItem item : this) {
size += item.getSizeOnDisk();
}
return size;
}
/**
* {@inheritDoc}
*
* Note: In STRICT mode, if a marker segment has a problem, this will not
* read it in. After reading all marker segments in, a syntax check across
* segments will be run, and this may throw an InvalidJpegFormat exception,
* leaving all segments in this instance.
*/
@Override
public void read(RandomAccessFile file) throws IOException {
if (file == null) {
throw new IllegalArgumentException("Input file may not be null");
}
int extraFFCount = 0;
while (file.getFilePointer() < file.length()) {
DataItem result = null;
long pos = file.getFilePointer();
int aByte = file.readUnsignedByte();
if (aByte != 0xFF) {
file.seek(pos);
result = new EntropyData();
result.read(file);
} else {
int markerIdentifier;
try {
markerIdentifier = file.readUnsignedByte();
} catch (IOException e) {
if (extraFFCount != 0) {
ExtraFf ff = new ExtraFf();
ff.setFfCount(extraFFCount + 1);
addItem(ff);
}
break;
}
switch (markerIdentifier) {
case 0x00:
file.seek(pos);
result = new EntropyData();
result.read(file);
break;
case 0xFF:
extraFFCount ++;
pos ++;
file.seek(pos);
break;
default:
List types = findCandidateSegmentTypes(markerIdentifier);
Exception last = null;
for (Marker segment : types) {
try {
segment.setDataMode(getDataMode());
segment.read(file);
result = segment;
break;
} catch (Exception e) {
last = e;
file.seek(pos + 2); // rewind to just after the 0xFF## markerid
}
}
if (result == null) {
throw new InvalidJpegFormat("Could not handle segment", last);
}
break;
}
}
if (result != null) {
if (extraFFCount != 0) {
ExtraFf ff = new ExtraFf();
ff.setFfCount(extraFFCount);
addItem(ff);
extraFFCount = 0;
}
addItem(result);
}
}
setModes();
if (getDataMode() == DataMode.STRICT && !validate().isEmpty()) {
throw new InvalidJpegFormat("The JPEG file is invalid.", validate().get(0));
}
}
/**
* {@inheritDoc}
*
* Note: In STRICT mode, if a marker segment has a problem, this will not
* read it in. After reading all marker segments in, a syntax check across
* segments will be run, and this may throw an InvalidJpegFormat exception,
* leaving all segments in this instance.
*/
@Override
public void read(InputStream stream) throws IOException {
if (stream == null) {
throw new IllegalArgumentException("Input stream may not be null");
}
DataInputStream diStream = Util.wrapAsDataInput(stream);
int extraFFCount = 0;
while (true) {
DataItem result = null;
int aByte;
diStream.mark(2);
try {
aByte = diStream.readUnsignedByte();
} catch (EOFException e) {
// Assume we're done now with the file.
break;
}
if (aByte != 0xFF) {
diStream.reset();
result = new EntropyData();
result.read(stream);
} else {
int markerIdentifier;
try {
markerIdentifier = diStream.readUnsignedByte();
} catch (EOFException e) {
if (extraFFCount != 0) {
ExtraFf ff = new ExtraFf();
ff.setFfCount(extraFFCount + 1);
addItem(ff);
}
break;
}
switch (markerIdentifier) {
case 0x00:
diStream.reset();
result = new EntropyData();
result.read(stream);
break;
case 0xFF:
extraFFCount ++;
diStream.reset();
diStream.readUnsignedByte(); // just read this. if we get another exception, we're very unahppy, so let it pass through
break;
default:
Exception last = null;
List types = findCandidateSegmentTypes(markerIdentifier);
diStream.mark(65536);
for (Marker segment : types) {
try {
segment.setDataMode(getDataMode());
segment.read(diStream);
result = segment;
break;
} catch (Exception e) {
last = e;
diStream.reset();
}
}
if (result == null) {
throw new InvalidJpegFormat("Could not handle segment", last);
}
break;
}
}
if (result != null) {
if (extraFFCount != 0) {
ExtraFf ff = new ExtraFf();
ff.setFfCount(extraFFCount);
addItem(ff);
extraFFCount = 0;
}
addItem(result);
}
}
setModes();
if (getDataMode() == DataMode.STRICT && !validate().isEmpty()) {
throw new InvalidJpegFormat("The JPEG file is invalid.", validate().get(0));
}
}
/**
* {@inheritDoc}
*/
@Override
public void write(OutputStream stream) throws IOException {
for (DataItem item : this) {
item.write(stream);
}
}
/**
* {@inheritDoc}
*/
@Override
public List validate() {
List results = super.validate();
for (DataItem item : dataItems) {
results.addAll(item.validate());
}
results.addAll(validator.validate(dataItems));
return results;
}
/**
* {@inheritDoc}
*/
@Override
public void clearPassthrough() {
super.clearPassthrough();
for (int index = getItemCount()-1; index >= 0; index--) {
DataItem item = getItem(index);
if (item instanceof ExtraFf) {
deleteItem(index);
} else {
item.clearPassthrough();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof JpegData)) {
return false;
}
JpegData jOther = (JpegData) other;
if (getItemCount() != jOther.getItemCount()) {
return false;
}
Iterator thisIterator = this.iterator();
Iterator otherIterator = jOther.iterator();
while (thisIterator.hasNext()) {
if (!thisIterator.next().equals(otherIterator.next())) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 59 * hash + (this.dataItems != null ? this.dataItems.hashCode() : 0);
return hash;
}
/**
* {@inheritDoc}
*/
@Override
protected void changeChildrenModes() {
super.changeChildrenModes();
for (DataItem item : this) {
item.setDataMode(getDataMode());
}
setModes();
}
/**
* {@inheritDoc}
*/
@Override
protected void checkModeChange() {
setModes();
}
/**
* Update the Frame and Hierarchical modes on all items. Start off using the
* modes specified on this JpegData instance. however, if we find a DHP
* segment, set all hierarchical modes to true. If we find a SOF segment,
* apply its FrameMode to all marker segments governed by it (everything
* after the last frame (or start of file) to the end of this frame (or end
* of file)
*/
protected void setModes() {
int lastEntropyindex = -1;
FrameMode currentFrameMode = getFrameMode();
boolean currentHierMode = getHierarchicalMode();
for (int currentIndex = 0; currentIndex < getItemCount(); currentIndex++) {
DataItem item = getItem(currentIndex);
if (item instanceof Marker) {
int markerId = ((Marker)item).getMarkerId();
// Found DHP. Revise the hiearchical mode specified for all items before this
if (markerId == DhpSegment.MARKERID) {
currentHierMode = true;
for (int subIndex = 0; subIndex <= currentIndex; subIndex++) {
getItem(subIndex).setHierarchicalMode(currentHierMode);
}
}
// Found an SOF. Update everything before the frame
if ((markerId >= SofSegment.FIRST1_MARKERID && markerId <= SofSegment.LAST1_MARKERID) ||
(markerId >= SofSegment.FIRST2_MARKERID && markerId <= SofSegment.LAST2_MARKERID) ||
(markerId >= SofSegment.FIRST3_MARKERID && markerId <= SofSegment.LAST3_MARKERID) ||
(markerId >= SofSegment.FIRST4_MARKERID && markerId <= SofSegment.LAST4_MARKERID)) {
currentFrameMode = FrameMode.fromValue(markerId);
for (int subIndex = lastEntropyindex+1; subIndex <= currentIndex; subIndex++) {
getItem(subIndex).setFrameMode(currentFrameMode);
}
}
}
if (item instanceof EntropyData) {
lastEntropyindex = currentIndex;
}
item.setHierarchicalMode(currentHierMode);
item.setFrameMode(currentFrameMode);
}
}
/**
* Examines the set of markerTypes this JpegData can make use of, and compares
* the provided markerId against them, returning the subset of marker types
* that may be able to parse that kind of data. The items are returned in
* the same order that they are specified in the markerTypes array.
*/
private List findCandidateSegmentTypes(int markerId) {
List matches = new ArrayList();
for (Class extends Marker> segmentType : markerTypes) {
MarkerId simpleAntn = segmentType.getAnnotation(MarkerId.class);
if (simpleAntn != null && simpleAntn.value() == markerId) {
try {
Constructor constructor = segmentType.getDeclaredConstructor();
matches.add((Marker) constructor.newInstance());
} catch (Exception e) {
throw new InvalidJpegFormat("No constructor found for " +
segmentType.getName());
}
} else {
MarkerIdRange rangeAntn = segmentType.getAnnotation(MarkerIdRange.class);
MarkerIdSet setAntn = segmentType.getAnnotation(MarkerIdSet.class);
if (rangeAntn != null && inRange(rangeAntn, markerId) ||
setAntn != null && inSet(setAntn, markerId)) {
try {
Constructor intConstructor = segmentType.getDeclaredConstructor(int.class);
matches.add((Marker) intConstructor.newInstance(markerId));
} catch (Exception e) {
throw new InvalidJpegFormat("No constructor(int) found for " +
segmentType.getName());
}
}
}
}
return matches;
}
/**
* Returns true if the specified markerID is in the range specifiedby the
* MarkerIdRange annotation
*/
private boolean inRange(MarkerIdRange annotation, int markerId) {
return markerId >= annotation.first() && markerId <= annotation.last();
}
/**
* Returns true if the specified markerID value is among the values defined
* in the MarkerIdSet annotation.
*/
private boolean inSet(MarkerIdSet annotation, int markerId) {
int[] values = annotation.value();
for (int value : values) {
if (value == markerId) {
return true;
}
}
return false;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy