Skip to content

Commit

Permalink
Merge branch 'drewnoakes:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
nosnhojbob authored Feb 7, 2024
2 parents 7239f55 + b19ac06 commit c64e240
Show file tree
Hide file tree
Showing 26 changed files with 317 additions and 206 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Java CI with Maven

on:
push:
branches: [ "master" ]
branches: [ "main" ]
pull_request:
branches: [ "master" ]
branches: [ "main" ]

jobs:
build:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ It will process files of type:
* PNG
* BMP
* GIF
* HEIF (HEIC & AVIF)
* ICO
* PCX
* QuickTime
Expand Down
1 change: 1 addition & 0 deletions Source/com/drew/imaging/FileType.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public enum FileType
QuickTime("MOV", "QuickTime Movie", "video/quicktime", "mov", "qt"),
Mp4("MP4", "MPEG-4 Part 14", "video/mp4", "mp4", "m4a", "m4p", "m4b", "m4r", "m4v"),
Heif("HEIF", "High Efficiency Image File Format", "image/heif", "heif", "heic"),
Avif("AVIF", "AV1 Image File Format", "image/avif", "avif"),
Eps("EPS", "Encapsulated PostScript", "application/postscript", "eps", "epsf", "epsi"),
Mp3("MP3", "MPEG Audio Layer III", "audio/mpeg", "mp3"),

Expand Down
1 change: 1 addition & 0 deletions Source/com/drew/imaging/ImageMetadataReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ public static Metadata readMetadata(@NotNull final InputStream inputStream, fina
case Eps:
return EpsMetadataReader.readMetadata(inputStream);
case Heif:
case Avif:
return HeifMetadataReader.readMetadata(inputStream);
case Crx:
return Mp4MetadataReader.readMetadata(inputStream);
Expand Down
16 changes: 8 additions & 8 deletions Source/com/drew/imaging/png/PngMetadataReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private static void processChunk(@NotNull Metadata metadata, @NotNull PngChunk c
SequentialReader reader = new SequentialByteArrayReader(bytes);

// Profile Name is 1-79 bytes, followed by the 1 byte null character
byte[] profileNameBytes = reader.getNullTerminatedBytes(79 + 1);
byte[] profileNameBytes = reader.getNullTerminatedBytes(79 + 1, false);
PngDirectory directory = new PngDirectory(PngChunkType.iCCP);
directory.setStringValue(PngDirectory.TAG_ICC_PROFILE_NAME, new StringValue(profileNameBytes, _latin1Encoding));
byte compressionMethod = reader.getInt8();
Expand Down Expand Up @@ -197,13 +197,13 @@ private static void processChunk(@NotNull Metadata metadata, @NotNull PngChunk c
SequentialReader reader = new SequentialByteArrayReader(bytes);

// Keyword is 1-79 bytes, followed by the 1 byte null character
StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _latin1Encoding);
StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _latin1Encoding, false);
String keyword = keywordsv.toString();

// bytes left for text is:
// total bytes length - (Keyword length + null byte)
int bytesLeft = bytes.length - (keywordsv.getBytes().length + 1);
StringValue value = reader.getNullTerminatedStringValue(bytesLeft, _latin1Encoding);
StringValue value = reader.getNullTerminatedStringValue(bytesLeft, _latin1Encoding, false);
List<KeyValuePair> textPairs = new ArrayList<KeyValuePair>();
textPairs.add(new KeyValuePair(keyword, value));
PngDirectory directory = new PngDirectory(PngChunkType.tEXt);
Expand All @@ -213,7 +213,7 @@ private static void processChunk(@NotNull Metadata metadata, @NotNull PngChunk c
SequentialReader reader = new SequentialByteArrayReader(bytes);

// Keyword is 1-79 bytes, followed by the 1 byte null character
StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _latin1Encoding);
StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _latin1Encoding, false);
String keyword = keywordsv.toString();
byte compressionMethod = reader.getInt8();

Expand Down Expand Up @@ -250,20 +250,20 @@ private static void processChunk(@NotNull Metadata metadata, @NotNull PngChunk c
SequentialReader reader = new SequentialByteArrayReader(bytes);

// Keyword is 1-79 bytes, followed by the 1 byte null character
StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _utf8Encoding);
StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _utf8Encoding, false);
String keyword = keywordsv.toString();
byte compressionFlag = reader.getInt8();
byte compressionMethod = reader.getInt8();
// TODO we currently ignore languageTagBytes and translatedKeywordBytes
byte[] languageTagBytes = reader.getNullTerminatedBytes(bytes.length);
byte[] translatedKeywordBytes = reader.getNullTerminatedBytes(bytes.length);
byte[] languageTagBytes = reader.getNullTerminatedBytes(bytes.length, false);
byte[] translatedKeywordBytes = reader.getNullTerminatedBytes(bytes.length, false);

// bytes left for compressed text is:
// total bytes length - (Keyword length + null byte + comp flag byte + comp method byte + lang length + null byte + translated length + null byte)
int bytesLeft = bytes.length - (keywordsv.getBytes().length + 1 + 1 + 1 + languageTagBytes.length + 1 + translatedKeywordBytes.length + 1);
byte[] textBytes = null;
if (compressionFlag == 0) {
textBytes = reader.getNullTerminatedBytes(bytesLeft);
textBytes = reader.getNullTerminatedBytes(bytesLeft, false);
} else if (compressionFlag == 1) {
if (compressionMethod == 0) {
try {
Expand Down
3 changes: 3 additions & 0 deletions Source/com/drew/imaging/quicktime/QuickTimeTypeChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ public class QuickTimeTypeChecker implements TypeChecker
_ftypMap.put("hevc", FileType.Heif);
_ftypMap.put("hevx", FileType.Heif);

// AVIF
_ftypMap.put("avif", FileType.Avif);

// CRX
_ftypMap.put("crx ", FileType.Crx);
}
Expand Down
47 changes: 30 additions & 17 deletions Source/com/drew/lang/SequentialReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -331,52 +331,65 @@ public StringValue getStringValue(int bytesRequested, @Nullable Charset charset)
/**
* Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
*
* @param maxLengthBytes The maximum number of bytes to read. If a zero-byte is not reached within this limit,
* reading will stop and the string will be truncated to this length.
* @param maxLengthBytes The maximum number of bytes to read. If a zero-byte is not reached within this limit,
* reading will stop and the string will be truncated to this length.
* @param moveToMaxLength When true, the reader progresses maxLengthBytes on every call. When false, the reader
* stops when the first null is encountered (or the maximum is reached).
* @return The read string.
* @throws IOException The buffer does not contain enough bytes to satisfy this request.
*/
@NotNull
public String getNullTerminatedString(int maxLengthBytes, Charset charset) throws IOException
public String getNullTerminatedString(int maxLengthBytes, Charset charset, boolean moveToMaxLength) throws IOException
{
return getNullTerminatedStringValue(maxLengthBytes, charset).toString();
return getNullTerminatedStringValue(maxLengthBytes, charset, moveToMaxLength).toString();
}

/**
* Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
*
* @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit,
* reading will stop and the string will be truncated to this length.
* @param charset The <code>Charset</code> to register with the returned <code>StringValue</code>, or <code>null</code> if the encoding
* is unknown
* @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit,
* reading will stop and the string will be truncated to this length.
* @param charset The <code>Charset</code> to register with the returned <code>StringValue</code>, or <code>null</code> if the encoding
* is unknown
* @param moveToMaxLength When true, the reader progresses maxLengthBytes on every call. When false, the reader
* stops when the first null is encountered (or the maximum is reached).
* @return The read string.
* @throws IOException The buffer does not contain enough bytes to satisfy this request.
*/
@NotNull
public StringValue getNullTerminatedStringValue(int maxLengthBytes, Charset charset) throws IOException
public StringValue getNullTerminatedStringValue(int maxLengthBytes, Charset charset, boolean moveToMaxLength) throws IOException
{
byte[] bytes = getNullTerminatedBytes(maxLengthBytes);
byte[] bytes = getNullTerminatedBytes(maxLengthBytes, moveToMaxLength);

return new StringValue(bytes, charset);
}

/**
* Returns the sequence of bytes punctuated by a <code>\0</code> value.
*
* @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit,
* the returned array will be <code>maxLengthBytes</code> long.
* @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit,
* the returned array will be <code>maxLengthBytes</code> long.
* @param moveToMaxLength When true, the reader progresses maxLengthBytes on every call. When false, the reader
* stops when the first null is encountered (or the maximum is reached).
* @return The read byte array, excluding the null terminator.
* @throws IOException The buffer does not contain enough bytes to satisfy this request.
*/
@NotNull
public byte[] getNullTerminatedBytes(int maxLengthBytes) throws IOException
public byte[] getNullTerminatedBytes(int maxLengthBytes, boolean moveToMaxLength) throws IOException
{
byte[] buffer = new byte[maxLengthBytes];

// Count the number of non-null bytes
int length = 0;
while (length < buffer.length && (buffer[length] = getByte()) != 0)
length++;
byte[] buffer;

if (moveToMaxLength) {
buffer = getBytes(maxLengthBytes);
while (length < buffer.length && buffer[length] != 0)
length++;
} else {
buffer = new byte[maxLengthBytes];
while (length < buffer.length && (buffer[length] = getByte()) != 0)
length++;
}

if (length == maxLengthBytes)
return buffer;
Expand Down
38 changes: 36 additions & 2 deletions Source/com/drew/metadata/Directory.java
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,15 @@ public Long getLongObject(int tagType)
try {
return Long.parseLong(o.toString());
} catch (NumberFormatException nfe) {
return null;
// convert the char array to an int
String s = o.toString();
byte[] bytes = s.getBytes();
long val = 0;
for (byte aByte : bytes) {
val = val << 8;
val += (aByte & 0xff);
}
return val;
}
} else if (o instanceof Rational[]) {
Rational[] rationals = (Rational[])o;
Expand Down Expand Up @@ -887,6 +895,12 @@ public java.util.Date getDate(int tagType, @Nullable String subsecond, @Nullable

String dateString = o.toString();

/*
* This is a common NULL value known for cameras like Olympus C750UZ.
*/
if (dateString.equals("0000:00:00 00:00:00"))
return null;

// if the date string has subsecond information, it supersedes the subsecond parameter
Pattern subsecondPattern = Pattern.compile("(\\d\\d:\\d\\d:\\d\\d)(\\.\\d+)");
Matcher subsecondMatcher = subsecondPattern.matcher(dateString);
Expand All @@ -905,11 +919,31 @@ public java.util.Date getDate(int tagType, @Nullable String subsecond, @Nullable

for (String datePattern : datePatterns) {
try {

DateFormat parser = new SimpleDateFormat(datePattern);

/*
* Some older digital cameras, such as the Olympus C750UZ, may not have recorded a proper
* exif:DateTimeOriginal value. Instead of leaving this field empty, they used
* 0000:00:00 00:00:00 as a placeholder.
*
* "0000:00:00 00:00:00" will result in "Sun Nov 30 00:00:00 GMT 2",
* which is not what a user would expect.
*
* Any illegal formats should result in an exception and not be parsed.
*
* It's best to turn lenient mode off.
*/
parser.setLenient(false);

/*
* If the metadata has set a time zone we use that, and otherwise
* we assume that computer and image belong to the same geographical area.
*/
if (timeZone != null)
parser.setTimeZone(timeZone);
else
parser.setTimeZone(TimeZone.getTimeZone("GMT")); // don't interpret zone time
parser.setTimeZone(TimeZone.getDefault());

date = parser.parse(dateString);
break;
Expand Down
9 changes: 4 additions & 5 deletions Source/com/drew/metadata/bmp/BmpReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import com.drew.lang.Charsets;
import com.drew.lang.SequentialReader;
import com.drew.lang.annotations.NotNull;
import com.drew.metadata.Directory;
import com.drew.metadata.ErrorDirectory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
Expand Down Expand Up @@ -121,7 +120,7 @@ protected void readFileHeader(@NotNull final SequentialReader reader, final @Not
return;
}

Directory directory = null;
BmpHeaderDirectory directory = null;
try {
switch (magicNumber) {
case OS2_BITMAP_ARRAY:
Expand Down Expand Up @@ -153,11 +152,11 @@ protected void readFileHeader(@NotNull final SequentialReader reader, final @Not
directory.setInt(BmpHeaderDirectory.TAG_BITMAP_TYPE, magicNumber);
// skip past the rest of the file header
reader.skip(4 + 2 + 2 + 4);
readBitmapHeader(reader, (BmpHeaderDirectory) directory, metadata);
readBitmapHeader(reader, directory, metadata);
break;
default:
metadata.addDirectory(new ErrorDirectory("Invalid BMP magic number 0x" + Integer.toHexString(magicNumber)));
return;
break;
}
} catch (IOException e) {
if (directory == null) {
Expand Down Expand Up @@ -372,7 +371,7 @@ protected void readBitmapHeader(@NotNull final SequentialReader reader, final @N
}
reader.skip(headerOffset + profileOffset - reader.getPosition());
if (csType == ColorSpaceType.PROFILE_LINKED.getValue()) {
directory.setString(BmpHeaderDirectory.TAG_LINKED_PROFILE, reader.getNullTerminatedString(profileSize, Charsets.WINDOWS_1252));
directory.setString(BmpHeaderDirectory.TAG_LINKED_PROFILE, reader.getNullTerminatedString(profileSize, Charsets.WINDOWS_1252, true));
} else {
ByteArrayReader randomAccessReader = new ByteArrayReader(reader.getBytes(profileSize));
new IccReader().extract(randomAccessReader, metadata, directory);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ public static NikonPictureControl1Directory read(byte[] bytes) throws IOExceptio

NikonPictureControl1Directory directory = new NikonPictureControl1Directory();

directory.setObject(TAG_PICTURE_CONTROL_VERSION, reader.getStringValue(4, Charsets.UTF_8));
directory.setObject(TAG_PICTURE_CONTROL_NAME, reader.getStringValue(20, Charsets.UTF_8));
directory.setObject(TAG_PICTURE_CONTROL_BASE, reader.getStringValue(20, Charsets.UTF_8));
directory.setObject(TAG_PICTURE_CONTROL_VERSION, reader.getNullTerminatedStringValue(4, Charsets.UTF_8, true));
directory.setObject(TAG_PICTURE_CONTROL_NAME, reader.getNullTerminatedStringValue(20, Charsets.UTF_8, true));
directory.setObject(TAG_PICTURE_CONTROL_BASE, reader.getNullTerminatedStringValue(20, Charsets.UTF_8, true));
reader.skip(4);
directory.setObject(TAG_PICTURE_CONTROL_ADJUST, reader.getUInt8());
directory.setObject(TAG_PICTURE_CONTROL_QUICK_ADJUST, reader.getUInt8());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ public static NikonPictureControl2Directory read(byte[] bytes) throws IOExceptio

NikonPictureControl2Directory directory = new NikonPictureControl2Directory();

directory.setString(TAG_PICTURE_CONTROL_VERSION, reader.getStringValue(4, Charsets.UTF_8).toString());
directory.setString(TAG_PICTURE_CONTROL_NAME, reader.getStringValue(20, Charsets.UTF_8).toString());
directory.setString(TAG_PICTURE_CONTROL_BASE, reader.getStringValue(20, Charsets.UTF_8).toString());
directory.setStringValue(TAG_PICTURE_CONTROL_VERSION, reader.getNullTerminatedStringValue(4, Charsets.UTF_8, true));
directory.setStringValue(TAG_PICTURE_CONTROL_NAME, reader.getNullTerminatedStringValue(20, Charsets.UTF_8, true));
directory.setStringValue(TAG_PICTURE_CONTROL_BASE, reader.getNullTerminatedStringValue(20, Charsets.UTF_8, true));

reader.skip(4);
directory.setObject(TAG_PICTURE_CONTROL_ADJUST, reader.getByte());
Expand Down
2 changes: 1 addition & 1 deletion Source/com/drew/metadata/heif/boxes/HandlerBox.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public HandlerBox(SequentialReader reader, Box box) throws IOException
reader.skip(4); // Pre-defined
handlerType = reader.getString(4);
reader.skip(12); // Reserved
name = reader.getNullTerminatedString((int)box.size - 32, Charsets.UTF_8);
name = reader.getNullTerminatedString((int)box.size - 32, Charsets.UTF_8, false);
}

public String getHandlerType()
Expand Down
Loading

0 comments on commit c64e240

Please sign in to comment.