From 77f34d2bc2a35a69330a4ffe4ed00b18b28430fd Mon Sep 17 00:00:00 2001 From: ashrafuzzamanpurno Date: Tue, 26 May 2026 15:12:18 +0600 Subject: [PATCH 1/3] Introduces a common ISO Base Media FileFormat container-walking infrastructure. Designed so other readers (MP4, QuickTime, HEIF) can migrate onto it; CR3 is the first consumer. --- Source/com/drew/imaging/isobmff/IsoBox.java | 86 +++++++++++++ .../drew/imaging/isobmff/IsoBoxVisitor.java | 67 ++++++++++ .../drew/imaging/isobmff/IsoBoxWalker.java | 116 ++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 Source/com/drew/imaging/isobmff/IsoBox.java create mode 100644 Source/com/drew/imaging/isobmff/IsoBoxVisitor.java create mode 100644 Source/com/drew/imaging/isobmff/IsoBoxWalker.java diff --git a/Source/com/drew/imaging/isobmff/IsoBox.java b/Source/com/drew/imaging/isobmff/IsoBox.java new file mode 100644 index 000000000..c1244686a --- /dev/null +++ b/Source/com/drew/imaging/isobmff/IsoBox.java @@ -0,0 +1,86 @@ +/* + * 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.isobmff; + +import com.drew.lang.SequentialReader; + +import java.io.IOException; +import java.util.Arrays; + +/** + * Represents an ISO Base Media File Format (ISOBMFF) box header. + *

+ * ISO/IEC 14496-12 section 4.2 defines the box structure: + *

+ */ +public final class IsoBox +{ + /** Total size of the box in bytes including the header, or -1 if the box extends to EOF. */ + public final long size; + + /** Four-character box type code. */ + public final String type; + + /** 16-byte user-defined type for {@code uuid} boxes; {@code null} otherwise. */ + public final byte[] usertype; + + /** Byte count of the payload (data after the full header). */ + final long payloadSize; + + IsoBox(SequentialReader reader) throws IOException + { + int headerSize = 8; // minimum: 4 (size field) + 4 (type field) + + long rawSize = reader.getUInt32(); + this.type = reader.getString(4); + + long resolvedSize; + if (rawSize == 1) { + resolvedSize = reader.getInt64(); + headerSize += 8; + } else if (rawSize == 0) { + resolvedSize = -1; // extends to EOF + } else { + resolvedSize = rawSize; + } + this.size = resolvedSize; + + if ("uuid".equals(this.type)) { + this.usertype = reader.getBytes(16); + headerSize += 16; + } else { + this.usertype = null; + } + + this.payloadSize = (resolvedSize == -1) ? -1 : resolvedSize - headerSize; + } + + /** Returns {@code true} if this is a {@code uuid} box whose usertype matches the given 16 bytes. */ + public boolean usertypeMatches(byte[] expected) + { + return usertype != null && Arrays.equals(usertype, expected); + } +} diff --git a/Source/com/drew/imaging/isobmff/IsoBoxVisitor.java b/Source/com/drew/imaging/isobmff/IsoBoxVisitor.java new file mode 100644 index 000000000..bc0e26ee8 --- /dev/null +++ b/Source/com/drew/imaging/isobmff/IsoBoxVisitor.java @@ -0,0 +1,67 @@ +/* + * 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.isobmff; + +import com.drew.lang.annotations.NotNull; + +import java.io.IOException; + +/** + * Visitor interface for {@link IsoBoxWalker}. + *

+ * For each box encountered the walker first asks {@link #shouldRecurse} — if {@code true} it + * calls {@link #processContainer} to obtain the visitor for the nested level, then recurses. + * Otherwise it asks {@link #shouldVisit} — if {@code true} it reads the payload bytes and calls + * {@link #visit}. If neither, the box is skipped. + */ +public abstract class IsoBoxVisitor +{ + /** + * Returns {@code true} if the walker should recurse into this box's payload as a nested box sequence. + * When {@code true}, {@link #processContainer} is called next. + */ + protected abstract boolean shouldRecurse(@NotNull IsoBox box); + + /** + * Called before recursing into a container box. Returns the visitor to use for the nested level. + * Returning {@code this} continues with the same visitor; returning a new instance delegates to + * a format-specific sub-handler. + */ + @NotNull + protected abstract IsoBoxVisitor processContainer(@NotNull IsoBox box); + + /** + * Returns {@code true} if the walker should read the payload and call {@link #visit}. + * Only consulted when {@link #shouldRecurse} returned {@code false}. + */ + protected abstract boolean shouldVisit(@NotNull IsoBox box); + + /** + * Called with the complete payload bytes of a visited box. + * + * @param box the box header (type, size, usertype) + * @param payload bytes of the box payload (everything after the full box header) + */ + protected abstract void visit(@NotNull IsoBox box, @NotNull byte[] payload) throws IOException; + + /** Records an error encountered during parsing. */ + public abstract void addError(@NotNull String message); +} diff --git a/Source/com/drew/imaging/isobmff/IsoBoxWalker.java b/Source/com/drew/imaging/isobmff/IsoBoxWalker.java new file mode 100644 index 000000000..d178134f3 --- /dev/null +++ b/Source/com/drew/imaging/isobmff/IsoBoxWalker.java @@ -0,0 +1,116 @@ +/* + * 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.isobmff; + +import com.drew.lang.SequentialReader; +import com.drew.lang.annotations.NotNull; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Walks an ISO Base Media File Format (ISOBMFF) box sequence, dispatching each box + * to an {@link IsoBoxVisitor}. + *

+ * Handles standard 32-bit sizes, 64-bit large sizes ({@code size == 1}), EOF-terminated + * boxes ({@code size == 0}), and {@code uuid} boxes whose 16-byte usertype is parsed into + * {@link IsoBox#usertype} before any payload is read. + */ +public final class IsoBoxWalker +{ + private IsoBoxWalker() {} + + /** + * Starts a walk over the boxes in {@code inputStream} using the given visitor. + * The stream is read with Motorola (big-endian) byte order, which is mandatory for ISOBMFF. + */ + public static void walk(@NotNull InputStream inputStream, @NotNull IsoBoxVisitor visitor) + { + com.drew.lang.StreamReader reader = new com.drew.lang.StreamReader(inputStream); + reader.setMotorolaByteOrder(true); + walk(reader, -1L, visitor); + } + + /** + * Recursively walks boxes from the current position in {@code reader} until + * {@code containerEnd} bytes have been consumed (or EOF when {@code containerEnd == -1}). + */ + static void walk(@NotNull SequentialReader reader, long containerEnd, @NotNull IsoBoxVisitor visitor) + { + try { + while (containerEnd == -1 || reader.getPosition() < containerEnd) { + IsoBox box; + try { + box = new IsoBox(reader); + } catch (IOException e) { + // Clean EOF when reading the next box header: normal for open-ended containers. + if (containerEnd == -1) break; + visitor.addError(e.getMessage()); + break; + } + + if (box.payloadSize < 0 && box.size != -1) { + visitor.addError("Box payload size is negative for type: " + box.type); + break; + } + + long payloadEnd = (box.payloadSize == -1) ? -1L : reader.getPosition() + box.payloadSize; + + if (visitor.shouldRecurse(box)) { + walk(reader, payloadEnd, visitor.processContainer(box)); + // After recursion, ensure the reader is positioned at the end of this box. + // The inner walk may have stopped early (e.g. on an error), which would leave + // the reader in the middle of the container and corrupt all subsequent sibling + // box reads (e.g. the top-level XMP UUID after moov would be missed). + if (payloadEnd != -1) { + long remaining = payloadEnd - reader.getPosition(); + if (remaining > 0) + reader.skip(remaining); + } + } else if (visitor.shouldVisit(box)) { + if (box.payloadSize == -1) { + visitor.addError("Cannot visit EOF-terminated box: " + box.type); + break; + } else if (box.payloadSize > Integer.MAX_VALUE) { + visitor.addError("Box payload too large to read into memory: " + box.type); + reader.skip(box.payloadSize); + } else { + byte[] payload = reader.getBytes((int) box.payloadSize); + try { + visitor.visit(box, payload); + } catch (IOException e) { + // A single bad box must not abort the entire walk. + visitor.addError("Error visiting box '" + box.type + "': " + e.getMessage()); + } + } + } else { + if (box.payloadSize > 0) { + reader.skip(box.payloadSize); + } else if (box.payloadSize == -1) { + break; // EOF-terminated box, nothing more to do + } + } + } + } catch (IOException e) { + visitor.addError(e.getMessage()); + } + } +} From fba97482d6ec91a7016937b43a391dbcf62693e4 Mon Sep 17 00:00:00 2001 From: ashrafuzzamanpurno Date: Tue, 26 May 2026 15:17:06 +0600 Subject: [PATCH 2/3] Introduces Cr3MetadataReader and the CR3 handler hierarchy built on IsoBoxWalker. Decodes the three Canon uuid blocks: CMT1-CMT4 (TIFF blobs feeding ExifTiffHandler for IFD0, ExifSubIFD, Canon makernote and GPS), the XMP uuid (delegated to XmpReader), and the preview uuid (PRVW dimensions). Thumbnail dimensions are read from the THMB box and the compressor version from CNCV. --- .../com/drew/imaging/ImageMetadataReader.java | 3 + .../drew/imaging/cr3/Cr3CanonUuidHandler.java | 202 ++++++++++++++++++ Source/com/drew/imaging/cr3/Cr3Handler.java | 146 +++++++++++++ .../drew/imaging/cr3/Cr3MetadataReader.java | 80 +++++++ .../imaging/cr3/Cr3PreviewUuidHandler.java | 75 +++++++ Source/com/drew/metadata/cr3/Cr3BoxTypes.java | 61 ++++++ .../drew/metadata/cr3/Cr3ContainerTypes.java | 32 +++ .../com/drew/metadata/cr3/Cr3Descriptor.java | 54 +++++ .../com/drew/metadata/cr3/Cr3Directory.java | 79 +++++++ 9 files changed, 732 insertions(+) create mode 100644 Source/com/drew/imaging/cr3/Cr3CanonUuidHandler.java create mode 100644 Source/com/drew/imaging/cr3/Cr3Handler.java create mode 100644 Source/com/drew/imaging/cr3/Cr3MetadataReader.java create mode 100644 Source/com/drew/imaging/cr3/Cr3PreviewUuidHandler.java create mode 100644 Source/com/drew/metadata/cr3/Cr3BoxTypes.java create mode 100644 Source/com/drew/metadata/cr3/Cr3ContainerTypes.java create mode 100644 Source/com/drew/metadata/cr3/Cr3Descriptor.java create mode 100644 Source/com/drew/metadata/cr3/Cr3Directory.java diff --git a/Source/com/drew/imaging/ImageMetadataReader.java b/Source/com/drew/imaging/ImageMetadataReader.java index 5eba8059f..3bc3b2607 100644 --- a/Source/com/drew/imaging/ImageMetadataReader.java +++ b/Source/com/drew/imaging/ImageMetadataReader.java @@ -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; @@ -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: diff --git a/Source/com/drew/imaging/cr3/Cr3CanonUuidHandler.java b/Source/com/drew/imaging/cr3/Cr3CanonUuidHandler.java new file mode 100644 index 000000000..55bf95c3c --- /dev/null +++ b/Source/com/drew/imaging/cr3/Cr3CanonUuidHandler.java @@ -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. + *

+ * Decodes: + *

+ * 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 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. + *

+ * THMB payload layout (big-endian): + *

+     *   uint32 version/flags
+     *   uint16 width
+     *   uint16 height
+     *   uint32 jpeg_size
+     *   uint32 unknown
+     *   byte[] jpeg_data
+     * 
+ */ + 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 _rootClass; + + FixedRootExifTiffHandler(@NotNull Metadata metadata, + @NotNull Class rootClass) + { + super(metadata, null, 0); + _rootClass = rootClass; + } + + @Override + public void setTiffMarker(int marker) throws TiffProcessingException + { + pushDirectory(_rootClass); + } + } +} diff --git a/Source/com/drew/imaging/cr3/Cr3Handler.java b/Source/com/drew/imaging/cr3/Cr3Handler.java new file mode 100644 index 000000000..b51243ada --- /dev/null +++ b/Source/com/drew/imaging/cr3/Cr3Handler.java @@ -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. + *

+ * 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 compatibleBrands = new ArrayList(); + // 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])); + } +} diff --git a/Source/com/drew/imaging/cr3/Cr3MetadataReader.java b/Source/com/drew/imaging/cr3/Cr3MetadataReader.java new file mode 100644 index 000000000..5022d2006 --- /dev/null +++ b/Source/com/drew/imaging/cr3/Cr3MetadataReader.java @@ -0,0 +1,80 @@ +/* + * 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.IsoBoxWalker; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; +import com.drew.metadata.cr3.Cr3Directory; +import com.drew.metadata.file.FileSystemMetadataReader; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Reads metadata from Canon CR3 (Crx) raw image files. + *

+ * CR3 uses the ISO Base Media File Format (ISOBMFF) as its container, with Canon-specific + * {@code uuid} boxes that embed TIFF-formatted EXIF, makernote, and GPS data (CMT1–CMT4), + * an XMP packet, and JPEG thumbnail/preview images. + *

+ * Camera metadata is populated into the standard directories + * ({@link com.drew.metadata.exif.ExifIFD0Directory}, + * {@link com.drew.metadata.exif.ExifSubIFDDirectory}, + * {@link com.drew.metadata.exif.makernotes.CanonMakernoteDirectory}, + * {@link com.drew.metadata.exif.GpsDirectory}) by reusing the existing + * {@link com.drew.metadata.exif.ExifTiffHandler} pipeline. + * + * @see lclevy/canon_cr3 format specification + */ +public class Cr3MetadataReader +{ + @NotNull + public static Metadata readMetadata(@NotNull final File file) throws IOException + { + InputStream inputStream = new FileInputStream(file); + Metadata metadata; + try { + metadata = readMetadata(inputStream); + } finally { + inputStream.close(); + } + new FileSystemMetadataReader().read(file, metadata); + return metadata; + } + + @NotNull + public static Metadata readMetadata(@NotNull InputStream inputStream) + { + Metadata metadata = new Metadata(); + extract(inputStream, metadata); + return metadata; + } + + public static void extract(@NotNull InputStream inputStream, @NotNull Metadata metadata) + { + Cr3Directory directory = new Cr3Directory(); + metadata.addDirectory(directory); + IsoBoxWalker.walk(inputStream, new Cr3Handler(metadata, directory)); + } +} diff --git a/Source/com/drew/imaging/cr3/Cr3PreviewUuidHandler.java b/Source/com/drew/imaging/cr3/Cr3PreviewUuidHandler.java new file mode 100644 index 000000000..a3b78272a --- /dev/null +++ b/Source/com/drew/imaging/cr3/Cr3PreviewUuidHandler.java @@ -0,0 +1,75 @@ +/* + * 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.lang.SequentialByteArrayReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.cr3.Cr3Directory; + +import java.io.IOException; + +/** + * Parses the Canon preview UUID ({@code eaf42b5e-1c98-4b88-b9fb-b7dc406e4d16}). + *

+ * The UUID payload has an 8-byte preamble followed by a standard {@code PRVW} box. + * Decodes the {@code PRVW} header to extract preview image dimensions and stores + * them on {@link Cr3Directory}. The JPEG payload is discarded. + */ +class Cr3PreviewUuidHandler +{ + private static final int PREAMBLE_SIZE = 8; + private static final int BOX_HEADER_SIZE = 8; // size(4) + type(4) + + private final Cr3Directory directory; + + Cr3PreviewUuidHandler(@NotNull Cr3Directory directory) + { + this.directory = directory; + } + + /** + * Parses the full preview UUID payload, skipping the preamble and PRVW box + * header to reach the PRVW dimension fields. + *

+ * PRVW payload layout (big-endian, after preamble + box header): + *

+     *   uint32 unknown/flags
+     *   uint16 unknown
+     *   uint16 width
+     *   uint16 height
+     *   ...
+     *   byte[] jpeg_data
+     * 
+ */ + void parsePayload(@NotNull byte[] uuidPayload) + { + try { + SequentialByteArrayReader reader = new SequentialByteArrayReader(uuidPayload); + reader.skip(PREAMBLE_SIZE + BOX_HEADER_SIZE + 6); // preamble + PRVW box header + uint32 flags + uint16 unknown + int width = reader.getUInt16(); + int height = reader.getUInt16(); + directory.setInt(Cr3Directory.TAG_PREVIEW_WIDTH, width); + directory.setInt(Cr3Directory.TAG_PREVIEW_HEIGHT, height); + } catch (IOException e) { + directory.addError("Error reading PRVW dimensions: " + e.getMessage()); + } + } +} diff --git a/Source/com/drew/metadata/cr3/Cr3BoxTypes.java b/Source/com/drew/metadata/cr3/Cr3BoxTypes.java new file mode 100644 index 000000000..6e385c7a0 --- /dev/null +++ b/Source/com/drew/metadata/cr3/Cr3BoxTypes.java @@ -0,0 +1,61 @@ +/* + * 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.metadata.cr3; + +/** + * FourCC box type constants for Canon CR3 files. + *

+ * See https://github.com/lclevy/canon_cr3 for the CR3 format specification. + */ +public final class Cr3BoxTypes +{ + /** File type box (standard ISOBMFF). */ + public static final String BOX_FILE_TYPE = "ftyp"; + + /** Canon Compressor Version — 30-byte ASCII string identifying the encoder version. */ + public static final String BOX_CANON_COMPRESSOR_VERSION = "CNCV"; + + /** Canon Compressor Table Pointers — track layout metadata. Not decoded; skipped by the walker. */ + public static final String BOX_CANON_COMPRESSOR_TABLE = "CCTP"; + + /** Canon Compressor Data — per-track records within CCTP. Not decoded; skipped by the walker. */ + public static final String BOX_CANON_COMPRESSOR_DATA = "CCDT"; + + /** Canon Track Base Offsets — file offsets for each track. Not decoded; skipped by the walker. */ + public static final String BOX_CANON_TRACK_BASE_OFFSETS = "CTBO"; + + /** Canon Metadata in TIFF format — IFD0 (Make, Model, DateTime, …). */ + public static final String BOX_CANON_TIFF_IFD0 = "CMT1"; + + /** Canon Metadata in TIFF format — Exif sub-IFD (exposure, focal length, …). */ + public static final String BOX_CANON_TIFF_EXIF = "CMT2"; + + /** Canon Metadata in TIFF format — Canon makernote IFD. */ + public static final String BOX_CANON_TIFF_MAKERNOTE = "CMT3"; + + /** Canon Metadata in TIFF format — GPS IFD. */ + public static final String BOX_CANON_TIFF_GPS = "CMT4"; + + /** Thumbnail box — 160×120 JPEG inside the Canon metadata UUID. */ + public static final String BOX_THUMBNAIL = "THMB"; + + private Cr3BoxTypes() {} +} diff --git a/Source/com/drew/metadata/cr3/Cr3ContainerTypes.java b/Source/com/drew/metadata/cr3/Cr3ContainerTypes.java new file mode 100644 index 000000000..26ccd6a3f --- /dev/null +++ b/Source/com/drew/metadata/cr3/Cr3ContainerTypes.java @@ -0,0 +1,32 @@ +/* + * 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.metadata.cr3; + +/** + * ISOBMFF container (box-of-boxes) type constants used in Canon CR3 files. + */ +public final class Cr3ContainerTypes +{ + /** Movie container — holds tracks, header, and Canon metadata UUID. */ + public static final String BOX_MOVIE = "moov"; + + private Cr3ContainerTypes() {} +} diff --git a/Source/com/drew/metadata/cr3/Cr3Descriptor.java b/Source/com/drew/metadata/cr3/Cr3Descriptor.java new file mode 100644 index 000000000..e269ac4b1 --- /dev/null +++ b/Source/com/drew/metadata/cr3/Cr3Descriptor.java @@ -0,0 +1,54 @@ +/* + * 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.metadata.cr3; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.TagDescriptor; + +/** + * Provides human-readable descriptions for {@link Cr3Directory} tags. + */ +public class Cr3Descriptor extends TagDescriptor +{ + public Cr3Descriptor(@NotNull Cr3Directory directory) + { + super(directory); + } + + @Override + public String getDescription(int tagType) + { + switch (tagType) { + case Cr3Directory.TAG_COMPATIBLE_BRANDS: + return getCompatibleBrandsDescription(); + default: + return _directory.getString(tagType); + } + } + + private String getCompatibleBrandsDescription() + { + String[] values = _directory.getStringArray(Cr3Directory.TAG_COMPATIBLE_BRANDS); + if (values == null) + return null; + return String.join(", ", values); + } +} diff --git a/Source/com/drew/metadata/cr3/Cr3Directory.java b/Source/com/drew/metadata/cr3/Cr3Directory.java new file mode 100644 index 000000000..445892a6d --- /dev/null +++ b/Source/com/drew/metadata/cr3/Cr3Directory.java @@ -0,0 +1,79 @@ +/* + * 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.metadata.cr3; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * Holds CR3-container-level metadata: file type brand, Canon compressor version, + * and the dimensions of the embedded thumbnail and preview images. + *

+ * Camera data (EXIF, makernote, GPS) is stored in the standard directories + * ({@code ExifIFD0Directory}, {@code ExifSubIFDDirectory}, {@code CanonMakernoteDirectory}, + * {@code GpsDirectory}) that are populated via the CMT1–CMT4 TIFF blobs inside the file. + */ +public class Cr3Directory extends Directory +{ + public static final int TAG_MAJOR_BRAND = 1; + public static final int TAG_MINOR_VERSION = 2; + public static final int TAG_COMPATIBLE_BRANDS = 3; + public static final int TAG_COMPRESSOR_VERSION = 4; + public static final int TAG_THUMBNAIL_WIDTH = 5; + public static final int TAG_THUMBNAIL_HEIGHT = 6; + public static final int TAG_PREVIEW_WIDTH = 7; + public static final int TAG_PREVIEW_HEIGHT = 8; + + @NotNull + private static final HashMap _tagNameMap = new HashMap(); + + static { + _tagNameMap.put(TAG_MAJOR_BRAND, "Major Brand"); + _tagNameMap.put(TAG_MINOR_VERSION, "Minor Version"); + _tagNameMap.put(TAG_COMPATIBLE_BRANDS, "Compatible Brands"); + _tagNameMap.put(TAG_COMPRESSOR_VERSION, "Compressor Version"); + _tagNameMap.put(TAG_THUMBNAIL_WIDTH, "Thumbnail Width"); + _tagNameMap.put(TAG_THUMBNAIL_HEIGHT, "Thumbnail Height"); + _tagNameMap.put(TAG_PREVIEW_WIDTH, "Preview Width"); + _tagNameMap.put(TAG_PREVIEW_HEIGHT, "Preview Height"); + } + + public Cr3Directory() + { + this.setDescriptor(new Cr3Descriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "CR3"; + } + + @Override + @NotNull + protected HashMap getTagNameMap() + { + return _tagNameMap; + } +} From 7ed3a887c04953bb0b97c5565156da464e7f1ba7 Mon Sep 17 00:00:00 2001 From: ashrafuzzamanpurno Date: Tue, 26 May 2026 15:20:44 +0600 Subject: [PATCH 3/3] Expanded Canon makernote tag coverage and descriptor accuracy --- .../makernotes/CanonMakernoteDescriptor.java | 209 ++++++++++++++++-- .../makernotes/CanonMakernoteDirectory.java | 36 +++ 2 files changed, 226 insertions(+), 19 deletions(-) diff --git a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java index 665e81129..aa4ef5675 100644 --- a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java @@ -99,6 +99,8 @@ public String getDescription(int tagType) return getFlashDetailsDescription(); case CameraSettings.TAG_FOCUS_MODE_2: return getFocusMode2Description(); + case FocalLength.TAG_FOCAL_TYPE: + return getFocalTypeDescription(); case FocalLength.TAG_WHITE_BALANCE: return getWhiteBalanceDescription(); case FocalLength.TAG_AF_POINT_USED: @@ -127,6 +129,12 @@ public String getDescription(int tagType) return getColorToneDescription(); case CanonMakernoteDirectory.CameraSettings.TAG_SRAW_QUALITY: return getSRawQualityDescription(); + case CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_BRACKETING: + return getFocusBracketingDescription(); + case CanonMakernoteDirectory.CameraSettings.TAG_CLARITY: + return getClarityDescription(); + case CanonMakernoteDirectory.CameraSettings.TAG_HDR_PQ: + return getHdrPqDescription(); // It turns out that these values are dependent upon the camera model and therefore the below code was // incorrect for some Canon models. This needs to be revisited. @@ -440,17 +448,36 @@ public String getFlashDetailsDescription() Integer value = _directory.getInteger(CameraSettings.TAG_FLASH_DETAILS); if (value == null) return null; - if (((value >> 14) & 1) != 0) { - return "External E-TTL"; + StringBuilder sb = new StringBuilder(); + if ((value & (1 << 0)) != 0) { + append(sb, "Manual"); + } + if ((value & (1 << 1)) != 0) { + append(sb, "TTL"); + } + if ((value & (1 << 2)) != 0) { + append(sb, "A-TTL"); + } + if ((value & (1 << 3)) != 0) { + append(sb, "E-TTL"); + } + if ((value & (1 << 4)) != 0) { + append(sb, "FP sync enabled"); + } + if ((value & (1 << 7)) != 0) { + append(sb, "External"); } - if (((value >> 13) & 1) != 0) { - return "Internal flash"; + if ((value & (1 << 11)) != 0) { + append(sb, "FP sync used"); } - if (((value >> 11) & 1) != 0) { - return "FP sync used"; + if ((value & (1 << 13)) != 0) { + append(sb, "Internal"); } - if (((value >> 4) & 1) != 0) { - return "FP sync enabled"; + if ((value & (1 << 14)) != 0) { + append(sb, "External E-TTL"); + } + if (sb.length() > 0) { + return sb.toString(); } return "Unknown (" + value + ")"; } @@ -604,8 +631,10 @@ public String getSharpnessDescription() return "Normal"; case 0x001: return "High"; + case 0x7FFF: + return "n/a"; default: - return "Unknown (" + value + ")"; + return toSignedShortString(value); } } @@ -622,8 +651,10 @@ public String getSaturationDescription() return "Normal"; case 0x001: return "High"; + case 0x7FFF: + return "n/a"; default: - return "Unknown (" + value + ")"; + return toSignedShortString(value); } } @@ -640,8 +671,10 @@ public String getContrastDescription() return "Normal"; case 0x001: return "High"; + case 0x7FFF: + return "n/a"; default: - return "Unknown (" + value + ")"; + return toSignedShortString(value); } } @@ -668,12 +701,31 @@ public String getEasyShootingModeDescription() @Nullable public String getImageSizeDescription() { - return getIndexedDescription( - CameraSettings.TAG_IMAGE_SIZE, - "Large", - "Medium", - "Small" - ); + Integer value = _directory.getInteger(CameraSettings.TAG_IMAGE_SIZE); + if (value == null) + return null; + switch (value) { + case 0: + return "Large"; + case 1: + return "Medium"; + case 2: + return "Small"; + case 5: + return "Medium 1"; + case 6: + return "Medium 2"; + case 7: + return "Small 1"; + case 8: + return "Small 2"; + case 9: + return "Small 3"; + case 0xFFFF: + return "n/a"; + default: + return "Unknown (" + value + ")"; + } } @Nullable @@ -705,6 +757,22 @@ public String getContinuousDriveModeDescription() return delay == 0 ? "Single shot" : "Single shot with self-timer"; case 1: return "Continuous"; + case 2: + return "Movie"; + case 3: + return "Continuous, Speed Priority"; + case 4: + return "Continuous, Low"; + case 5: + return "Continuous, High"; + case 6: + return "Silent Single"; + case 8: + return "Continuous, High+"; + case 9: + return "Single, Silent"; + case 10: + return "Continuous, Silent"; } return "Unknown (" + value + ")"; } @@ -797,7 +865,7 @@ public String getDigitalZoomDescription() @Nullable public String getRecordModeDescription() { - return getIndexedDescription(CameraSettings.TAG_RECORD_MODE, 1, "JPEG", "CRW+THM", "AVI+THM", "TIF", "TIF+JPEG", "CR2", "CR2+JPEG", null, "MOV", "MP4"); + return getIndexedDescription(CameraSettings.TAG_RECORD_MODE, 1, "JPEG", "CRW+THM", "AVI+THM", "TIF", "TIF+JPEG", "CR2", "CR2+JPEG", null, "MOV", "MP4", "CRM", "CR3", "CR3+JPEG"); } @Nullable @@ -926,7 +994,73 @@ public String getColorToneDescription() @Nullable public String getSRawQualityDescription() { - return getIndexedDescription(CanonMakernoteDirectory.CameraSettings.TAG_SRAW_QUALITY, 0, "n/a", "sRAW1 (mRAW)", "sRAW2 (sRAW)"); + Integer value = _directory.getInteger(CameraSettings.TAG_SRAW_QUALITY); + if (value == null) + return null; + switch (value) { + case 0: + return "n/a"; + case 1: + return "sRAW1 (mRAW)"; + case 2: + return "sRAW2 (sRAW)"; + case 0xFFFF: + return "n/a"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getFocusBracketingDescription() + { + return getIndexedDescription(CameraSettings.TAG_FOCUS_BRACKETING, "Disable", "Enable"); + } + + @Nullable + public String getClarityDescription() + { + Integer value = _directory.getInteger(CameraSettings.TAG_CLARITY); + if (value == null) + return null; + if (value == 0x7FFF) + return "n/a"; + return Integer.toString(value > 32767 ? value - 65536 : value); + } + + @Nullable + public String getHdrPqDescription() + { + return getIndexedDescription(CameraSettings.TAG_HDR_PQ, "Off", "On"); + } + + @Nullable + public String getFocalTypeDescription() + { + Integer value = _directory.getInteger(FocalLength.TAG_FOCAL_TYPE); + if (value == null) + return null; + switch (value) { + case 0: + return "Fixed"; + case 2: + return "Zoom"; + default: + return "Unknown (" + value + ")"; + } + } + + private static String toSignedShortString(int value) + { + int signed = value > 32767 ? value - 65536 : value; + return String.format("%+d", signed); + } + + private static void append(@NotNull StringBuilder sb, @NotNull String bit) + { + if (sb.length() != 0) + sb.append(", "); + sb.append(bit); } /** @@ -1174,6 +1308,43 @@ else if (frac == 0x14) _lensTypeById.put(4154, "Canon EF-S 24mm f/2.8 STM"); _lensTypeById.put(4156, "Canon EF 50mm f/1.8 STM"); _lensTypeById.put(36912, "Canon EF-S 18-135mm f/3.5-5.6 IS USM"); + + // Canon RF lenses + _lensTypeById.put(61182, "Canon RF 50mm F1.2L USM"); + _lensTypeById.put(61183, "Canon RF 24-105mm F4L IS USM"); + _lensTypeById.put(61184, "Canon RF 28-70mm F2L USM"); + _lensTypeById.put(61185, "Canon RF 35mm F1.8 Macro IS STM"); + _lensTypeById.put(61186, "Canon RF 85mm F1.2L USM"); + _lensTypeById.put(61187, "Canon RF 85mm F1.2L USM DS"); + _lensTypeById.put(61188, "Canon RF 24-70mm F2.8L IS USM"); + _lensTypeById.put(61189, "Canon RF 15-35mm F2.8L IS USM"); + _lensTypeById.put(61190, "Canon RF 24-240mm F4-6.3 IS USM"); + _lensTypeById.put(61191, "Canon RF 70-200mm F2.8L IS USM"); + _lensTypeById.put(61195, "Canon RF 85mm F2 Macro IS STM"); + _lensTypeById.put(61196, "Canon RF 600mm F11 IS STM"); + _lensTypeById.put(61197, "Canon RF 800mm F11 IS STM"); + _lensTypeById.put(61198, "Canon RF 24-105mm F4-7.1 IS STM"); + _lensTypeById.put(61199, "Canon RF 100-500mm F4.5-7.1L IS USM"); + _lensTypeById.put(61200, "Canon RF 70-200mm F4L IS USM"); + _lensTypeById.put(61202, "Canon RF 100mm F2.8L Macro IS USM"); + _lensTypeById.put(61203, "Canon RF 400mm F2.8L IS USM"); + _lensTypeById.put(61204, "Canon RF 600mm F4L IS USM"); + _lensTypeById.put(61205, "Canon RF 14-35mm F4L IS USM"); + _lensTypeById.put(61206, "Canon RF 24mm F1.8 Macro IS STM"); + _lensTypeById.put(61207, "Canon RF 16mm F2.8 STM"); + _lensTypeById.put(61208, "Canon RF 100-400mm F5.6-8 IS USM"); + _lensTypeById.put(61209, "Canon RF 800mm F5.6L IS USM"); + _lensTypeById.put(61210, "Canon RF 1200mm F8L IS USM"); + _lensTypeById.put(61211, "Canon RF 5.2mm F2.8L Dual Fisheye"); + _lensTypeById.put(61213, "Canon RF 15-30mm F4.5-6.3 IS STM"); + _lensTypeById.put(61215, "Canon RF 135mm F1.8L IS USM"); + _lensTypeById.put(61217, "Canon RF 100-300mm F2.8L IS USM"); + _lensTypeById.put(61218, "Canon RF 200-800mm F6.3-9 IS USM"); + _lensTypeById.put(61220, "Canon RF 35mm F1.4L VCM"); + _lensTypeById.put(61222, "Canon RF 28mm F2.8 STM"); + _lensTypeById.put(61224, "Canon RF 10-20mm F4L IS STM"); + _lensTypeById.put(61226, "Canon RF 24-105mm F2.8L IS USM Z"); + _lensTypeById.put(65535, "N/A"); } } diff --git a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java index 4cd7aa932..0dcf281ae 100644 --- a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java @@ -112,7 +112,12 @@ public class CanonMakernoteDirectory extends Directory public static final int TAG_LIGHTING_OPTIMIZER_ARRAY = 0x4018; // not currently decoded public static final int TAG_LENS_INFO_ARRAY = 0x4019; // not currently decoded public static final int TAG_AMBIANCE_INFO_ARRAY = 0x4020; // not currently decoded + public static final int TAG_MULTI_EXPOSURE_ARRAY = 0x4021; // not currently decoded public static final int TAG_FILTER_INFO_ARRAY = 0x4024; // not currently decoded + public static final int TAG_HDR_INFO_ARRAY = 0x4025; // not currently decoded + public static final int TAG_AF_CONFIG_ARRAY = 0x4028; // not currently decoded + public static final int TAG_RAW_BURST_MODE_ROLL = 0x403F; // not currently decoded + public static final int TAG_LEVEL_INFO_ARRAY = 0x4059; // not currently decoded public final static class CameraSettings { @@ -275,6 +280,23 @@ public final static class CameraSettings public static final int TAG_COLOR_TONE = OFFSET + 0x29; public static final int TAG_SRAW_QUALITY = OFFSET + 0x2D; + + /** + * 0 = Disable + * 1 = Enable + */ + public static final int TAG_FOCUS_BRACKETING = OFFSET + 0x2F; + + /** + * Signed numeric clarity offset. 0x7FFF = n/a. + */ + public static final int TAG_CLARITY = OFFSET + 0x33; + + /** + * 0 = Off + * 1 = On + */ + public static final int TAG_HDR_PQ = OFFSET + 0x34; } public final static class FocalLength @@ -283,6 +305,11 @@ public final static class FocalLength private static final int OFFSET = 0xC200; + /** + * 0 = Fixed + * 2 = Zoom + */ + public static final int TAG_FOCAL_TYPE = OFFSET + 0x00; /** * 0 = Auto * 1 = Sunny @@ -544,7 +571,11 @@ public final static class AFInfo _tagNameMap.put(CameraSettings.TAG_MANUAL_FLASH_OUTPUT, "Manual Flash Output"); _tagNameMap.put(CameraSettings.TAG_COLOR_TONE, "Color Tone"); _tagNameMap.put(CameraSettings.TAG_SRAW_QUALITY, "SRAW Quality"); + _tagNameMap.put(CameraSettings.TAG_FOCUS_BRACKETING, "Focus Bracketing"); + _tagNameMap.put(CameraSettings.TAG_CLARITY, "Clarity"); + _tagNameMap.put(CameraSettings.TAG_HDR_PQ, "HDR PQ"); + _tagNameMap.put(FocalLength.TAG_FOCAL_TYPE, "Focal Type"); _tagNameMap.put(FocalLength.TAG_WHITE_BALANCE, "White Balance"); _tagNameMap.put(FocalLength.TAG_SEQUENCE_NUMBER, "Sequence Number"); _tagNameMap.put(FocalLength.TAG_AF_POINT_USED, "AF Point Used"); @@ -664,7 +695,12 @@ public final static class AFInfo _tagNameMap.put(TAG_LIGHTING_OPTIMIZER_ARRAY, "Lighting Optimizer Array"); _tagNameMap.put(TAG_LENS_INFO_ARRAY, "Lens Info Array"); _tagNameMap.put(TAG_AMBIANCE_INFO_ARRAY, "Ambiance Info Array"); + _tagNameMap.put(TAG_MULTI_EXPOSURE_ARRAY, "Multi Exposure Array"); _tagNameMap.put(TAG_FILTER_INFO_ARRAY, "Filter Info Array"); + _tagNameMap.put(TAG_HDR_INFO_ARRAY, "HDR Info Array"); + _tagNameMap.put(TAG_AF_CONFIG_ARRAY, "AF Config Array"); + _tagNameMap.put(TAG_RAW_BURST_MODE_ROLL, "Raw Burst Mode Roll"); + _tagNameMap.put(TAG_LEVEL_INFO_ARRAY, "Level Info Array"); } public CanonMakernoteDirectory()