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/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()); + } + } +} 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; + } +} 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()