Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Source/com/drew/imaging/ImageMetadataReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import com.drew.imaging.avi.AviMetadataReader;
import com.drew.imaging.bmp.BmpMetadataReader;
import com.drew.imaging.cr3.Cr3MetadataReader;
import com.drew.imaging.eps.EpsMetadataReader;
import com.drew.imaging.gif.GifMetadataReader;
import com.drew.imaging.heif.HeifMetadataReader;
Expand Down Expand Up @@ -194,6 +195,8 @@ public static Metadata readMetadata(@NotNull final InputStream inputStream, fina
return AviMetadataReader.readMetadata(inputStream);
case Wav:
return WavMetadataReader.readMetadata(inputStream);
case Crx:
return Cr3MetadataReader.readMetadata(inputStream);
case QuickTime:
return QuickTimeMetadataReader.readMetadata(inputStream);
case Mp4:
Expand Down
202 changes: 202 additions & 0 deletions Source/com/drew/imaging/cr3/Cr3CanonUuidHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Copyright 2002-2019 Drew Noakes and contributors
*
* 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.
*
* More information about this project is available at:
*
* https://drewnoakes.com/code/exif/
* https://github.com/drewnoakes/metadata-extractor
*/
package com.drew.imaging.cr3;

import com.drew.imaging.isobmff.IsoBox;
import com.drew.imaging.isobmff.IsoBoxVisitor;
import com.drew.imaging.tiff.TiffProcessingException;
import com.drew.imaging.tiff.TiffReader;
import com.drew.lang.ByteArrayReader;
import com.drew.lang.SequentialByteArrayReader;
import com.drew.lang.annotations.NotNull;
import com.drew.lang.annotations.Nullable;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.cr3.Cr3BoxTypes;
import com.drew.metadata.cr3.Cr3Directory;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.exif.ExifTiffHandler;
import com.drew.metadata.exif.GpsDirectory;
import com.drew.metadata.exif.makernotes.CanonMakernoteDirectory;

import java.io.IOException;

/**
* Visits the sub-boxes of the Canon main metadata UUID
* ({@code 85c0b687-820f-11e0-8111-f4ce462b6a48}) inside the {@code moov} container.
* <p>
* Decodes:
* <ul>
* <li>CMT1 — IFD0 data (Make, Model, DateTime, …) → {@link ExifIFD0Directory}</li>
* <li>CMT2 — Exif sub-IFD data (exposure, focal length, …) → {@link ExifSubIFDDirectory}</li>
* <li>CMT3 — Canon makernote data → {@link CanonMakernoteDirectory}</li>
* <li>CMT4 — GPS IFD data → {@link GpsDirectory}</li>
* <li>CNCV — Canon compressor version string → {@link Cr3Directory}</li>
* <li>THMB — Thumbnail dimensions → {@link Cr3Directory}</li>
* </ul>
* Each CMT blob is a self-contained TIFF byte stream (starts with {@code II}/{@code MM}
* byte-order mark + 0x002A magic) and is decoded via the existing {@link ExifTiffHandler}
* infrastructure to avoid duplicating any TIFF/IFD parsing logic.
*/
class Cr3CanonUuidHandler extends IsoBoxVisitor
{
private final Metadata metadata;
private final Cr3Directory directory;

Cr3CanonUuidHandler(@NotNull Metadata metadata, @NotNull Cr3Directory directory)
{
this.metadata = metadata;
this.directory = directory;
}

@Override
protected boolean shouldRecurse(@NotNull IsoBox box)
{
return false; // no nested containers inside Canon main UUID
}

@Override
@NotNull
protected IsoBoxVisitor processContainer(@NotNull IsoBox box)
{
return this;
}

@Override
protected boolean shouldVisit(@NotNull IsoBox box)
{
String t = box.type;
return Cr3BoxTypes.BOX_CANON_TIFF_IFD0.equals(t)
|| Cr3BoxTypes.BOX_CANON_TIFF_EXIF.equals(t)
|| Cr3BoxTypes.BOX_CANON_TIFF_MAKERNOTE.equals(t)
|| Cr3BoxTypes.BOX_CANON_TIFF_GPS.equals(t)
|| Cr3BoxTypes.BOX_CANON_COMPRESSOR_VERSION.equals(t)
|| Cr3BoxTypes.BOX_THUMBNAIL.equals(t);
}

@Override
protected void visit(@NotNull IsoBox box, @NotNull byte[] payload) throws IOException
{
String t = box.type;
if (Cr3BoxTypes.BOX_CANON_TIFF_IFD0.equals(t)) {
processTiff(payload, null /* standard ExifTiffHandler starts at IFD0 */);
} else if (Cr3BoxTypes.BOX_CANON_TIFF_EXIF.equals(t)) {
processTiff(payload, ExifSubIFDDirectory.class);
} else if (Cr3BoxTypes.BOX_CANON_TIFF_MAKERNOTE.equals(t)) {
processTiff(payload, CanonMakernoteDirectory.class);
} else if (Cr3BoxTypes.BOX_CANON_TIFF_GPS.equals(t)) {
processTiff(payload, GpsDirectory.class);
} else if (Cr3BoxTypes.BOX_CANON_COMPRESSOR_VERSION.equals(t)) {
processCompressorVersion(payload);
} else if (Cr3BoxTypes.BOX_THUMBNAIL.equals(t)) {
processThumbnail(payload);
}
}

@Override
public void addError(@NotNull String message)
{
directory.addError(message);
}

/**
* Decodes a CMT TIFF blob.
*
* @param payload raw TIFF bytes (starts with byte-order mark + 0x002A magic)
* @param rootClass directory class to push before processing, or {@code null} to use the
* default ExifTiffHandler behaviour (which pushes ExifIFD0Directory)
*/
private void processTiff(@NotNull byte[] payload,
@Nullable Class<? extends Directory> rootClass)
{
try {
ByteArrayReader reader = new ByteArrayReader(payload);
ExifTiffHandler handler = (rootClass == null)
? new ExifTiffHandler(metadata, null, 0)
: new FixedRootExifTiffHandler(metadata, rootClass);
new TiffReader().processTiff(reader, handler, 0);
} catch (TiffProcessingException e) {
directory.addError("CR3 TIFF processing failed: " + e.getMessage());
} catch (IOException e) {
directory.addError("CR3 TIFF I/O error: " + e.getMessage());
}
}

private void processCompressorVersion(@NotNull byte[] payload)
{
// CNCV is a 30-byte ASCII string (may be NUL-padded)
int len = payload.length;
while (len > 0 && payload[len - 1] == 0)
len--;
directory.setString(Cr3Directory.TAG_COMPRESSOR_VERSION, new String(payload, 0, len));
}

/**
* Parses THMB thumbnail header to extract dimensions.
* <p>
* THMB payload layout (big-endian):
* <pre>
* uint32 version/flags
* uint16 width
* uint16 height
* uint32 jpeg_size
* uint32 unknown
* byte[] jpeg_data
* </pre>
*/
private void processThumbnail(@NotNull byte[] payload)
{
try {
SequentialByteArrayReader reader = new SequentialByteArrayReader(payload);
reader.skip(4); // version/flags uint32
int width = reader.getUInt16();
int height = reader.getUInt16();
directory.setInt(Cr3Directory.TAG_THUMBNAIL_WIDTH, width);
directory.setInt(Cr3Directory.TAG_THUMBNAIL_HEIGHT, height);
} catch (IOException e) {
directory.addError("Error reading THMB dimensions: " + e.getMessage());
}
}

/**
* An {@link ExifTiffHandler} variant that starts IFD processing at a caller-specified
* directory class rather than the default {@link ExifIFD0Directory}.
* Used for CMT2 (ExifSubIFD), CMT3 (Canon makernote), and CMT4 (GPS).
*/
private static final class FixedRootExifTiffHandler extends ExifTiffHandler
{
private final Class<? extends Directory> _rootClass;

FixedRootExifTiffHandler(@NotNull Metadata metadata,
@NotNull Class<? extends Directory> rootClass)
{
super(metadata, null, 0);
_rootClass = rootClass;
}

@Override
public void setTiffMarker(int marker) throws TiffProcessingException
{
pushDirectory(_rootClass);
}
}
}
146 changes: 146 additions & 0 deletions Source/com/drew/imaging/cr3/Cr3Handler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright 2002-2019 Drew Noakes and contributors
*
* 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.
*
* More information about this project is available at:
*
* https://drewnoakes.com/code/exif/
* https://github.com/drewnoakes/metadata-extractor
*/
package com.drew.imaging.cr3;

import com.drew.imaging.isobmff.IsoBox;
import com.drew.imaging.isobmff.IsoBoxVisitor;
import com.drew.lang.SequentialByteArrayReader;
import com.drew.lang.annotations.NotNull;
import com.drew.metadata.Metadata;
import com.drew.metadata.cr3.Cr3BoxTypes;
import com.drew.metadata.cr3.Cr3ContainerTypes;
import com.drew.metadata.cr3.Cr3Directory;
import com.drew.metadata.xmp.XmpReader;

import java.io.IOException;
import java.util.ArrayList;

/**
* Root ISOBMFF visitor for Canon CR3 files.
* <p>
* Handles the top-level box sequence ({@code ftyp}, {@code moov}, {@code uuid}).
* Delegates Canon-metadata UUID content to {@link Cr3CanonUuidHandler} and
* preview UUID content to {@link Cr3PreviewUuidHandler}.
*/
class Cr3Handler extends IsoBoxVisitor
{
/**
* Canon main-metadata UUID: {@code 85c0b687-820f-11e0-8111-f4ce462b6a48}.
* Resides inside {@code moov}; contains CMT1–CMT4, THMB, CNCV, …
*/
static final byte[] UUID_CANON_MAIN = {
(byte)0x85, (byte)0xc0, (byte)0xb6, (byte)0x87,
(byte)0x82, (byte)0x0f, (byte)0x11, (byte)0xe0,
(byte)0x81, (byte)0x11, (byte)0xf4, (byte)0xce,
(byte)0x46, (byte)0x2b, (byte)0x6a, (byte)0x48
};

/**
* Canon preview UUID: {@code eaf42b5e-1c98-4b88-b9fb-b7dc406e4d16}.
* Top-level; contains the PRVW sub-box.
*/
static final byte[] UUID_CANON_PREVIEW = {
(byte)0xea, (byte)0xf4, (byte)0x2b, (byte)0x5e,
(byte)0x1c, (byte)0x98, (byte)0x4b, (byte)0x88,
(byte)0xb9, (byte)0xfb, (byte)0xb7, (byte)0xdc,
(byte)0x40, (byte)0x6e, (byte)0x4d, (byte)0x16
};

/**
* Canon XMP UUID: {@code be7acfcb-97a9-42e8-9c71-999491e3afac}.
* Top-level; payload is a raw XMP packet.
*/
static final byte[] UUID_CANON_XMP = {
(byte)0xbe, (byte)0x7a, (byte)0xcf, (byte)0xcb,
(byte)0x97, (byte)0xa9, (byte)0x42, (byte)0xe8,
(byte)0x9c, (byte)0x71, (byte)0x99, (byte)0x94,
(byte)0x91, (byte)0xe3, (byte)0xaf, (byte)0xac
};

final Metadata metadata;
final Cr3Directory directory;

Cr3Handler(@NotNull Metadata metadata, @NotNull Cr3Directory directory)
{
this.metadata = metadata;
this.directory = directory;
}

@Override
protected boolean shouldRecurse(@NotNull IsoBox box)
{
return Cr3ContainerTypes.BOX_MOVIE.equals(box.type)
|| box.usertypeMatches(UUID_CANON_MAIN);
}

@Override
@NotNull
protected IsoBoxVisitor processContainer(@NotNull IsoBox box)
{
if (box.usertypeMatches(UUID_CANON_MAIN))
return new Cr3CanonUuidHandler(metadata, directory);
return this;
}

@Override
protected boolean shouldVisit(@NotNull IsoBox box)
{
return Cr3BoxTypes.BOX_FILE_TYPE.equals(box.type)
|| box.usertypeMatches(UUID_CANON_XMP)
|| box.usertypeMatches(UUID_CANON_PREVIEW);
}

@Override
protected void visit(@NotNull IsoBox box, @NotNull byte[] payload) throws IOException
{
if (Cr3BoxTypes.BOX_FILE_TYPE.equals(box.type)) {
processFtyp(payload, box.size);
} else if (box.usertypeMatches(UUID_CANON_XMP)) {
new XmpReader().extract(payload, metadata);
} else if (box.usertypeMatches(UUID_CANON_PREVIEW)) {
new Cr3PreviewUuidHandler(directory).parsePayload(payload);
}
}

@Override
public void addError(@NotNull String message)
{
directory.addError(message);
}

private void processFtyp(byte[] payload, long boxSize) throws IOException
{
SequentialByteArrayReader reader = new SequentialByteArrayReader(payload);
String majorBrand = reader.getString(4);
long minorVersion = reader.getUInt32();

ArrayList<String> compatibleBrands = new ArrayList<String>();
// ftyp payload = major(4) + minor(4) + compatible brands in groups of 4
for (int i = 16; i < boxSize; i += 4) {
compatibleBrands.add(reader.getString(4));
}

directory.setString(Cr3Directory.TAG_MAJOR_BRAND, majorBrand);
directory.setLong(Cr3Directory.TAG_MINOR_VERSION, minorVersion);
directory.setStringArray(Cr3Directory.TAG_COMPATIBLE_BRANDS,
compatibleBrands.toArray(new String[0]));
}
}
Loading