From d38c0a546ee34d383f28b6d1fcddc0511d7312bc Mon Sep 17 00:00:00 2001 From: Markus Koschany <apo@debian.org> Date: Fri, 30 Jun 2017 21:38:10 +0200 Subject: [PATCH] New upstream version 2.10.1 --- CONTRIBUTING.md | 30 - LICENSE-2.0.txt | 2 +- README.md | 102 +- Resources/favicon-16px.png | Bin 0 -> 590 bytes Resources/favicon-256.ico | Bin 0 -> 9979 bytes Resources/favicon-256px.png | Bin 0 -> 7111 bytes Resources/favicon-32px.png | Bin 0 -> 1014 bytes Resources/favicon-48.ico | Bin 0 -> 2926 bytes Resources/favicon-48px.png | Bin 0 -> 1490 bytes Resources/favicon.xcf | Bin 0 -> 32098 bytes Resources/metadata-extractor-logo-square.svg | 69 + Resources/metadata-extractor-logo.svg | 8 + .../com/drew/metadata/GeoTagMapBuilder.java | 27 +- Samples/com/drew/metadata/SampleUsage.java | 4 +- Samples/com/drew/metadata/XmpSample.java | 70 + Source/com/drew/imaging/FileType.java | 4 +- Source/com/drew/imaging/FileTypeDetector.java | 12 +- .../com/drew/imaging/ImageMetadataReader.java | 101 +- .../imaging/ImageProcessingException.java | 2 +- .../drew/imaging/PhotographicConversions.java | 2 +- .../drew/imaging/bmp/BmpMetadataReader.java | 2 +- Source/com/drew/imaging/bmp/package-info.java | 6 + Source/com/drew/imaging/bmp/package.html | 34 - .../drew/imaging/gif/GifMetadataReader.java | 15 +- Source/com/drew/imaging/gif/package-info.java | 4 + Source/com/drew/imaging/gif/package.html | 33 - .../drew/imaging/ico/IcoMetadataReader.java | 59 + Source/com/drew/imaging/ico/package-info.java | 4 + .../drew/imaging/jpeg/JpegMetadataReader.java | 33 +- .../imaging/jpeg/JpegProcessingException.java | 2 +- .../drew/imaging/jpeg/JpegSegmentData.java | 2 +- .../jpeg/JpegSegmentMetadataReader.java | 16 +- .../drew/imaging/jpeg/JpegSegmentReader.java | 24 +- .../drew/imaging/jpeg/JpegSegmentType.java | 66 +- .../com/drew/imaging/jpeg/package-info.java | 4 + Source/com/drew/imaging/jpeg/package.html | 33 - Source/com/drew/imaging/package-info.java | 5 + Source/com/drew/imaging/package.html | 33 - .../drew/imaging/pcx/PcxMetadataReader.java | 59 + Source/com/drew/imaging/pcx/package-info.java | 4 + .../drew/imaging/png/PngChromaticities.java | 20 + Source/com/drew/imaging/png/PngChunk.java | 20 + .../com/drew/imaging/png/PngChunkReader.java | 20 + Source/com/drew/imaging/png/PngChunkType.java | 93 +- Source/com/drew/imaging/png/PngColorType.java | 20 + Source/com/drew/imaging/png/PngHeader.java | 20 + .../drew/imaging/png/PngMetadataReader.java | 397 +++-- .../imaging/png/PngProcessingException.java | 2 +- Source/com/drew/imaging/png/package-info.java | 6 + Source/com/drew/imaging/png/package.html | 34 - .../drew/imaging/psd/PsdMetadataReader.java | 18 +- Source/com/drew/imaging/psd/package-info.java | 4 + Source/com/drew/imaging/psd/package.html | 33 - .../drew/imaging/raf/RafMetadataReader.java | 72 + Source/com/drew/imaging/raf/package-info.java | 4 + Source/com/drew/imaging/riff/RiffHandler.java | 65 + .../imaging/riff/RiffProcessingException.java | 50 + Source/com/drew/imaging/riff/RiffReader.java | 101 ++ .../com/drew/imaging/riff/package-info.java | 4 + .../com/drew/imaging/tiff/TiffDataFormat.java | 2 +- Source/com/drew/imaging/tiff/TiffHandler.java | 14 +- .../drew/imaging/tiff/TiffMetadataReader.java | 23 +- .../imaging/tiff/TiffProcessingException.java | 2 +- Source/com/drew/imaging/tiff/TiffReader.java | 84 +- .../com/drew/imaging/tiff/package-info.java | 4 + Source/com/drew/imaging/tiff/package.html | 33 - .../drew/imaging/webp/WebpMetadataReader.java | 61 + .../com/drew/imaging/webp/package-info.java | 4 + .../com/drew/lang/BufferBoundsException.java | 2 +- Source/com/drew/lang/ByteArrayReader.java | 32 +- Source/com/drew/lang/ByteConvert.java | 45 + Source/com/drew/lang/ByteTrie.java | 2 +- Source/com/drew/lang/Charsets.java | 40 + Source/com/drew/lang/CompoundException.java | 2 +- Source/com/drew/lang/DateUtil.java | 32 + Source/com/drew/lang/GeoLocation.java | 4 +- Source/com/drew/lang/KeyValuePair.java | 29 +- Source/com/drew/lang/NullOutputStream.java | 2 +- .../com/drew/lang/RandomAccessFileReader.java | 20 +- Source/com/drew/lang/RandomAccessReader.java | 102 +- .../drew/lang/RandomAccessStreamReader.java | 36 +- Source/com/drew/lang/Rational.java | 119 +- .../drew/lang/SequentialByteArrayReader.java | 35 +- Source/com/drew/lang/SequentialReader.java | 95 +- Source/com/drew/lang/StreamReader.java | 40 +- Source/com/drew/lang/StreamUtil.java | 46 + Source/com/drew/lang/StringUtil.java | 4 +- Source/com/drew/lang/annotations/NotNull.java | 2 +- .../com/drew/lang/annotations/Nullable.java | 2 +- .../drew/lang/annotations/package-info.java | 5 + Source/com/drew/lang/annotations/package.html | 34 - Source/com/drew/lang/package-info.java | 4 + Source/com/drew/lang/package.html | 33 - Source/com/drew/metadata/Age.java | 7 +- Source/com/drew/metadata/Directory.java | 295 +++- ...TagDescriptor.java => ErrorDirectory.java} | 54 +- Source/com/drew/metadata/Face.java | 2 +- Source/com/drew/metadata/Metadata.java | 100 +- .../com/drew/metadata/MetadataException.java | 2 +- Source/com/drew/metadata/MetadataReader.java | 4 +- Source/com/drew/metadata/Schema.java | 41 + Source/com/drew/metadata/StringValue.java | 76 + Source/com/drew/metadata/Tag.java | 12 +- Source/com/drew/metadata/TagDescriptor.java | 202 ++- .../metadata/adobe/AdobeJpegDescriptor.java | 24 +- .../metadata/adobe/AdobeJpegDirectory.java | 3 +- .../drew/metadata/adobe/AdobeJpegReader.java | 30 +- .../com/drew/metadata/adobe/package-info.java | 4 + Source/com/drew/metadata/adobe/package.html | 33 - .../metadata/bmp/BmpHeaderDescriptor.java | 31 +- .../drew/metadata/bmp/BmpHeaderDirectory.java | 21 + Source/com/drew/metadata/bmp/BmpReader.java | 23 +- .../com/drew/metadata/bmp/package-info.java | 6 + Source/com/drew/metadata/bmp/package.html | 34 - .../metadata/exif/ExifDescriptorBase.java | 1234 ++++++++++++++ .../drew/metadata/exif/ExifDirectoryBase.java | 764 +++++++++ .../metadata/exif/ExifIFD0Descriptor.java | 178 +-- .../drew/metadata/exif/ExifIFD0Directory.java | 74 +- .../metadata/exif/ExifImageDescriptor.java | 37 + .../metadata/exif/ExifImageDirectory.java | 64 + .../metadata/exif/ExifInteropDescriptor.java | 42 +- .../metadata/exif/ExifInteropDirectory.java | 18 +- Source/com/drew/metadata/exif/ExifReader.java | 80 +- .../metadata/exif/ExifSubIFDDescriptor.java | 804 +--------- .../metadata/exif/ExifSubIFDDirectory.java | 676 +------- .../exif/ExifThumbnailDescriptor.java | 264 +-- .../metadata/exif/ExifThumbnailDirectory.java | 355 +---- .../drew/metadata/exif/ExifTiffHandler.java | 602 ++++++- .../com/drew/metadata/exif/GpsDescriptor.java | 13 +- .../com/drew/metadata/exif/GpsDirectory.java | 49 +- .../PanasonicRawDistortionDescriptor.java | 159 ++ .../exif/PanasonicRawDistortionDirectory.java | 86 + .../exif/PanasonicRawIFD0Descriptor.java | 58 + .../exif/PanasonicRawIFD0Directory.java | 153 ++ .../exif/PanasonicRawWbInfo2Descriptor.java | 71 + .../exif/PanasonicRawWbInfo2Directory.java | 103 ++ .../exif/PanasonicRawWbInfoDescriptor.java | 71 + .../exif/PanasonicRawWbInfoDirectory.java | 102 ++ .../drew/metadata/exif/PrintIMDescriptor.java | 58 + .../drew/metadata/exif/PrintIMDirectory.java | 67 + .../makernotes/AppleMakernoteDescriptor.java | 59 + .../makernotes/AppleMakernoteDirectory.java | 70 + .../makernotes/CanonMakernoteDescriptor.java | 461 +++++- .../makernotes/CanonMakernoteDirectory.java | 110 +- .../CasioType1MakernoteDescriptor.java | 9 +- .../CasioType1MakernoteDirectory.java | 3 +- .../CasioType2MakernoteDescriptor.java | 16 +- .../CasioType2MakernoteDirectory.java | 3 +- .../FujifilmMakernoteDescriptor.java | 3 +- .../FujifilmMakernoteDirectory.java | 3 +- .../makernotes/KodakMakernoteDescriptor.java | 3 +- .../makernotes/KodakMakernoteDirectory.java | 3 +- .../KyoceraMakernoteDescriptor.java | 11 +- .../makernotes/KyoceraMakernoteDirectory.java | 3 +- .../makernotes/LeicaMakernoteDescriptor.java | 3 +- .../makernotes/LeicaMakernoteDirectory.java | 3 +- .../LeicaType5MakernoteDescriptor.java | 79 + .../LeicaType5MakernoteDirectory.java | 79 + .../NikonType1MakernoteDescriptor.java | 3 +- .../NikonType1MakernoteDirectory.java | 3 +- .../NikonType2MakernoteDescriptor.java | 14 +- .../NikonType2MakernoteDirectory.java | 7 +- ...mpusCameraSettingsMakernoteDescriptor.java | 1412 +++++++++++++++++ ...ympusCameraSettingsMakernoteDirectory.java | 201 +++ .../OlympusEquipmentMakernoteDescriptor.java | 386 +++++ .../OlympusEquipmentMakernoteDirectory.java | 118 ++ .../OlympusFocusInfoMakernoteDescriptor.java | 217 +++ .../OlympusFocusInfoMakernoteDirectory.java | 110 ++ ...pusImageProcessingMakernoteDescriptor.java | 240 +++ ...mpusImageProcessingMakernoteDirectory.java | 212 +++ .../OlympusMakernoteDescriptor.java | 309 +++- .../makernotes/OlympusMakernoteDirectory.java | 508 +++++- ...pusRawDevelopment2MakernoteDescriptor.java | 230 +++ ...mpusRawDevelopment2MakernoteDirectory.java | 111 ++ ...mpusRawDevelopmentMakernoteDescriptor.java | 155 ++ ...ympusRawDevelopmentMakernoteDirectory.java | 91 ++ .../OlympusRawInfoMakernoteDescriptor.java | 141 ++ .../OlympusRawInfoMakernoteDirectory.java | 142 ++ .../PanasonicMakernoteDescriptor.java | 305 +++- .../PanasonicMakernoteDirectory.java | 126 +- .../makernotes/PentaxMakernoteDescriptor.java | 3 +- .../makernotes/PentaxMakernoteDirectory.java | 3 +- .../ReconyxHyperFireMakernoteDescriptor.java | 105 ++ .../ReconyxHyperFireMakernoteDirectory.java | 105 ++ .../ReconyxUltraFireMakernoteDescriptor.java | 110 ++ .../ReconyxUltraFireMakernoteDirectory.java | 116 ++ .../makernotes/RicohMakernoteDescriptor.java | 13 +- .../makernotes/RicohMakernoteDirectory.java | 3 +- .../SamsungType2MakernoteDescriptor.java | 213 +++ .../SamsungType2MakernoteDirectory.java | 89 ++ .../makernotes/SanyoMakernoteDescriptor.java | 3 +- .../makernotes/SanyoMakernoteDirectory.java | 7 +- .../makernotes/SigmaMakernoteDescriptor.java | 3 +- .../makernotes/SigmaMakernoteDirectory.java | 3 +- .../SonyType1MakernoteDescriptor.java | 33 +- .../SonyType1MakernoteDirectory.java | 5 +- .../SonyType6MakernoteDescriptor.java | 3 +- .../SonyType6MakernoteDirectory.java | 3 +- .../exif/makernotes/package-info.java | 5 + .../metadata/exif/makernotes/package.html | 33 - .../com/drew/metadata/exif/package-info.java | 4 + Source/com/drew/metadata/exif/package.html | 33 - .../metadata/file/FileMetadataDescriptor.java | 63 + .../metadata/file/FileMetadataDirectory.java | 65 + .../metadata/file/FileMetadataReader.java | 49 + .../com/drew/metadata/file/package-info.java | 6 + .../metadata/gif/GifAnimationDescriptor.java | 62 + .../metadata/gif/GifAnimationDirectory.java | 63 + .../metadata/gif/GifCommentDescriptor.java | 37 + .../metadata/gif/GifCommentDirectory.java | 65 + .../metadata/gif/GifControlDescriptor.java | 37 + .../metadata/gif/GifControlDirectory.java | 134 ++ .../metadata/gif/GifHeaderDescriptor.java | 21 + .../drew/metadata/gif/GifHeaderDirectory.java | 28 +- .../drew/metadata/gif/GifImageDescriptor.java | 37 + .../drew/metadata/gif/GifImageDirectory.java | 77 + Source/com/drew/metadata/gif/GifReader.java | 381 ++++- .../com/drew/metadata/gif/package-info.java | 6 + Source/com/drew/metadata/gif/package.html | 34 - .../com/drew/metadata/icc/IccDescriptor.java | 54 +- .../com/drew/metadata/icc/IccDirectory.java | 3 +- Source/com/drew/metadata/icc/IccReader.java | 88 +- .../com/drew/metadata/icc/package-info.java | 4 + Source/com/drew/metadata/icc/package.html | 33 - .../com/drew/metadata/ico/IcoDescriptor.java | 90 ++ .../com/drew/metadata/ico/IcoDirectory.java | 80 + Source/com/drew/metadata/ico/IcoReader.java | 110 ++ .../com/drew/metadata/ico/package-info.java | 4 + .../drew/metadata/iptc/IptcDescriptor.java | 145 +- .../com/drew/metadata/iptc/IptcDirectory.java | 84 +- Source/com/drew/metadata/iptc/IptcReader.java | 113 +- .../drew/metadata/iptc/Iso2022Converter.java | 39 +- .../com/drew/metadata/iptc/package-info.java | 4 + Source/com/drew/metadata/iptc/package.html | 33 - .../drew/metadata/jfif/JfifDescriptor.java | 28 +- .../com/drew/metadata/jfif/JfifDirectory.java | 24 +- Source/com/drew/metadata/jfif/JfifReader.java | 49 +- .../com/drew/metadata/jfif/package-info.java | 4 + Source/com/drew/metadata/jfif/package.html | 33 - .../drew/metadata/jfxx/JfxxDescriptor.java | 72 + .../com/drew/metadata/jfxx/JfxxDirectory.java | 70 + Source/com/drew/metadata/jfxx/JfxxReader.java | 80 + .../com/drew/metadata/jfxx/package-info.java | 4 + .../jpeg/HuffmanTablesDescriptor.java | 67 + .../metadata/jpeg/HuffmanTablesDirectory.java | 360 +++++ .../metadata/jpeg/JpegCommentDescriptor.java | 3 +- .../metadata/jpeg/JpegCommentDirectory.java | 3 +- .../drew/metadata/jpeg/JpegCommentReader.java | 24 +- .../com/drew/metadata/jpeg/JpegComponent.java | 24 +- .../drew/metadata/jpeg/JpegDescriptor.java | 71 +- .../com/drew/metadata/jpeg/JpegDhtReader.java | 101 ++ .../com/drew/metadata/jpeg/JpegDirectory.java | 3 +- .../com/drew/metadata/jpeg/JpegDnlReader.java | 77 + Source/com/drew/metadata/jpeg/JpegReader.java | 22 +- .../com/drew/metadata/jpeg/package-info.java | 4 + Source/com/drew/metadata/jpeg/package.html | 33 - Source/com/drew/metadata/package-info.java | 6 + Source/com/drew/metadata/package.html | 33 - .../com/drew/metadata/pcx/PcxDescriptor.java | 86 + .../com/drew/metadata/pcx/PcxDirectory.java | 87 + Source/com/drew/metadata/pcx/PcxReader.java | 87 + .../com/drew/metadata/pcx/package-info.java | 4 + .../metadata/photoshop/DuckyDirectory.java | 69 + .../drew/metadata/photoshop/DuckyReader.java | 116 ++ .../photoshop/PhotoshopDescriptor.java | 96 +- .../photoshop/PhotoshopDirectory.java | 177 ++- .../metadata/photoshop/PhotoshopReader.java | 120 +- .../photoshop/PsdHeaderDescriptor.java | 79 +- .../photoshop/PsdHeaderDirectory.java | 3 +- .../drew/metadata/photoshop/PsdReader.java | 70 +- .../drew/metadata/photoshop/package-info.java | 4 + .../com/drew/metadata/photoshop/package.html | 33 - .../png/PngChromaticitiesDirectory.java | 21 + .../com/drew/metadata/png/PngDescriptor.java | 73 +- .../com/drew/metadata/png/PngDirectory.java | 46 +- .../com/drew/metadata/png/package-info.java | 6 + Source/com/drew/metadata/png/package.html | 34 - .../metadata/tiff/DirectoryTiffHandler.java | 51 +- .../com/drew/metadata/tiff/package-info.java | 6 + Source/com/drew/metadata/tiff/package.html | 34 - .../drew/metadata/webp/WebpDescriptor.java | 47 + .../com/drew/metadata/webp/WebpDirectory.java | 67 + .../drew/metadata/webp/WebpRiffHandler.java | 164 ++ .../com/drew/metadata/webp/package-info.java | 6 + .../com/drew/metadata/xmp/XmpDescriptor.java | 140 +- .../com/drew/metadata/xmp/XmpDirectory.java | 153 +- Source/com/drew/metadata/xmp/XmpReader.java | 344 ++-- Source/com/drew/metadata/xmp/XmpWriter.java | 37 + .../com/drew/metadata/xmp/package-info.java | 4 + Source/com/drew/metadata/xmp/package.html | 33 - .../drew/tools/ExtractJpegSegmentTool.java | 4 +- Source/com/drew/tools/FileUtil.java | 2 +- .../ProcessAllImagesInFolderUtility.java | 384 ++++- Source/com/drew/tools/ProcessUrlUtility.java | 4 +- Source/com/drew/tools/package-info.java | 5 + Source/com/drew/tools/package.html | 33 - Tests/Data/withTypicalHuffman.jpg | Bin 0 -> 1570 bytes Tests/Data/withXmp.jpg | Bin 0 -> 12666 bytes .../imaging/jpeg/JpegMetadataReaderTest.java | 38 +- .../imaging/jpeg/JpegSegmentDataTest.java | 2 +- .../imaging/jpeg/JpegSegmentReaderTest.java | 32 +- .../drew/imaging/png/PngChunkReaderTest.java | 20 + .../drew/imaging/png/PngChunkTypeTest.java | 26 +- .../imaging/png/PngMetadataReaderTest.java | 90 +- Tests/com/drew/lang/ByteArrayReaderTest.java | 2 +- Tests/com/drew/lang/ByteConvertTest.java | 25 + Tests/com/drew/lang/ByteTrieTest.java | 2 +- .../com/drew/lang/CompoundExceptionTest.java | 2 +- Tests/com/drew/lang/GeoLocationTest.java | 2 +- Tests/com/drew/lang/NullOutputStreamTest.java | 2 +- .../drew/lang/RandomAccessFileReaderTest.java | 7 +- .../lang/RandomAccessStreamReaderTest.java | 2 +- Tests/com/drew/lang/RandomAccessTestBase.java | 46 +- Tests/com/drew/lang/RationalTest.java | 138 +- .../drew/lang/SequentialAccessTestBase.java | 12 +- .../lang/SequentialByteArrayReaderTest.java | 20 + Tests/com/drew/lang/StreamReaderTest.java | 20 + Tests/com/drew/lang/StringUtilTest.java | 2 +- Tests/com/drew/metadata/AgeTest.java | 2 +- Tests/com/drew/metadata/DirectoryTest.java | 89 +- Tests/com/drew/metadata/MetadataTest.java | 92 +- Tests/com/drew/metadata/MockDirectory.java | 2 +- .../metadata/adobe/AdobeJpegReaderTest.java | 4 +- .../com/drew/metadata/bmp/BmpReaderTest.java | 4 +- .../exif/CanonMakernoteDescriptorTest.java | 43 +- .../drew/metadata/exif/ExifDirectoryTest.java | 112 +- .../metadata/exif/ExifIFD0DescriptorTest.java | 35 +- .../exif/ExifInteropDescriptorTest.java | 11 +- .../drew/metadata/exif/ExifReaderTest.java | 47 +- .../exif/ExifSubIFDDescriptorTest.java | 31 +- .../exif/ExifThumbnailDescriptorTest.java | 11 +- .../exif/NikonType1MakernoteTest.java | 12 +- .../exif/NikonType2MakernoteTest1.java | 2 +- .../exif/NikonType2MakernoteTest2.java | 47 +- .../PanasonicMakernoteDescriptorTest.java | 2 +- .../metadata/exif/SonyType1MakernoteTest.java | 2 +- .../metadata/exif/SonyType6MakernoteTest.java | 2 +- .../com/drew/metadata/gif/GifReaderTest.java | 8 +- .../com/drew/metadata/icc/IccReaderTest.java | 48 +- .../drew/metadata/iptc/IptcDirectoryTest.java | 109 ++ .../drew/metadata/iptc/IptcReaderTest.java | 71 +- .../metadata/iptc/Iso2022ConverterTest.java | 24 +- .../drew/metadata/jfif/JfifReaderTest.java | 12 +- .../jpeg/HuffmanTablesDescriptorTest.java | 60 + .../jpeg/HuffmanTablesDirectoryTest.java | 94 ++ .../drew/metadata/jpeg/JpegComponentTest.java | 2 +- .../metadata/jpeg/JpegDescriptorTest.java | 19 +- .../drew/metadata/jpeg/JpegDhtReaderTest.java | 99 ++ .../drew/metadata/jpeg/JpegDirectoryTest.java | 2 +- .../drew/metadata/jpeg/JpegReaderTest.java | 4 +- .../metadata/photoshop/PsdReaderTest.java | 19 +- .../com/drew/metadata/xmp/XmpReaderTest.java | 170 +- Tests/com/drew/testing/TestHelper.java | 2 +- build.xml | 192 --- pom.xml | 67 +- 355 files changed, 19034 insertions(+), 5872 deletions(-) delete mode 100644 CONTRIBUTING.md create mode 100644 Resources/favicon-16px.png create mode 100644 Resources/favicon-256.ico create mode 100644 Resources/favicon-256px.png create mode 100644 Resources/favicon-32px.png create mode 100644 Resources/favicon-48.ico create mode 100644 Resources/favicon-48px.png create mode 100644 Resources/favicon.xcf create mode 100644 Resources/metadata-extractor-logo-square.svg create mode 100644 Resources/metadata-extractor-logo.svg create mode 100644 Samples/com/drew/metadata/XmpSample.java create mode 100644 Source/com/drew/imaging/bmp/package-info.java delete mode 100644 Source/com/drew/imaging/bmp/package.html create mode 100644 Source/com/drew/imaging/gif/package-info.java delete mode 100644 Source/com/drew/imaging/gif/package.html create mode 100644 Source/com/drew/imaging/ico/IcoMetadataReader.java create mode 100644 Source/com/drew/imaging/ico/package-info.java create mode 100644 Source/com/drew/imaging/jpeg/package-info.java delete mode 100644 Source/com/drew/imaging/jpeg/package.html create mode 100644 Source/com/drew/imaging/package-info.java delete mode 100644 Source/com/drew/imaging/package.html create mode 100644 Source/com/drew/imaging/pcx/PcxMetadataReader.java create mode 100644 Source/com/drew/imaging/pcx/package-info.java create mode 100644 Source/com/drew/imaging/png/package-info.java delete mode 100644 Source/com/drew/imaging/png/package.html create mode 100644 Source/com/drew/imaging/psd/package-info.java delete mode 100644 Source/com/drew/imaging/psd/package.html create mode 100644 Source/com/drew/imaging/raf/RafMetadataReader.java create mode 100644 Source/com/drew/imaging/raf/package-info.java create mode 100644 Source/com/drew/imaging/riff/RiffHandler.java create mode 100644 Source/com/drew/imaging/riff/RiffProcessingException.java create mode 100644 Source/com/drew/imaging/riff/RiffReader.java create mode 100644 Source/com/drew/imaging/riff/package-info.java create mode 100644 Source/com/drew/imaging/tiff/package-info.java delete mode 100644 Source/com/drew/imaging/tiff/package.html create mode 100644 Source/com/drew/imaging/webp/WebpMetadataReader.java create mode 100644 Source/com/drew/imaging/webp/package-info.java create mode 100644 Source/com/drew/lang/ByteConvert.java create mode 100644 Source/com/drew/lang/Charsets.java create mode 100644 Source/com/drew/lang/DateUtil.java create mode 100644 Source/com/drew/lang/StreamUtil.java create mode 100644 Source/com/drew/lang/annotations/package-info.java delete mode 100644 Source/com/drew/lang/annotations/package.html create mode 100644 Source/com/drew/lang/package-info.java delete mode 100644 Source/com/drew/lang/package.html rename Source/com/drew/metadata/{DefaultTagDescriptor.java => ErrorDirectory.java} (51%) create mode 100644 Source/com/drew/metadata/Schema.java create mode 100644 Source/com/drew/metadata/StringValue.java create mode 100644 Source/com/drew/metadata/adobe/package-info.java delete mode 100644 Source/com/drew/metadata/adobe/package.html create mode 100644 Source/com/drew/metadata/bmp/package-info.java delete mode 100644 Source/com/drew/metadata/bmp/package.html create mode 100644 Source/com/drew/metadata/exif/ExifDescriptorBase.java create mode 100644 Source/com/drew/metadata/exif/ExifDirectoryBase.java create mode 100644 Source/com/drew/metadata/exif/ExifImageDescriptor.java create mode 100644 Source/com/drew/metadata/exif/ExifImageDirectory.java create mode 100644 Source/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java create mode 100644 Source/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java create mode 100644 Source/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java create mode 100644 Source/com/drew/metadata/exif/PanasonicRawIFD0Directory.java create mode 100644 Source/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java create mode 100644 Source/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java create mode 100644 Source/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java create mode 100644 Source/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java create mode 100644 Source/com/drew/metadata/exif/PrintIMDescriptor.java create mode 100644 Source/com/drew/metadata/exif/PrintIMDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java create mode 100644 Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java create mode 100644 Source/com/drew/metadata/exif/makernotes/package-info.java delete mode 100644 Source/com/drew/metadata/exif/makernotes/package.html create mode 100644 Source/com/drew/metadata/exif/package-info.java delete mode 100644 Source/com/drew/metadata/exif/package.html create mode 100644 Source/com/drew/metadata/file/FileMetadataDescriptor.java create mode 100644 Source/com/drew/metadata/file/FileMetadataDirectory.java create mode 100644 Source/com/drew/metadata/file/FileMetadataReader.java create mode 100644 Source/com/drew/metadata/file/package-info.java create mode 100644 Source/com/drew/metadata/gif/GifAnimationDescriptor.java create mode 100644 Source/com/drew/metadata/gif/GifAnimationDirectory.java create mode 100644 Source/com/drew/metadata/gif/GifCommentDescriptor.java create mode 100644 Source/com/drew/metadata/gif/GifCommentDirectory.java create mode 100644 Source/com/drew/metadata/gif/GifControlDescriptor.java create mode 100644 Source/com/drew/metadata/gif/GifControlDirectory.java create mode 100644 Source/com/drew/metadata/gif/GifImageDescriptor.java create mode 100644 Source/com/drew/metadata/gif/GifImageDirectory.java create mode 100644 Source/com/drew/metadata/gif/package-info.java delete mode 100644 Source/com/drew/metadata/gif/package.html create mode 100644 Source/com/drew/metadata/icc/package-info.java delete mode 100644 Source/com/drew/metadata/icc/package.html create mode 100644 Source/com/drew/metadata/ico/IcoDescriptor.java create mode 100644 Source/com/drew/metadata/ico/IcoDirectory.java create mode 100644 Source/com/drew/metadata/ico/IcoReader.java create mode 100644 Source/com/drew/metadata/ico/package-info.java create mode 100644 Source/com/drew/metadata/iptc/package-info.java delete mode 100644 Source/com/drew/metadata/iptc/package.html create mode 100644 Source/com/drew/metadata/jfif/package-info.java delete mode 100644 Source/com/drew/metadata/jfif/package.html create mode 100644 Source/com/drew/metadata/jfxx/JfxxDescriptor.java create mode 100644 Source/com/drew/metadata/jfxx/JfxxDirectory.java create mode 100644 Source/com/drew/metadata/jfxx/JfxxReader.java create mode 100644 Source/com/drew/metadata/jfxx/package-info.java create mode 100644 Source/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java create mode 100644 Source/com/drew/metadata/jpeg/HuffmanTablesDirectory.java create mode 100644 Source/com/drew/metadata/jpeg/JpegDhtReader.java create mode 100644 Source/com/drew/metadata/jpeg/JpegDnlReader.java create mode 100644 Source/com/drew/metadata/jpeg/package-info.java delete mode 100644 Source/com/drew/metadata/jpeg/package.html create mode 100644 Source/com/drew/metadata/package-info.java delete mode 100644 Source/com/drew/metadata/package.html create mode 100644 Source/com/drew/metadata/pcx/PcxDescriptor.java create mode 100644 Source/com/drew/metadata/pcx/PcxDirectory.java create mode 100644 Source/com/drew/metadata/pcx/PcxReader.java create mode 100644 Source/com/drew/metadata/pcx/package-info.java create mode 100644 Source/com/drew/metadata/photoshop/DuckyDirectory.java create mode 100644 Source/com/drew/metadata/photoshop/DuckyReader.java create mode 100644 Source/com/drew/metadata/photoshop/package-info.java delete mode 100644 Source/com/drew/metadata/photoshop/package.html create mode 100644 Source/com/drew/metadata/png/package-info.java delete mode 100644 Source/com/drew/metadata/png/package.html create mode 100644 Source/com/drew/metadata/tiff/package-info.java delete mode 100644 Source/com/drew/metadata/tiff/package.html create mode 100644 Source/com/drew/metadata/webp/WebpDescriptor.java create mode 100644 Source/com/drew/metadata/webp/WebpDirectory.java create mode 100644 Source/com/drew/metadata/webp/WebpRiffHandler.java create mode 100644 Source/com/drew/metadata/webp/package-info.java create mode 100644 Source/com/drew/metadata/xmp/XmpWriter.java create mode 100644 Source/com/drew/metadata/xmp/package-info.java delete mode 100644 Source/com/drew/metadata/xmp/package.html create mode 100644 Source/com/drew/tools/package-info.java delete mode 100644 Source/com/drew/tools/package.html create mode 100644 Tests/Data/withTypicalHuffman.jpg create mode 100644 Tests/Data/withXmp.jpg create mode 100644 Tests/com/drew/lang/ByteConvertTest.java create mode 100644 Tests/com/drew/metadata/iptc/IptcDirectoryTest.java create mode 100644 Tests/com/drew/metadata/jpeg/HuffmanTablesDescriptorTest.java create mode 100644 Tests/com/drew/metadata/jpeg/HuffmanTablesDirectoryTest.java create mode 100644 Tests/com/drew/metadata/jpeg/JpegDhtReaderTest.java delete mode 100644 build.xml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 1a8664e..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,30 +0,0 @@ -You want to contribute to _metadata-extractor_? Great! - -The easiest way to contribute is to provide test images for the [image database] -(https://github.com/drewnoakes/metadata-extractor/wiki/ImageDatabase). - -There are several coding tasks available to be worked on in the [issues list] -(https://github.com/drewnoakes/metadata-extractor/issues). If you have something -else in mind, that's great too. If you want to have your pull request merged, -it's probably best to discuss the idea on the mailing list first. - -There are a few simple but important guidelines for pull requests. Code not meeting -these guidelines will need to be amended before being accepted. - -* **Keep your commits short and sweet.** Only change one thing at a time, and clearly - identify the change in the commit message. See the recent project history for an - idea of what this means. - -* **Keep your PRs short and sweet.** If you have several features you wish to contribute, - split them out into separate PRs. - -* **Match the existing code style.** This include things like brace placement, indentation - (spaces not tabs), and so on. - -* **No 'churn'.** If your IDE changes lots of code automatically, turn that feature off or - use a more friendly IDE. If you think a wide-sweeping change should be applied to - the codebase, please discuss that on the mailing list and, if agreed, it will be - made in a single commit to all code. - -The goal of these guidelines is to make your contribution clearer to read and review for -all, both now and in the future. diff --git a/LICENSE-2.0.txt b/LICENSE-2.0.txt index abf4a4c..5715329 100644 --- a/LICENSE-2.0.txt +++ b/LICENSE-2.0.txt @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2002-2015 Drew Noakes + Copyright 2002-2017 Drew Noakes Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 6975a75..780ebb7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,35 @@ - + [](https://travis-ci.org/drewnoakes/metadata-extractor) +[](https://maven-badges.herokuapp.com/maven-central/com.drewnoakes/metadata-extractor) +[](http://issuestats.com/github/drewnoakes/metadata-extractor) +[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=TNXDJKCDV5Z2C&lc=GB&item_name=Drew%20Noakes&item_number=metadata%2dextractor&no_note=0&cn=Add%20a%20message%20%28optional%29%3a&no_shipping=1¤cy_code=GBP&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) _metadata-extractor_ is a straightforward Java library for reading metadata from image files. - Metadata metadata = ImageMetadataReader.readMetadata(imagePath); +## Installation -With that `metadata` object, you can [iterate or query](https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted#2._Query_Tag_s) the +The easiest way is to install the library via its [Maven package](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.drewnoakes%22%20AND%20a%3A%22metadata-extractor%22). + + <dependency> + <groupId>com.drewnoakes</groupId> + <artifactId>metadata-extractor</artifactId> + <version>2.10.0</version> + </dependency> + +Alternatively, download it from the [releases page](https://github.com/drewnoakes/metadata-extractor/releases). + +## Usage + +```java +Metadata metadata = ImageMetadataReader.readMetadata(imagePath); +``` + +With that `Metadata` instance, you can [iterate or query](https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted#2-query-tags) the [various tag values](https://github.com/drewnoakes/metadata-extractor/wiki/SampleOutput) that were read from the image. +## Features + The library understands several formats of metadata, many of which may be present in a single image: * [Exif](http://en.wikipedia.org/wiki/Exchangeable_image_file_format) @@ -17,23 +38,37 @@ The library understands several formats of metadata, many of which may be presen * [JFIF / JFXX](http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format) * [ICC Profiles](http://en.wikipedia.org/wiki/ICC_profile) * [Photoshop](http://en.wikipedia.org/wiki/Photoshop) fields +* [WebP](http://en.wikipedia.org/wiki/WebP) properties * [PNG](http://en.wikipedia.org/wiki/Portable_Network_Graphics) properties * [BMP](http://en.wikipedia.org/wiki/BMP_file_format) properties * [GIF](http://en.wikipedia.org/wiki/Graphics_Interchange_Format) properties +* [ICO](https://en.wikipedia.org/wiki/ICO_(file_format)) properties +* [PCX](http://en.wikipedia.org/wiki/PCX) properties It will process files of type: * JPEG * TIFF +* WebP * PSD * PNG * BMP * GIF -* Camera Raw (NEF/CR2/ORF/ARW/RW2/...) - -Special camera-specific data is decoded for most cameras manufactured by: +* ICO +* PCX +* Camera Raw + * NEF (Nikon) + * CR2 (Canon) + * ORF (Olympus) + * ARW (Sony) + * RW2 (Panasonic) + * RWL (Leica) + * SRW (Samsung) + +Camera-specific "makernote" data is decoded for cameras manufactured by: * Agfa +* Apple * Canon * Casio * Epson @@ -46,57 +81,44 @@ Special camera-specific data is decoded for most cameras manufactured by: * Olympus * Panasonic * Pentax +* Reconyx * Sanyo * Sigma/Foveon * Sony Read [getting started](https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted) for an introduction to the basics of using this library. -# Mailing Lists - -Three mailing lists exist: +## Questions & Feedback -* [metadata-extractor-announce](http://groups.google.com/group/metadata-extractor-announce) for read-only announcements of new releases -* [metadata-extractor-dev](http://groups.google.com/group/metadata-extractor-dev) for discussion about development and notifications of changes to issues and source code -* [metadata-extractor-changes](http://groups.google.com/group/metadata-extractor-changes) for automated emails when code, issues or the wiki are changed +The quickest way to have your questions answered is via [Stack Overflow](http://stackoverflow.com/questions/tagged/metadata-extractor). +Check whether your question has already been asked, and if not, ask a new one tagged with both `metadata-extractor` and `java`. -# Credits +Bugs and feature requests should be provided via the project's [issue tracker](https://github.com/drewnoakes/metadata-extractor/issues). +Please attach sample images where possible as most issues cannot be investigated without an image. -This library is developed by [Drew Noakes](https://drewnoakes.com/code/exif/). - -Thanks are due to the many [users](https://github.com/drewnoakes/metadata-extractor/wiki/UsedBy) who sent in suggestions, bug reports, -[sample images](https://github.com/drewnoakes/metadata-extractor/wiki/ImageDatabase) from their cameras as well as encouragement. -Wherever possible, they have been credited in the source code and commit logs. +## Contributing -# Feedback +If you want to get your hands dirty, making a pull request is a great way to enhance the library. +In general it's best to create an issue first that captures the problem you want to address. +You can discuss your proposed solution in that issue. +This gives others a chance to provide feedback before you spend your valuable time working on it. -Have questions or ideas? Try the [mailing list](http://groups.google.com/group/metadata-extractor-dev). +An easier way to help is to contribute to the [sample image file library](https://github.com/drewnoakes/metadata-extractor-images/wiki) used for research and testing. -Found a bug or have a patch? Search the issue list and if it isn't already there, create it. +## Credits -# Contribute - -The easiest way to help is to contribute to the [sample image file library](https://github.com/drewnoakes/metadata-extractor/wiki/ImageDatabase) -used for research and testing. +This library is developed by [Drew Noakes](https://drewnoakes.com/code/exif/). -If you want to get your hands dirty, clone this repository, enhance the library and let us know -to pull from your clone. Ask around on the mailing list to avoid duplication of work. +Thanks are due to the many [users](https://github.com/drewnoakes/metadata-extractor/wiki/UsedBy) who sent in suggestions, bug reports, +[sample images](https://github.com/drewnoakes/metadata-extractor-images/wiki) from their cameras as well as encouragement. +Wherever possible, they have been credited in the source code and commit logs. -# License +## Other languages -Copyright 2002-2015 Drew Noakes +- .NET [metadata-extractor-dotnet](https://github.com/drewnoakes/metadata-extractor-dotnet) is a complete port to C#, maintained alongside this library +- PHP [php-metadata-extractor](https://github.com/gomoob/php-metadata-extractor) wraps this Java project, making it available to users of PHP -> 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: diff --git a/Resources/favicon-16px.png b/Resources/favicon-16px.png new file mode 100644 index 0000000000000000000000000000000000000000..e8b96b53a0220c3da61e025a31b6351a1ea20bed GIT binary patch literal 590 zcmV-U0<ryxP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004b3#c}2nYxW zd<bNS00009a7bBm00066000660fB>5@c;k-8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H10n155K~y-6jg!4=Q&AYkf6u+QktEbE>X6nB4kc0%oZM<M*g>gu zb?^_6c99HX8U$NvI@IFi<m4v!v8a=pt00Jo6cnTg6)A%81DlX~HHr6iXj+=yVDe1w zdEfK=&I9Kh(Lvd4_LLcyfl<&$$rjD5A2e-73Rrd=@}*Mgg?5`Wnam{UekbrQ{gpW3 zMEfeXiZW>4jMW{pw_poE1#r!{)&>|KKbJIPcynb%-+x+=?BdqkOjEw^Uy``7x#pM$ zFfx&wlv(i;3=hyKB<kf^V58-$Z2?@@+IkW(@9>%2$uP8gfB-77)I2bgsDIfu{!gA+ zI0uqJS;m6Uuk}5H!K-@u4*0zQGzuW&=E}pR>%E-X1^`<H9Yvf?pU%Ar39CCdY&1Ui zCF|b;Al^BFoJE=40uGt%u4sQ6)!0xVM}@@iud|&14rY71q>NX*!K*dzK(X0u29X4U z=~n$N3^nCy`}*3no*W8ZKfe7w0sv9~!XA8hwDi17nut7!Wi!T1IzKXTAylYpH&d;# z_A^=k9sqIA6y`DK8ODH6p2+u#sv0{O2zMq?FVDF@R$i{{OCNuN=?JhNaC=DJ_s8uY clQpjV14#v~Z7Kc>xBvhE07*qoM6N<$f^jbZ%m4rY literal 0 HcmV?d00001 diff --git a/Resources/favicon-256.ico b/Resources/favicon-256.ico new file mode 100644 index 0000000000000000000000000000000000000000..abcb5576c05ae2ddc411faa952304b99b7965202 GIT binary patch literal 9979 zcmajFXH-+q7cP7f2py!02vStK5I~x=fI+EBM?g9V0#b$03B5=a=^dm6q(~J6A|*;k z>Agsi8hS5p`2F7xcdh&3X05Db_L<pd_MX|#v*+vs03ZM#xcPtpR>1cz06fF}heH3? z4u=3hJ5EMP`M)+k7ywk*0DzVCf9*0n0C;&H03y_1JSQh%B*6_OS5ky)-dw%;5)t5j zUb_^(0f4)3CAgfnN5<BSw~vukCU$oKJE1gLqj1L{=9j`?(t`pospryANcv``i(M0* zM@q&n2}Q97EaY=%P-L2?=bhkLTF^qq?c#i47%bIT;?dgdTkecIro-%m!HYmX8tJRA zjO{XR-G4qgsp9@MVY4~AcJ!+G+BSXU&?nv9|Gdn42A@Vr*t)-rS=P_Th*B|==rD&I zt;Cw3<Mx%P7VVKkKVI7X!Zba66Mv?pd!v6BTnX`zgV*89?P!XoZylff<SDTfl2C#d zg0%wP%AvKXW(&HV*)>a1GyxV0#X1EqgP8_z*+c43Uq>X2)kp4)z#Y}1H}sp_w|Hf) zRV^O2;H9+nW=S{kC-)S<G|~F#653Pj%S0n~DbQYVxJ)4`)~b5qT_;n1#*zO(KLH{J zmG!9YedM+D6n|Sas_gb}RW-G~P!YS)O}-U|0!s*`W|yT4wNpfRMujqo__ygh+Qjhj z&)ovruD8leTCcvjSTjV7j*h0uUhT%Ng8k|6dubMAJ;Q$!UQ`pMvO4uNH8o}J#O0dm z0=a({($YkG-T6;G@mw>f{FD4ml?Czy0==szHs8#d0Ux_86H)1ZGwM)(z)TyGLkmk1 zI+xxf(*vUwCfC@OA*L=8R(!_0cV9Y}uQLw51aqGDYXOi(?PbE5nP}eJ`&<Yl&T|Sn zGQ4dg9x2SxpLjrVbibPd&fC6@nnUI_TwV8vS?39KnpJ}t=RBXSLE>0OM`N1257$Sg zRHR?=BzpN%?mVQD=|{6$(_Y^FcWi5C=l|X7aJ^Y3=oQE#PPyO>aIKOU3({%Xr|r@m zG(=*pt0$y8XFYM6OTK0J(VavQm5-kv%O#E}EX7PtI)CWV$O@_gbLCG|Tfd|ToW}+t zdH?Rko+zR&D3&nW+iu4~PQ_4;>NkB)T2QF<4LP(9Rn|lDRgpo+g#8f<SKG1ugEFA| z>Bp`mI#w}|mQNUW0%vAI>F5{3<BI&#zAs<CM2O9L{CyJV2>wYcVW{|)gDB9h+q15d zB&XwurFxdXZK9>xW012=O{-A#)ZN;8wME`vODNSI_(pirL;Z%dRAx{?L_}nc|K}mT zE>Lj4%W_Wz<>DGuR338n=--@W^#o_wY*~AO=1ev*I4K;OEA_igDbbkfa>HKqd_yzF z0-_GS{o3h=v-k1#rJ&*mEFaq+fwnQB)ZRyAE*ajlo~xM+jwb%rA^o5BCh)wBMAMtA zweenoX!W&*Hm^CTrwPALA(Eh)%U8~SQ5E9vy{3jAdlxG1u<pEw{VlJOXcGP}v7mu( ziX<l-=L8+rdhkd~b?~$!OrYycwE#y#*8uW@l3`U+JEi1Ly5g^Cw~f(e_1;SZ5UmnV z;#nw<|AyrA;0ul5+VhGKXvpcURm*Lgfif}-iRtF&67E{{VD~vWRfX||jMt40GmRph z<EeCLhMKy%(eJyM(QKsOFoIr7LVJQUFBum#=a@zTUZjRHM}iI>0^?kT<tt=g)?T)l znb~;q^J~H@;}`=1@ZSB+cE<}P<W<?XZ>JYkn;j&kw797w0(gB_p^0k2X{>{UY3b>* z&~;y0VfBZK1)6%LF6me%4AUYx)f+3J)U5ccXS_%%8(UikkS|v`VlR~|Vb#v=r?1WD zdb%muCGf+9t^|bxhx46ur$0l8t;zk?=VwjS1>w|U=Xw`<0C`I_LqaT(@C(VUnlWRa z-OeJCOX;IleUSFDBwC`yl|fiae{y{M#DsLV@sq|PYpSX-(N~gl<J55V0>pH~`YqZ$ zDNs6y%xtAMwU)Ksg2j)9jeU4)YV7(@zOEL`&~~!>w>4&F)Q(+gRZLX0#j)W_8L*ck z?d_h4nwhs?FYT5<+->hQ{sQd%!`|9PZ)=Ffb5v`V49C?QPE~EOrlqABDjfLDNknl6 zj@jCxik1y2Ywn<b!-E6q)uSSiG)LQ8=dW7?CduZBaNChO&g87$OqHs+4uQ=&mbyCm z+Z1~d*ayTMS<Qi>xWCe1qDz~7Dg|DFnDR>A9(l3~<NRDWu6@Xhi;m~b*=;2A&>!S2 zGz?lK?B6#I$Lg;R<69jdl_o-|Qfk4UaE|{J8E)*nRG|;Lm{=aqD~_ly#9%P(<@UAU z@s~O>+mb<o4>=;>i<P87D4&PK2`eir)j_ZgpH;4pGI4W(N0B>`sBO!6tmjA6cNKVH zP}a7j<*!YMCw<NB19B%xh2atxCMOh+b+}U=Hm)<Ly9kNBck+!6pv3n|#gbXn;CZMA z`<IoM`@L_Nf+ch<5u>)Hs4nrH_!Cb}Q{JJb1@ba~GP(9z^dt++i40?KXw29^Dkn>v zp;H)DFgQ3p?X%M8ws8g7Ou3CLEF5NGpWK;(-3Hf3A9cn@M@JWZc;G;};KGwbr0r=0 zcO=;EeN!#@tU4BFhHwyTgJ7ehe-hTRdcy&q_XdISez~@|Mzsyt*`lV#%JlcW1l^OA z3uGAZrysTzvj{S~ka%(KB$_u{i6tr<<#aQ{H<O%o?rm)MXW;1q8a}<p4pcJPU6y_Z zo6G~L!q(H<Gv4%|F<S4rtamgEL#e7O*^$kEErmS!8oXfw+(#`c#T6Bfb4$^8#aeWB ztwGukl~=FoOBzX)cNYj>HG3V-+KcW(s8;X{A_b*9MB<&)3Ql(LaIo}l!v1do`?Nvi zWwN3hj<NVEhmhd31LizR)Fw6C%*;$}?yv<enmTh#_zuObDn*D~s+$@cMHAgqVptQp ze2ACM3z&;Mv_&9v#sJQz1kg@I47fPlk3FHwo^K3^!(UMy2Z1q<hdA`?+!(76m^ynQ zIyFvF#y93+abKx;2MoM9p-^Wyl@m?(EuPMQkpgXDUea5>SI+=%sLBYD$hoihV0szN z(k;1NtV;Mfj}!)9_qFCiw~h^K`}X}rm+Do_^5$hjTx2jTk90&!*3DTg5N>Yo=oSoE zEMf;ca8A-xxDBJEJOfiMgvw`Ojd^hH<DR!}!LX38p8S_Bi1=|NcvAj561gIJto<Go z9^3-dsiq1o%gnJ+6CWGtfafGg4s19Qh<}8=Y~7NS1FiFZ1K^2qCNu&FcXxNMY(Lx` z4<jr45i#=NPGMLw0B1LqX-CBdKc`Mf={Q*Y`eHx!_QJasS2DTMQkv^|$&T!|v=n&m zs%ei)Ha`@chEwLXw&q7bYt2b>Dk`uZb9X{HT8(b9krpkcxSr+FlYVvN^RT9pq6}@3 z&ADoBIzxb6XaW(M3$c<35vd`+EBc+}`@H1O!2^vs;+UNWPQwWvPc2<K>0$gor+lp$ zFmL8SG4$K%h48jSf%Lwk(!K9krl;a{=A^I(MCW#`#(T{3HjHhmVZ=r<@#_8q7(IIs z<=*j?#A@AnztaOM`Q$bAZ?E}-Pag!QQZVXOLE;o2Wa*M)Wo>k3HYD3wk4`9{pKzE& z={NA8*A5uL&CkflC_N+p&Pf86kD~?tc(`5%rxT5~Dre6KOGK8a(-{G#4^4RtH?v4C zW<Qks{=8R`ws{uRB`&59{(>pNm)P?48~I~$>&P3&gIXop>ry(SV1JF3Ul<RXKJdSP zPD9z_&d**1-pLzgdkh@Hj7-pi#I$gj+k8nFk2vSl-9KYS4W6BUeiBX<;O+;H=SDVq zzQZ~<xatJn+Fv?afr%@bK8O|ZO<x?%l0jTeuj)NAkR2z`1sDDa{sw$DGd@?PJEhIL zo&Q}+19QUqH==*dC7qZ-nDspbdboI`g*rPG&(q_{kl7JG&Z96B23Z`D_UR+nZuiMn z$jprDB>sIEoDNg=thfqDMfdvL9iISc5&PUmKK?vqu>NN&mN1|k^tRAPfL8)aW7At7 zkcf<sbcCqORy5bj%(N)QTY>%8xI_E!RTX%>L8>oJ{Jne|FU^9_1CEotFXYBm(b{ES z>CjzY-qn(?9mOE0htQB^#(Ee{=8(>f4W9~t_0>8fap!ueq8+t1@4My96e*Eff53_y zk|@SMTg>A6#2E(NRgoPOgsKYM`XJTjhRaWSobLuUuTAETCqm;Z+d*1_%6mWT73*uY zmzVU=VJcPELN`*XNMnF}txkYzkJ9E>cB3SpBonksuNNE_`rbQ?y`J8)Rc2xV8Bx3F zqi$$O<M!yC?4<v97BO8gM;+!hAZ#FMyN<}*%PICLX9$q*9>rJXi~a&UdGh+Yy#DIm z!Xo=Jv9KWxv`V-TT<pKs*BO2lYaS8-+}uQH9bBwZ?@zNdX&xFu#0#Rio6my`O*$&Y ze-fGTP}yD$-O(0Kn!`WiUdSWMrH~R{B?6-Hyw8I??rT~QfG^AB6v938FgTMxM}Gz^ zS<biR#J^~pv3Dz@;krK{gM)u$TIF9HM~C88<8vShPSKTL2sY`7YR5!dPDF-02@K!i zc?rK~+P~@$IU4SEdYDnq1#dugPuxTPO(!%Q69<9*4XpbMA1S=LZ~f4e-Ru|mmrjow zde^F(`o;kVd-Ui#kKPH>mh)MsFnq-+vGpWnE!rE&ZE+xZLmI?ULvV>4>$CH|rLKfH zO-#crnmS_*0AJz_-s$^6g;OcNS`1DRUG3q-OZNPwNld~6(h(bwIM$U2O6`8H4SIk& zYemj%YALt#ler@h(z7EYwIvz3s#Z_F<6O*y%HH`@@R6(uYNIN_qzjP0Z1&{;)z#9b zonm&2Fp9gjodg#D@{~j~w+uf@k5KQ-y2e<##9xeM_fDZAtp~x9hG{j402RI@e1c<f z7}4;f0d>XPBAn9wz7jY?M7QFb<qbeBaw`=Eo18{uvHI@&ye<0-G!TE8F##``@Sj}y zM<2ueYOOD@*!*hYRmmtp4VQnNGTe^b&z{TqLmf;O?b1j2rq^yRXhWdVzL#sDZ(Lul zu-fr2kN8b4;;^h3xIP_Hxja>!KbG7!ow9dJzvaMrPA)3q31Lpa<8unloEenY?-jbE z1+hW~f^s}c=^odZKrWva#PUaST;@h|dwXfL&S0K)PL5<}f0pip|GH$bpz|WfX3~?k ztNB|CYGb!wV<kfco&UmXW)b1w>AmS|qcaqi64Md=v}B*zoI&!4BXG(m49XG1#Gac4 zD&9kXxb<jeMKVm#c}bo85CY5d%EppmIuh|x7bW4w@^cMW!I?+Gg>WXP3@qho_?}@? z|6Ix+=Y<x?D=!~TC`Uh2xA+{}-XHXf%R5@~6m92|j&9~Jy9En_nJRk_;Xbr#C;g_X z84|%eR|2JbqH|PH5yC~vHhL3~*=1a6<-8IMWwG9PKSzv{6EZG><K7Cd82bleX>oI) z@n}bcvniUeiw_`(EEc;<_5!h#PpQ21as?m$r6hL)0jn=?=+by&U%Zyp2P|c~BL0%Y z4s%<IxXU87SvCmpX}k%j{~WM!=lV|%<*Xg($Te_U@Lm7ycJH}BL8_&Ho4sroAF!;D z6PQk(SNu)iF7yymN7SodgEGX9aizBJII}&afq@Ij8V&~m#lIf8tsS}J64(el!om8A z6=&9+i~@D`%3{CSiz$1xoXtk<cJJfjI}(IDP0w47Id<#@^q)ODScm7WT#pRAJCDP1 zCElbN_z1S(6ADdX_OS0%ivDW?7@IZ!A6>FzwY}HQ?bAn<t#hpOjZICW#f{%y5qGrx z0INISIw2Dxc%dzz_tVvFe{$?`O^v_T8Dy+^2vICvO&xwq;(+sT?Wic=*sELfbsH&4 zYIXi0ojc_Y=UjZ=vj?wahfBxP)7}hF6YGK!x*2#YP^gZQSgE~VJuR+GJc-IP1>M%Z ztG|lY2eY}PSe>~6D->Fr`DB+$$C@O!!3bye2QNLoZN|-wnHi(IX*f;fTEr>w5}RMp zi!R4PJI;axrh(|MXP#RnIM!Hz)iCP<(mgG6cx8v5I)jJsgXwT8CQd~wsl)jZO9J6e zrLn9W!yH6>xVdNNlbG)Bb@FaSOTD4g1q<Q;0a{7m^!!^;iXyIm>JJj&^`b-DWf|;m z>?1PFuN=r=_2x=FeA#fB!h*e>p1{I<=MDhft*!U_9DL<$hGCIp3J>@W*%{VC9D>A> z1V2RF;|&P!F$92SUi!IPQW}MP9BKAytZkuu0E|xN!=PT$V-6HfkQd4e;LN;=#@|CM z6IrnLQ4q4*jDrDjvsJ-4@KtSxzUqNKF<ey+64&Q`!28-P3|t84f~hpsaalqxz~ioq zrfs7qxSux<2L9<+{iB!Lex$=d!wY#t7TEto8J$i=2jocF2Js?`l<s{gq*OxU@@y z7v%rU0Jvyz?Q*#Nt6;ntTnN<#vxzQ(0S||fyku?9&*NsQ_T)_*rVr<T7L(+d8BZ;< z`;_5?Woocj8gpNBB<65Az_QpP*hbMrOiN*#Pl18Pn`Y4TTO!OOsu+0F+kA!J=>|Sh z<`*`=sn+xW)zopE8EwnWfBw@cNN1jdk7J>xK++t)F>X?1wMt5-HoYMYQ2>rjuJY_Q zs9N;bp@oa%3rOqs+py<h_Z2T#a_SD8RUu!gcW@4}@mTB=oo~1LWzLiQh1f&L5~P~G z-}zT(>FzR#1y34I1hoV;zz@Zc^@-dc1l`5e-*}Sdv{?OUg2?FgLoucg)pbv;fq+^3 z;ZFjQf`t0SpSp4&>d6lNvUy){i`>h`k@I6O^dQi(g}e+O<1h%OTrz-lXLP3AS5ps6 z6s6(H%qPhyx4NQLZyFj1R>%#*dxUDN&-k%8Uks)^;4%Fk#)nzbQY5y-odmA(GjbQ_ zEC0(W)GxnRKrv)y<J8dmh*zNRqHBE;teod07QN6=BZkNKU0;E?#=BdIo6K$#s4bu< zE=-@<Jy*Nl-g+d{PTTb&m>iuCPRCnXwk5tAPQJgx+)EoPUjGvjDlYyC@NKHk=5;E= zQOg0>uUPj{g4Pq?)sHhR?Ko;7B!$Jbf)7)zwDJ8yY^GM0RT_P*1vzXfXpvk`#5DD; zh;S#1hHpp93h&)le0~aYM52JgqKa(#IPWklG$i4R;`U<JvWnQ*3v<;^I-ugE{)pa9 z6OWs-PPR$ifyA7~f7H?~(Go;!b9a&7$FIQmB$_)E&koS=SW4#cQow!<N%tWX<lvu< z8ZG4n9kv9Kz1-e3s#;2J(PE(CEO$IUDG9zFof|vWU2a*tA)CJ$&^|=?0+$cAGc3tr z{{|SHQ9>h#a5MYT@)_6U|6wt)yb-|TbuFA(KQ&|=#SP-*DLSPP{EU2Yga5pod5*`- z=0xW)&XO$qIIwPTK?(iFaRq*sr|qdxlg}!YqnxSx0ghHQ{vBriND+5c3@3$o%peFX z7JEUt()h3~eJtkF$NL>GK~x2k|G(HbHIT?m=Bb(LQb;2Jo86KOz_AN#_JEn4MPXp) zKQ%dqzF`b)5ko7lS=Ln&qa%dE_Cj%tQlfk?u#4-^;39s7Gmeco`+)hTRg60v2-50E zl(PBGsNUfH{V?TXQUInjK7_;>^*oQ~00K;ECOc$rNQU5C<CQdXK~gz`h+81K*r`z| z|FG9Y8cm!Ta12IQ<Cb84=I_~z^!xV>3t}P|+BOhT*)j8HSn{y+x6AXEr#N=UyG|!a z+s()~1)CISi(QM}<m~Pgrmy|Y@hS&gliDJTXLg3udl+S0`Yds4dk6E9^?QA-BvQ?e zejD!DhDeE5PxPD=+ZH2#w3U3;Z>}nns`@71)djSZgyjoz4!f|yJxa)T`^a-^xM->x zM<D%&%$SrPWrp?Jf~!mN@~^;zCd9h3^;=KHBZm1*DVurD_VK^2FgrGHb=lYD&mmYX zC~O=YWF(0W%;p*9^eMzQGm$vHu?|?v`bs->FNbhhA()w`aR|Xm<IjFJlx3wNwsP(@ zu8fN_n-sC@Ql2wr6SJs@fXqM@N8>7Jx?>PD5p7oV2Tbe-UXrpB<e=c-`4RcQ2yd(5 zaC!wgD1Ws_IOuRQD&p5CH0$l5hNd2t_LT9|V*H*BgB5@AIRZ1T09@P`9W8s))twMo zsP?K-kA$^L(TIBy3{IrdypP9)eTPQzEkc6kEScnuR>G5~snrZLdby7R<Va0>)(ww& z`1q|UEzJ2pJ&Ze!Y1}=Z&P3ZSlbEYQh-TVMXTjq&Zx&v}L6(WkR3XG8Z8>E1a=wjU zNI!oYmWwKZ<~Eo+jfdGPvR)^sx5^HtD3Wxhp(`Pko+FFK!36~*rmB#AV=zQo*KXSQ z_z}_dIbKu*YXxZ#gbrOOLW)qPtN}NDU`8|ibu_D-C$w`gu3dUdH5n<<Ah?nfNI?is zc}k^XI$jMtN4>1SCOd&wff+0)gf}Mww0YcOcuKk3CIYBr4J4B{ni&5vZ=f+La^z*i zy>{t|jzOv%)m5*A1E_AJlSb+t^hPAcbpkP;x*t&KOAjx=s+3K&1;+jveOp3M|2sw3 zRgpPFb=l=nmlbD^Wp&rf2`ZZoJC03!x9u7VJSW1tHTM`pI|mQ`n1oI-uBi3wY~$;; zf<com87%hFkd<ZHc3=y?R^x&0&P*N`Zc!R&p*Bavul9lz;;56yrZVJf5T(R<womo9 z?Xoc`?VIvS$7QcGj;&hmODnvp0RuKEQ%5pxdr&8H!DG89iQ?ji>ce{WjyP|u@f4}$ z4BU2Y#iRs9Hgc#B;~4SH#K%XtXO*bE*v{E{K?1JDG?>OMP>go6L#ygMg{eIEPr*X@ z=u@Yh&kz8*wRmdXkTEL&%^$Y7qZNSB1}n(oi~mXTH#pcl@!jT|e-qrI8EdT?a%U(F z)@2QZ*``bx7sVI`?Dt5N3(}ct3kQ6qM+XRQ4DHKOG*XKHXg|Spj8N!gPBpo7h5FoO z(g(|s0LW{ftH20vy8~fLK0snZkHW(I4O|%H2ysG3jmD(4%uLDh{EKESI`6koC8K=} zo+X_}-tyaV9dXR`=zXqmsBB7a^(yBv2PRaS$;LKKn)flpsrapHi;oL?d=u?~Bl7rW zB%EB5hR6!Ugu2OV=M7-#0m1=1bE#1gactJ$Tmj_MKAUDQ&AIK}=<vrMZ0|b}Kz}3n zE#&zfiQy=KfKAw-T)IHO$xVD6#t2X4$xFaX>;fkoJ0kdgUsp2<(+4G?uL(;ZC+}}s z*c#!=YJif$3wXJ_Y2g1|d;@UhI{^A$@l6kW8U_H6mp8>Xn}EJFLwju<+R)b8)tYvz zNZNO~kZ&Xf@N`q98fr7-tG6~0ub1)^g5>g*lyc<deHMwM1NJa@5gj(_?35oD-MHK$ z&DDNU<~?9H#e)|UX-~f8eblQjQxjsonz5!DMqwg!%XM_p?%Mw-)%D0K_<^8xe}<1m zujaf$oD$0N!(f?me)v7DL+!B<<}aWcL{nZ~Uhdl3)8xDP59`!&i6oF3%U{DYbP5bQ zWN66<DxPoXePs3dC^#xQn0Q%?AbIZ!Whe{y@@DpXBZ<x|-m{-JV<_dsvGgM|R!W-< zW`;OW>s%|PCCiSY|5UO02b)9`ZMZs<N$ei)_=lh$^C3-oi@Z*;-&|J(sD57}Ie#+} zyU-m>+@m4B^QzKRPVRv@3!zV!_Na;WY3GKTH$Np&O)8uO&VIzN`QRQbIIzE8GMz1e zYUlj2fHf>ZfY-r5j#GcPK4Fi6m}(wL#Hn-W9G3{wmHm)SzW{YoNi;XDQz^gY!Cihc zBndT5X%_rcIZROVwV|PR*{Zy=v$MPPYNn%{HHPb2LwtJg1s84O%%#yve`dWx@VkDs z1E<%mpq{TTMjd!xo9jK0FxH)V!7|s<((c5V7{d+k2*Sa}MyxFG1>x|v@TV2JZcEU? znM#^^%j?~{Ev5O;e6sf4v$He*{9tXkZHD*obV_SLTU-5~wCd+E^i@B-YX53`pRWzT zJv!N>*Ed?y?a=wZ!F7EiMbGLUjlqYxrKW$QhZWN4?^KP|$VY@p-Uk>fDOB$rS$cY( zzkRJr#^X!2^k`Na)-fA0u8{?N{r<yZ7n<5IfUfxytx~!5lCnN|xZP^8JYdc0?gyP^ z(!3~T6HCt^PBj-x`ETo;pVBIUy>3x8%}u|^)}F7=ObTDbSbl#0pb@|HQpQ;|0{h$g z0Rgk!dLH>HIubPeYV=>vaE9(^v9Y45tq_zCx2=ZVG{*wIGK=#lsvOWRd=#0SQr~yp z529QP4f#6w@LttBZkgl57z=(0?oNP)DW8Xs<<0J(73-8BPcXqtWpf7BO<L{W&OyZw z*~pwGxVgGfoXVre2*HokQEj*Qclmk}j*R>!LW>9|<B2#44DK@~{Zy=dXBVOs*ruvO z)6M(E9_+v|{<67F^h=P++niGBGAg=_`gP{#d&;~|VP;X&8Z+7OlncFQE58;#SNtN% z=1k=vL!ul&o0fFZp(s8Q;R`K>);e!70d59lwaHeQMto)$&w6k%U-QTKZ?`cObPM~I z!<z;)jx)CK!xBr{D1}(!5n{&&?eMYpPlh!?yACxRFJdF6p%sy@;mpb3s|0B-4Z{G# zkA>yg2Prmhcfa7B3jKbf>-Fr!>J!uPtPQ@O_CYpW(k1T^V*O%p0o6fnYNr&%{73rT z)0)MBOUeALsn1L*aO-cIyIDsfbuE|PHplOZg0(GROo%D%nkNU8AHvFi7VndP{v+X` zN!^YVeF9PN40ynVFQZ77A{?V(c69Kk`_bpYV8O`+`sB9-iH;<j?gIA@^O$~EZSBCI zlM)=$H7}PI@9F41SE-_u2)3mww>CETEdLwt<JzA`kI$YZuGDDGUShT_+FbwI`+ASo zunWwyybrjvcs4F+`RrNPv0MBKH`>=#^cHNpV}g@CDT(U5L+T;X$GJ3_$8%#7W5I;; z>nyS}{b{?sVzxcDN(;Mh$#7ureEjM1EH&Hl%?nSzY{5Df^WJF9=h;(hEv>|lGi7Im z+!r^S>z+H}RU;2R{d&J}U7zrG-`8NGDK@te9w#&?Wy%D#-}#JL%Bk3$wz~#RUi_US z;eT8F|G;6~YZCzFe>lvte{F_)BckU9hsC3#6PoR`G#D`c1I_jE4~F^I<3=ec9%@o$ zOxp^L%ay!$+qo6z9#3Y@h;L~7WO#9DX^G>csyHF7lr=n@mi99d9e3a^UV5CDNW`(5 zjX~P1>7FXqb8i2lA8(cEw;b-s`sq$`fX3kSF8aPQmdEd&k-Tze+AGjp3vo{uPVIg0 z&yRD5|1wd00e;R?7b#>j8#TgQU2jSXgE;jyj1n-g$R<Cw@Nl(KnDaM?+gu!-R@G|3 zntW`i`nHln2o;J?|D?Z5Lw0MB>%sc6zG?n)={wNtu}Lp#LF`WIbQiS78wv1K4NE~2 ziv1@<uU=;9ixYm7)5io3RvbzP{V4T1qc~{>9+R;CUFmKw*l5m}BmfK{%nBK5+5OGL z#HEsA_K#(bA8yQnS!X2zG89s0z!a&Q<u?9|Bjf6<&qF$}71fyl#T)OY&ry2cf0T+~ zVO10M1o8NRYr2>GWFpJ9=Vy~OSxh9xvMrY~U_cXSyXWIkbMRdyEnF(XN*JU1L{`X| zO}p4uOzTV4k+A;tJ-~T@c13XY4hE^7_3Jni#VE-@gy7H+82BcuYh1pi!r~veMpVh- z<gGY+{Bw&XL8SZwPYLqn9rqP!!~bIsu;Q+>{<jBQy}S+p0MGSi54;L#PLj4a)L{tC z#-0gOv8fH$)PhPP)?v{dq1tuhG4Nbjc@nj^#o7;^sMBiufdlPl85kJakd#J%Fg`wB zkV42uN$7hbvK(sSB6fvhr4R#=7YP_=9~pw5!!6D3o1^_rna4h>-r47_{mt?;jEszv z@1pK#EsH~elYmlISp)inp`jr?fI6asO-F)|k&qq5_hE<dUp*Q!RX5fnUGRGc&c2(+ za~o+SqAV(>#d?XJc=ClF_S}9d>m7rs6I6mHZ|x0oLn%>&in->UxBFBmu2$D7_=NC% z9s75A(i}m%CLW9TdCwZa>E!rywkN*qd+VYAHz|XAsv;ry%6KqLj>DF(|HBTo4P_0> z$%K9migR5i&_n7Tfqt2BN1tis-*{DygjU1k6wqR#8@p<MZ*PV`EUpX-vUopBwOet{ ztXNPszEIDOc*1fRXvrL1ow#7A-YQ|paKBYAroQeUD!**ZP$H7~&YUBhGo_FGZg%Y| zy>~UHh%@N!hqqK&(~-jcP%4pra(46K<v$sA%h!@lJ0~Z_RqrVlL`(-pFJSzYj<$U) z668Xwe|7?JnVr-VnKG2g!dYE1tFG@t@)XES+U+3d9^K`4vcKg`l&CKuI}Vk*1cQHt z5Hu&H;J`%K(xv>bYcCZLrLoiQP^bCArq3H8tk9UIK9NRP(QIJWT*MAFa?#bJbD4j8 z6YfC_ls}Pjbei$1PUT=<tXsYN@%90`Vi_uTJ?tfu_jzUH;fEYIS@Aqn{r*@h5nWOi zUi3(*8-$C#IpN+*=Q4b)kth79-7QA0HWyYOFat4NQi}t9TiSrndkEV|FMAPwUQfV| z*s<%YL<*Lt*FnH1HkYfN<5K2$y|jQG3MS8EO7r#0c9EyQaam?MkGs%hrU{uSKC5~E zw-Zk~nd*6=!c(z}y4vGhlcFJ65hC=wboswjS^pCc)WrOGJOqwv=Z5RL`_|JeSRoU6 zp6@<#oAa_K1RZ=G8dlrr`L~Z{ws|@`_u}b2*`xjxG^Yst64G2HFT}rbG`U~=7Vp}J zq9@?oX(o9SmB4l7YNOK)PF}V$h-7WdI#K1QAJW?&D+P#sO)futmi>)rk53+v&gQ<_ zoXe)N{AsR?!mIlzdb7Yz`gf5mU2H8EC72kT>n`sRKXeV+`zZCQAAii||BTiD4}A;a Ar~m)} literal 0 HcmV?d00001 diff --git a/Resources/favicon-256px.png b/Resources/favicon-256px.png new file mode 100644 index 0000000000000000000000000000000000000000..660a6dd892c2c4ba4c49eb49be413cd72a72f0a6 GIT binary patch literal 7111 zcmXYW2UHVX*YzY2sx+x8JRl0v38EAc2pAMFC?$01RZyxDI-y8Wn$mld7LX!UkPty4 z9h5F5fE1+#=}q{D_xrO})?`lZoVjP;oW1v*L}@>Iz(9AI4gdfK4Rxe006@Tx5P*gX zJUsCzu?G*7o~jyX8t~;$V;c#c)4Hh}djbF-;`|MnuNA8XgO^^Y8NJYRwSR%J@~{Ii z7>vYI7iUi!D>pj{R}Y7bjk}it;0jU$se<;-T%Y#yH?zwk{~aJtX#A{IyJQmEsWzB= zz0mjW14TG2V=c?WxtYQ{HS;eWV~IE7)%%Xn*Xce!m%?V)Aaj|VB?Zz5M4Gw0_@9}l z!kL#WhxrG?=79nNHo#bu+-2Un{ov=%hh85O)>?AvMo(MLoH9ms{WH9Rj>{dUDOok7 z9s0|8lmq?EnANjrc5@jB8hi=*FF(@M5xjFb#>=`NS!P78QBIe3ulE0iR>8bgkoA-* zo4PU?>-z`aL`rRM%4;BtpnAbiRS0O7nZj;&e%&%0Yp{)4iGJbZFz!L7XRvzQ#}RpR z?UDaRkgi%WtH#Y<>!M0$C>sGhMQU4bwqmnHN>3p|mtagNWjiE4PBP=a3)w1(R4T&7 z+0{%u@8B-T+zT4$r^3YIvc=n8ygpN$lxVBLm2-YaX=(L^-*z5d6I<dcw1qM2cG-Gd zbc>42tkk5F`!sb4O^Y0V-z|yuWGc64J^ke2z!fz*I-0J0`ZsPF8pKZ7%Q~m*6ZxI` zq=qJq&#kAqxjB0?KF`ts$onyuo-WhtC2^1_a>kSTTj4uPHpB-A@vE6wvnN>r@49T0 za2dZc>v2Dz+^Y&ha|`nN|NKU#21Y9_&dB&7?k+k$%F9=-Ja(^Gxjgt7dgHKP4}dkH z7pbSGV?^_=31P4xa%%cYqHQDIsXQ@>B9M6Y@6Ex-Z68OiVY51(p4-EGvsC&msIbc< zpZkAc@w}s>vCV$FD<hK+6^%rae1n)b1z42&3H%Oh|E~PrcXD<P`s};A(xMb<1o4j7 zEVKvC9wx;>^zqwlT?T`uSh7RSgkr~x516_1Q)VFhK{QRp`0>6<(wN#p?9ZR>Z+djH zL#v@e1rs$6j~PQ|$st(LUt4hp>bMie1>(lW%l(^fC2+wS`@TDP9ByS*g`m%pEx@pR zdk{A9Y>!u{ZU5Qza-jR(yRHRxK3RyKe}r(tjjV*S(GRBkl?8`=A3l7DlAZDXbtm2x z`i)KARQ;(SO^9>1Pkjen?$<runi+|<34D$B;Egsdy&}}1mxIGHUNuPXX4*606ZJvQ zMSJ=(r9rjZw{MdqzU@*P0EO4OY_}fbJUpX|D_$Lm|0dbiOx%c=DQ_>-oz9_!CP%{a z?tX96NHS;nxB5)xcvUyn2Br<=eB$=S-EV*6pOpF=ve?xhl5Jx*(|W}(cx3v`_$+5N zx>^J|yy{QfnxODClgVhQK~oq(*o^gt*3JYkPEmiJ#3aLY7f;=P;VR``_)ZSr@+(r` z<lB4{_g(d2l11e2q{2qANxIxf5Cna`2IvUBCTz+TA=zbLBPp2BHGn-~=2})jr<VT6 zQ16_2xjNdS-TTi3!logTbQCTUw5sqR>_jK5?zr+5{M8}Tvh9ZBK=}nCo#op5QsFx7 zFfWn{N^N{D^GTD-bkpsQ@icY<S8aX$=;y!0(Hv~xFor`<9zAg*KLzZXV`7t}C{{;P zFhQRJLv*huixu%Nq8DwZr&sTM`<P&49&17c-MY5c?s}quJuUzA>F}g_?JJ!n8#r}L zuxP+CJV`4ooo|pjJtIRIz7oJDtu3HlsB2i}kwNAra?dlc*xTJq%YJ)$UlhyY=;Y)A z2@tBlY^4b$EIT`Y3vhhjz&@$G02N5+N>JNzIo`~0`!R%B|GC}z;HddxVdO>GW5W|e zfB|2_l@LcG{YYWGcFf%WZ$~lRKgGRPV+eXtfgq3f<dVK?{BwN#z=D3JDN$#hFAZf* z^O5e@JS|eY5Hr=d!o;?97m@+FV71hnR>#+1!yCwYm4A4Ba_nqZwZ0C@)pqdrS8MF_ zs58IDvaE~@-nH>VIk1(g=;xJ%o1V4dFYA`aTxst${{U?LCNpgiHgsg)3fAbB4#zi` zPFAn;rKhKxs_g`l<fDZ{#+;mR#fzrQwU-FrkzpYm+R;&1*1e7O<0p8@pBH9nz@OvA zc#ETcD-=shJ%*gUfA{p@S99D+NFNZpXGelW3x8xK#+0@AS4kQ{xbsV&?)mUZgE$wB zqjyEY?s(9W(?&N7|H6Q0<<g_$|Gc_8)^NJ}w$%kzWpOj@u2xtg2z=t}NOSjvN@K{$ z#Nv2<NmN4-kw|Q>cvc4;f2^;xp%5x1AQ*+5uc8md`3uk{EG;e7gd$e`mxbOb#gjtz zUT?m}ZP?C|ecs_dKSUOVW^X9icCNvEIBGd}7~B-phD$xT-Eblfk#70q_>SD}Vl4T; zgHP-LGo^1D`GQR?g|~KCP<cf~;ETpdL_*gBEpFp3%RfptiKIiz)aSS<$^5Ku+@8HQ zJt>l;+rvbVjW0LSt0>cE>K8>94h~LD`7brSTs?)YrE+46iiUale{N19IH3(OdmV3M zVq%KlTz6rf^AO3ULHn2?U8y#D?Q0b7*TjKkNQd$@N;SC#C6jGyR$VAX&l#Aa(-ZF* z-8SItgqs{IH{SM@@=De$RN|tX5@;*omE!TB^S!Z|WZhyXo1|%$+s%VqOLo`)Z*`+T zlfnSd@$cPtVNuHIvJEs@;~7Ykc9`0j_TzwzvH6j*pR>*lrJ<H`Ubp<Rz3C&?=!cLL z-ormEsjPG*EyP@r#q0ldfS?65mrol?o9H$F&QTk+`0ma;li7x`EK!)emb&YG`>mT+ z;lU;aNK4Nrp8YE1pE7y<FGc+&s90iELs)3~4o^NaZtddL>FMdZykQ%#n>unW#CGNE zs&3PG)HF9W$s~ED#_}a}`O_{O7xEN)qi@65%>fWoDtHGb7FrS+$e+;lOl%Ab@~@1( zi{zNM0LY#lt7DatlShwaCdV1e#m2mCu4$BPLIL{&Ms2P`6`72HlBt46sc<~cg5lzS z4P3~^>daU2x!2UMPc0I7yA?J{P}J}9=@H1400$vL>)0?lpzj-@%&>AsG(QLCp+w{r z*Ow_>vF5eGynOkbea?i}CT{R62$HTECxV&z2+BMcu9{6Y7XjfDo^`+zc^Q@u{^u5M z2MQ+mF#q1WeM)l+{Q?phh6n0VX*U;@NLMe??wjdDN%C|%j)DoaUm_m2t}CcORzyDm z$fS4+R!NMPmzQr&Ab7{!*H!(PSk*|k2=WC0teeI1YvroM4gJ!xaj4wc$#xv)+;hC= z1(mWg*0WiKuQ^ZI7%99^>9<PP-V`22GUvCp7DU18tm$(rE6LuZOW}g8X6J4Mug4wV zvp9OtuZ?{W(Og=biN3u?su5;41^91HV8ZiYc1o{q*D_p@`AqkDR^i*=jt+@7cJsR1 zaDw+eTaOM7gv7VW00%CjJqZ%av5`@PY)g{N=-VsX`b_4&Cs%JxkGM{A?A&U;#WU-8 zxeXOTYo_#8J7|Dt_zc3lwSOwVTz}l}cAZ5v<&XBKClX<Y*Td2nFB?|F;?=Kb8!(WS z9rdSI725gs4jAEyASW>g4qWeb21da7nVFeoWENaHNFa*|;*qz88}z}P1cF@!e`Z7y zwp5$l46qci6fs@PraPH=QxWj(ztZ%zqtGrnS!3u2VkxEkdO+aF4-2L}KTrqtO3^EK z^+%yWI!m2IZ`MBO@851iIpgm4AB9}XAHI4E*hQFG5Tt0?kcgMFr4b@>Hzu$A7&B}1 z>G<)Dda@9_AM&j*w#nx?*}c(IKZI#}VQ&c`r(t<L?sh=N{Ajik=5%V=P~1d$oXP-N z^dsyO@ZQS&7{z|bmd{!6Sx<*}!1pVv|BpuoEtfRk3mANNeoqf~bSRf^D3YnPDYu`` zXeAA?*`x0>#{S{#Q?68+9@S6!B@mW@(DbQ14NfEU`d=BJfauZsb7F73pEOzdu^vYq zTmgAn<S!{I4`+4kZ3s@nMk%<$w3RDc>XfGO8gK2OL4Sn9`zTRrqJ9w6V~Zf)fTn*| zVaLJy$$lp)<0t~U{3AP|>%;RphCh3;*r_1`Z1Hjff;DSM|Gb98LSSXN-b~KD;coG! zR-50I3LeJP*Lpvo>VgWm%Rkn6<cw)EO}eXJe`TabsR<iHP%VxB5)E%WA6PrHAni|t zzpZMA=t*gAeR-zdP={VzFeF4gtUkMW9%XrL4lw+wmlWD!cI?b)QV>(%hF2T*LgT|< z_(kwHaQL(;P0V4V>gN5mO-)%}ia%HW8T6S~)&MG4PkaJMn<zM~V6wJyOZ+Rif>pam zDN$lEAAmb|o}5)Qoc=dA&%a13ZORI-mM(&p1a0+oM4rZ3zls9RZ*p@5TJo?Vh;`wo zb$AqwD1`M&fe1D{d9RG}&g)D8&1YJKuWga^xf0V}MIy>VYN?Ucw;`-P*F>=Uea&-{ z$VH{xBBW0~5iI$A%zMC=_jp4^?gQG2zgv@lH24A=9Q>iws`_X@CLCOi_aSs(qSL@v z<m5Z3uM=&#QJJcA2uha+r4l~rziVEpunM<xAk2DBM1yO3;sqqu+z3cwJPiITr2ZFj zr0DdTgMcN!RVTDlzekJk*RK2GIRHCb9E3~a&!yQa#2iw&K7vV{eCYX#w?^{tE_C+v zp@Owk|FC0y&VEeV8kobRba2tMFV_Od1<|n0zAr3bO4a2OXsXO|&kc$cpH5v`IuVGz z?122ffqZCM_X{-SI_{_yJH4i-*)DOx3xiRd85yZ7&CEmD-T4ec%#F+0Oe}nNp&9Oo zlIJ!67#_FyNOX4L+t8_2Ow`fBb?tPBw~r6$bo0t7qYbGIj~r^v6-$F;dH-H2QfKp~ zTF|kqp_62xR6tG$&JSZ6zck`bdGOMy-7l(uBTP&yrB(g_+$OI|ZLryGM44>twJnO@ z=3<2fDUC_`Dn))1D%g7$8Cd6VLM9i~NG~fyOX+z0?obkT6~6yKB?#_fG4Gr)DmL}! z{0lS;t{Cv|59E{3hf^~8?c+TOi<5XV9}#KHj@7J4)0T*1aLQorT{p(>xRX?(qwY}W z2H!en)Xkm7`F>ww-<y*y;-adcU{>t$j13X;$72@XF@VmEm2+cZwBBTvjif@ixjn=F zCTK+=Ov-&;a4q>x+vzORoYolU6S6|Ml>0Aa?F=RoI<+-*W_E-lGZVkY+$-JYvF1|P z6AYR3kAMrta`We9LrS&?Z<xfVmlPtT+!wSNc43Ho-yHG<;@2dKw0Q-jxhkpgG%RaR zx(LbbmPuwljNCG9?kA=GaG%4&jC}oXzy<rcyX8n%w|)>F{kx>6%Gh@N)AgnG<8G;< zFz%`zOr$@X)<M4|DpNje^Hj2IONPV}9VK0?>1a3sn^^>{mHSc{oY!IX1&J1nQ(vBk z#y^!_G7kzNvw?G9^=`*RURAf?mm9#)*vxmAZiSGU@3Ht9=1B?sVrF;=0hS+u?9y~z z-&;MqH)Q5^b;<>oO`f(?Igj~^Ryh#Bzv;Z8{s+M7rL!MB%rnlAJ<pISsptKi?S7;X zDVBxbYrR)b-jMlV2MGOq-?!&uy9h&AeHq_=9p+cgmzV1L_Osek8@YtAe33{9P|_*> z^3R?ZXkeo#s0SM=m)!X_GYhr(t4acAP9~pe<*qfM+x_-SF3D4GHb21c3vN0O7~j9Y zvx3ZDIvW{yejHC0N;<b0l$fhw2aLMX{1Km7)Pw#=5*_~ped$sjtLr^;Z=c$$Y9;Y; zG&MKNlr()ZqW#+T1*+}Nba3G&)g!c|;Wy8h+ds!{)z$|29>K<1hA<^^H5Vh9<aciD z{@E)I-uLa+ebPpcySqFq!0yGod4u#e|NeC&<>9jNjCA{fi?jxigl;a;N*wNMY24kd z&K|rcw@8xabYZtcz;b8t%3zMrT|ReVzz#=1^BnwT(RZNBYcvDPe&cH>wsCoFb$Z(D zN;;T{L60`|t^C>t!o0`+%}sYHD$5YU$0MKhQcyMK5Vbr8fMO4xL{Yw**bz2F8O9E# zm^hTFx)>>eS&)o$D~sb392TSz1LvMoKxetV)gihbbJw2P04fC%0=$aK?ZKzeRCREC z+7~+D$-GP3zjDN{IPnX_&Rp1F&Dv4}a?x~=(T2aBgUZHw^AZ66Th|cyKJ3)pipZ<L z9U1%?wmGba*@eg^OTCHuPc%5P#}oisL^()IcXf)y1k;~s@wJ7E0SI=bH-m=Bw*+xu zAU|9b0Lwg$q1?hO(%A6#F;eq8jza-Ct7Rz?^tA4)F>1${7Ku`U#rJvbh(583fEED; zP!?Tn&?V#nB3=drwl)r`Yx%QK;J0!0Zw{3WaeXdUQJDCJkp3^4gbWsTAXmXDR1{mR z$wK_vUll<C+AbYYNYH%~-~{j4<-*xnINky+f*U}u%FIInZ<mq$6tvI#aVyj_hGs!a zf!S{*bh%dMlZ*WR<zTQ}3t^-~`j{(E0zJT@>^#&_-9lDRZ9`0ri`9>H(DG9f!aKSI zu<va-rR;bK-P4qiHUU%XdP8dK1&_=&RA%4*=#XN!&ZPuZs5yi_H*idt9$WJ;wFAvz z%1RRqs>x}-^C}D8cm-ZGKR$<bXy-&ch`6SH!kb&a<Bo!TytoMh<mf%$Co|h_*J&-1 z@`2X-rY!`OvEA`Yf8oj^osCF37=qhE8j-uQ*oGwGH&R}5+V(#5xp=!T%`hdyemK!m zK-=KZ0SKO<98Q#cEk$iio7j~L(@t>-QqKPXE^_Zx!Q3xf;X5I=c!qLHqRSwZdBFtH zo!OCkO-nl@NrqJ@tAH-I!tRt!yLo6JOf4^hLLArBkojePwgk$&BVzeELX5ber%r1N z?gUN?GV|tVtN!OHwEumsgcC1VjbDUc!x%w6%lzq+=eu!CXVVK0H{-f>&C>`XG~V4x z+ib;2r8S3RJh8m*{6On$V_jUSovrIp7z3dInnAI!=tO%uoN{fGr<W~GuHhRdTu#mi z2xxA|5p^pErDaE`GtO(2s`VgX`Q0?W9h4SodPICHbT`coO&R#gadK(#VN-yEl%NwM z8&>F!tghiH4Y;#t{Is{I_QGq)|C^NHYaCEiT$#fW?-xOaze@O^zA>M@_)zxfku@q& zA5yZ=AJw~N;eEc>$+4*4ktgZ=Pg=U=@u6&vSLP-B0}BHVVuZsfoB<v0g%lBAHS)(- z8UEB#f)Z2Fqh&WByZF!xk2luLsuxo5dR(kGDqQyuO2f`ZNn`s4i}?9-vH6t=@54mS z3Hg&dB9dLU&xz3;cXI?2Y4xnEV%jt1|G1b|)eI2vJ(JFAm>e>X7KYpqDL!PBdXIf{ z&i|q}@?H0NtZ9y8-4%GZL9%Z2z}@@~Y6WGskJF)9v;Q(&u!6h$Iw&hTzjw2~q{?}! zN75s_r!iDE^SzKfMM}iFF`1b7?%LPK5SGH9|34g%7D8jCbk7R4@Tv(w%&aQ}gX)5q z+2P^mRU6p+Uz%LgfC#R(sG+4NyekirVxn$FY=wi0QmVN#@E07==plCs7I&34XNTwf ztOVQ~NU`b5mt75*MxFEhwFu1;dH|s@K7<8}dXO)&0|S2Qrnuy+D!c-r@lBpSp{tt4 z#Lv;3Y}P)kc(c_^A48iJybmSRfJ-nt{p)C2@x_bAIoaDpwl)Y&`99BkM9Q$@r+>%z zd!V{goMlj@|IIA0gj$s3$eziZdv-5I%O^qB6jcMB$!$^Q)0@K?J(ra{`fS0qy+nM> z_qm}?0juTAv4Qky!=%2|PV)IFyCKUUzMgW_Z;et)L)jO2dO$Xl$zq}I5hsq|qlAK& z??jk}izjP90qNi6A*OyQH*MIET3%39HG)!G&>AQ=tlyK18Wyu;ZV@@!ru?|X<Jz*` z^{l>tgyFMcbaZi1QlQzfn&l!HGs>-HVL`od2>z4(k!|w7T<S%&FdmVnAq*dD5dYCo zw%tS7rDNZ5O|Z`#Q)SP}M2>hYtfHfWvqB!anpeX!TtneW1gqj7P})G~!d*KlK}JE5 zFR%Zv!`o^F-HaeR6-HasgD&S+MRg_;_&A3en|pZMQ^(UvD0@~-mV)F+R8~U4VBZ(- zE&8$7AJEvKdQlfgBHHh=%K1_aPNcEEcnh}uSDg|jYO0oOrIgiH>Vtb}HC(KQdE&t; z^p-s<ru!md5)RBZ))I*V@%yn&e~+iK2+oUi)+iXwberW2biCGn?om8!k;V!IqaA6> zz0jZ%(DZ@+{ik7-=u&uIqqW<3gp)eoS%P+}@?feuT}L{h3RdMaGH)JMSV(7yf^C~a zVTuOMQ|9~PG-t;Y(NTPr^r0|zLeXt{j3#p}wD}DWf$Nv6Rn;uDvx|A%!c&%?ujL!1 zmU2TFsgbGoSRPuA*8mT2j~mV|93ZQqTsDl-YZJj}k(aR|Wx{O}!7R!q3Ms42mw)rD z5{RiP4CS=F&KXIrp(w$cY9r}j7EVI)NP~;vi2S&I2r;qyI*YO5@EoFA(-JK?_S@{! z0_NiHLqdJ^1()b9=lqN1B{^f+-3=;InwG<^V-ug9x`slIX(*UTw}@;cWZ1i8LaKRX zoli%bShpPlp8Su?W-A?ARgQLs+61<m4|I2AiFgRhu)>Sbf>E9Ag{idB2e&Mh82-RC zl4h?aHgG!U5L4UNR5kX?pJeXa;jbyG8P!4o$JEI^rI%Z9H*2YV=V<wo5&`XD!)LA_ z8h=EJwQ>hIJzI&Xp|6_+wTD4Pw4Zn<4nC{IZN+uWG)Pei&8H)Dm>{v}6qi=iETg5W z@HeR<)tE!K-1jg5zCM5G(3m+R2`?D7xuh3NL_^h-Ddm172bt`w9RzHM&Dw|Ib>kdR zuPzOxBf1=b2&dFZ^Ws?3;O!py3MqC=v~=)C4nnZh>d>|_V-vI7m-YkV*AYhjtjT7N zu5kY=+{REPIskj-e;N|y=e#4$EC$F=7&6*e+askRt}r)3^k{54etJ?_<aZ2T@o~Qm z>H^yn=nVZVmML)4bHp{vyZ3?GuBIi2ol)*BL1MTfx1&?KqUbG{Tgg*TyuSzk+h(>M zSM2`zl}H8!RvJ49H}2fm&YB=Hf~A8+NNLeg@mC$7d6L+BeU2@@x}=T2F_E|4I9+q2 zf`7+I*r-al(jsvH)m3Se3dKT6x0iA&h|9<{k^BUTq%LT}zAHxT`&rFpX^zlj!Wnhh Yt(5IG8z(dHPd7kA?Gds<)iUJ&0d{MeJOBUy literal 0 HcmV?d00001 diff --git a/Resources/favicon-32px.png b/Resources/favicon-32px.png new file mode 100644 index 0000000000000000000000000000000000000000..7d4e80d5eb84e82a8aabe044b55eb6c5b9292e98 GIT binary patch literal 1014 zcmV<S0}1?zP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW zd<bNS00009a7bBm000CC000CC0X?ab4gdfE8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H119M44K~z|Uy_P*}TU8i_pYQrFu^~;=w6y`LiRwU&#D-M1*p5V@ zKSBykX{b~rI#3i;egcAQ-WV7d!hjH605b#x1A|3{k~=VT2n?VS38x}Os7ZxdHBOVp zaeO`o$JD;PcI?*9yPfxZ=X;;~bI!d&6B-&C+HH&(Q{^}?0QiBcAU|3blhf1FR{#=; z#35BV2O0+=sw|7}2k5uJ1>3gQ%RQyh@I)f<qAC}FT{TKJK~o_8z}Y&j7C=XA+n#M0 zU~n)wDx&FTfa^(>H4z=OZF_dxhRI}dzcD%obhaYALJOcVYr86AbfQMM0-Fs46>9i> zBJ%1s04WO?c9}>A0{&jmhe0N4EY6GY5@-;#ANbi>psFY8K7x2W9tK&gUc}8{u;Y<T zCX>HgmzV+Km31Oa&dki5FOQSS<N;%J-VyPy`yEK7tk~hVB6m6;^@3y_jkA^ETTs6_ zTNoq1SB|%~=&GZv*IPM?9U0$mL?@Wd4I7`e=N5aj#-}HP8;fZXp4^Hv&M*-*&J7}B z)aBBgYvuntaPau}sIhbo;Wc2-*8D&&uy1P3A2|&&=hE<jWdR&KIzFtbU*Y<;mmpSs z;g{P1VLf&9gB}(AybR;6yAZ0ah45~$@dnW2GItaBs{(?RK;->G$p3mv0T5nr@zcd( z;p}{7GVe0iQdtO5ho|0xp5KY^5`f}!c*W%|?(9T(2>`A-Ja2H_db61(BI35^8<H3F zB1o*Nq3VZE9DDn)iuH@2bw4<ws<-pkzX%nUo&$~5t91n^7QK@mPjRdSQ`I^ys`2d_ zD8h0ed;%B=6_&nk7Q&j(d~^jN)yyKN1ft7<@Hh8!Sd|VRL;r&Slp%8d@b{}7kyo05 z@D3b#_Yh+0{lhgw(U<e=n<{oP+MQnvHUuDI&bjz`6+W}zGN$=A{I<3Cp<k!dKQ#!z z;61MBnLmH{c&gdp`;i4is>3NnR(5^26X9iml`2Q^Ep^{|YzG1KveVQ}>&xi#?;U7E z-rr6`9zXWZn7}7B7UY0pOQMB;y4IJ@I1el7i>EpQq1+roUrU1OO0!-0>nAHAtOYx@ zj_L@o)_#C{fA2g1H?o(m^+un$2=XjKcbg%0=e?mJOXZZ`tPXn0_KobNYdz7YKNXL) kC=db(0-<(7c#+fp0rHYOSk463XaE2J07*qoM6N<$f*tA0G5`Po literal 0 HcmV?d00001 diff --git a/Resources/favicon-48.ico b/Resources/favicon-48.ico new file mode 100644 index 0000000000000000000000000000000000000000..295a1385b456709c7b67b0786c23bd129f90064d GIT binary patch literal 2926 zcmai!XE@sn8^-@JL(!NgRU=Y0TY^%1#@+-)TWYlSs9Dj9U9)D*QmdTUL{wX&s16#V zsa;!Da3pqV>UGZf@_u-)>;3ZF*E2ue&kq1V04;z(0A~sV9@7JW0ssII$iF!q4FGhV z)nKrH^Ez4p=w$?eBnvZrR%Tx2vr$$<1EkfNo?QSCV1~13NAoig0O-vPky<E^=+A|Y zJ}4WmgzmOAT%UU~mv0$;2Xhs&$k`CbaR+n5(<ABTO1W;VR)wMAb8YR&C8o6KodMdU zeh&*cBx@-|EF{^*^aG?^8177qtO28@JteOW*{kE?UDk@%O%mChWX}hUPkWz69TWr| zxW@_0+#?i6x(-<_=w=vV-BL#Cj4B=rSns1IzVg?DaAB{@%gf8w*LCtPR$RuLmVuQp zma8p~i*2eL`qjBu7#j6A?6P2ySu*1aBe~R?Fy`PO1hg)?{_fnHSIjnZw0iG7Ca^}i z6GaDiU=WWDerN`$d%hduCP+4jnyGO~@yNw;J+|O;da@%qnG*YUA^x@9l4QV>j-WMZ z_E9QEbd;CrKKI^~04LLh8%@qyTEZ@ZjFBHv<4&lb?>0=)QV=k%0LhGmA4pjV3uwm0 z5C|$oLecEx6KWMKF<V;F&p}Jnp3<JZ17%`gz<@<<_5(9=HElIgN_iF$0miv5&UoX7 z^WowRf3G~n&O+|Q=@=!-Slx7VMAxlp2L=X)-dlU-uXS%g?9@_mcE?PN>(w{vt?z_q z?YeQk1k=5Mo86$nmixE*X<Itm!!epLa=T1jP;Kp}-1KzE4RjLY$g5Y|8bE!RpXZDB z?mhS|8NWbwHIA(aU*9dmg@g*0KFaa&aa2Vd3h7mhCeG$}NB8!&j}^A+r}H$wk7!#) zp-<L_k>T%z)JPj$MczFXqcr%>^3{6ys}o3~xa#a^4^g*>=R)%YPCF@4<r2W##AYIJ z;PwE0;(61AMdBgL%GEiPX8&CLq-6=v^5(581yS&FxCb|uX56%84r$LL_PH-LM6bJF zOtD!#SDtF*<n|y|)bzfab_eNQPGJ+U^GU(fYV%QY_h5^j(+jh7x5{AQS9G;h^+1!P z?a_O}4E)}ta@L<c$spp5@gIZ4V%zbW+Xl{FvWP2ZwjzdDO+>fwD@qs`?{O_=$xqK% z>;~?}Le>-FTShJmH2aFHAMU5SN-2rI1339ABp3znQbyciGcpo!4CY2IP}n9HYBVsm z=CTk=z?8VyC#<N^_+gk#7DsCDc`3@3!R&)ukERl;8K*PBq6`j~c%QyEX!G@sw~pyG zvElqAS?@#RCo*Z?iC3tPHTHa7%TdS9z0pqM*WWRc)X}_?I&1l@6q*0aPVf7N#mdGH z;8M{75f%*A547pVotA*T!Yud88rqFVUzZk#Vp^TH>MS##eUu=@)m-V!%Ix@KppkoV z*NwR8!0G?Z3%UQN8&|6C6Q-|B{=$98iC|@-6^P=86ES;|G>d3VzKP_|d($k#Np(yF z9J5|Dl<wtwcv9+Vf69(3+eYXex##d5&Uw&1LhY3zRqmHx4I}*;S;Y3UI(r+Y@{g(c z>fn}!sVWs)GnIVC$a@`|l#&B^d>0k%ap+qehjP{A3!6dVl=mPhi4E^-c3CUOl)|kz z`Y;O0^tunCh56{z4OsH!(k<^C?2Uc8S~(IYGrh=@=UJ8O&%7BbeQCd(@2&e5Sraj> zBr=OLUs)2cvDG(jhA72(aW~w%?NF&bN}IJlcJ<n^Uha3C)f{!;FW26nWuJ%WaU5KF zK`=O)>Z&)X;-;sUco>rTUA*UEkivP*zxt;{;ZL8kpY*F<24~F|s$ZL*n3{-VTp$T* zd?OT6hOT=JdKoTKJX!7{<pK|LsK*7R{&&qDJSvsJ3%U%YS?QO~tao)YU3;c6CmXu7 z*@@Tpr)|aT<$MTUJZ;Zj-hJpW_4-NKD`bZ3h^jLm!iQWru=2c-GV6T`n*Oys&n)Fx zb4FOeP}dCEpzR#<KRA3wZUFKRhXr>}@0?+K@NW()rln<fdRtpU2cm{M+cSlUQlyM= zHnz)F?8UQQvXfeW28WQ(XM|?5xbV_BdMOi^R#sL-j+$$-3u|GIA9Hb4g1N<GD6~Zx z5%NigAs!BebIv;^+Yjb<e-UWc_(rv+d=7Fh<V9PK=zrw-Tqk(VSC9EdDBn(%)p~qr zkzB!$@Q+8LWGQN{;v(`y0-r4FF_-$4zqQ@@oF;w1=a=IQP(h8nYp&rz?z;0)4jG$E z<Fh8#UE5AsFPl5Q=QAQ?GmCQUDV!|lN5q6ltM<+ntF^wMn-kL!95UPFg4vG<92x^i zG!s=3*)>s93ct-u>=hZawCo3BMjH3kV&B$A9J3vD0@s*f%illsRc&+@Pcs0H^!&QT zrlo{VCZ<}I>ps`i4=-=b)4=AGqKkD4zR|E1Y&LjA@qV4Ofa%BgZ#T9+1K94OUstBu zy?I+JzpdFk<-?Fk7qf0lm12=!^*TA8#+C3f-`41&s?z{gK(9b#IBxHaap7atBzL(1 z6J-tAKp|9(*LCaq<^wtVQvo1wnCrXD+Jym(MahT5WGt@=6dWdEDLvewVSBq_%UCcf zW*yul7=SjIJAA(-m@VJ%i`J07-dFthxtIUP1F*9`>>m#VMMUfY0Bz9U9=H+T`Bcrv z(FU4Ox_vC&EM!W=wSoRjB59`eC!p|?>Buq-ZDv!?8kDfI1sCcOO^nYR6bkLdKyCqY zbab?_y75^mh+r_wa}K6zxNePMyo0=1_CR2yI>UQnS7+$vIN|lP!^kys=}8cwQ=5~Q zmv`DX^@8=PA_6#yu7znh@MM4a^2H9|Na`1|QDWp}gk!IykQslp_t4MaCkEB3M#)Gx zrCj0<%q@8%1ubhBwP#AXo<~#PXQsp#>KuSjk|<xli`g*Dm1pP2`JzK-5<a@!@0L+! z493IXXrFs7<NaE~HMm^wB~1}49k<ufL--C!0T4e24J|NWr1^b&BvDJm>k1)-%;5pS z2_8+^KgWua)MLU`1sLq>Zuft7Zd%SX5y|d$%*zKY<%Ymb2tz}~(oeEKu7F&F=h!KY zC;SFgb(4$j@Gxb;{TMg?wAS23M~iMHN9d()yYzPa4{Sx<grib2|Al#fq-g$U){CWW zYdq-Ifojp%iz%M$C9}zL1O&T0ffepTTpcU+UOiO_ARisoGzYUS$~zB_|I(Cd^7r~I zsKhF}Hb#zi2l1&YtJh(j7LW0HFx!WVc{89pY9V{E0^HO$EXxhAA=vtOvR@O0VPsi0 zjPt0LCMH*Ng{uAGG{P7Jxg9|H^3G*(GxDzNw(NwJJ=ij_dM>79K8ehMSqch&w<`75 zCNi7}Xi!%55BL_*S|9>n!mnM-`eP4nP=_reC7Sc0PnweVQ=W%tD3)W}cPF~R+)qnr z)4tY*(2MbOW($}H*3nsiRhGh1ws^&Q@53T#piJCPyZm^13x^|v>4m-y!K>-Y2cyZ? z4})&xvI**(#sWD)_kWWQYxy(n3Zuzvd=CyGE+lH7yv}Ie?q-ohsO<DJCl&=dm<yrf z=mEES>w@fypVxok+YZZ|s=sK+gL@X#8h#XLL>+}=rxs2!>5<qzaineN!*<R^cTBE? zKILl2e1s1pXs_i9(R5?*$L==2htAx*nT~+Q0U^IfRGw!A<6>MMAN6WHkDz#7ay_M5 znWk)(Pus~jhGf)w>*t23q);8bN?SS!+%*0OgO1u(1+KT`HSFt^c7S)TXonREg|2m$ y2^p{E%-69+d^$+mtnyYHt=8av(k2F>VWKGu)eg`6athkXQoTW-oACJWU;PgeO<Oqt literal 0 HcmV?d00001 diff --git a/Resources/favicon-48px.png b/Resources/favicon-48px.png new file mode 100644 index 0000000000000000000000000000000000000000..238c48b9945a9934cb4f7e5c825d2ca860b9bd5e GIT binary patch literal 1490 zcmV;@1ugoCP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00004b3#c}2nYxW zd<bNS00009a7bBm000II000II0aj1YTL1t68FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H11y4ytK~!jg&6-<mR8<s)zqRMI!)1yTZJ`CZ3Z+DwU`ezRVSrd^ zXo+0pqD6y2qe)4iLPC7v35|jB#si7OXiy>$lHzqJb&6t5$Pl6=B47hVs0dWrX)7(v zSw6IdV&|OcOot9%G7ods+H3!R_L+0`WrQR#XU?2_W6V<EMUY9VWQ)+ON~4G#Q}tBW z)g7$?w0FCrqN1Vc=|h(rqb0UYp$IvEQRT9ToK>Y-#6A=Z2Cql!SCSxnk}=jc(3SnD zcM5cKFc_?Xt!Cc5x(Zd^14hSa*Q!dD*PHfXAP{Kl9T<=Szu&(QWFPQIf7_^Hi)fXq z6o^n9Uyt%D*o%X~;Dx(a;wx~ExpU_)5z+mCC(#-SauxL19uo-}Sg>G0K_p@y2Yks? z)?ZZl#p_LbE)WQ`nM4XiBK9s)BpyJ7g3iu6n<0S_@caFxAcvDLD{fjrCt2~glkyk| z5#WFbCsp-vkd;7IKZ~Ch;V^JZm11DFORf)iDXtOl`~8-x+z?@eLrRA+Qe0bGd#vX| zadB~WXXl-7L1)G&|4}&H@=hoey50R<=FXkBMTBk6P4sYFzX8j#a$Up&vc0!>fIuMd zw=t}9tE<YDj*eUFdx;0oy1MT_0Ihd!X{N_zz_#r%ZgCMk6770zZS6_mhNBF_@%sAu zf1`v{L~5LS)ZCcAfx^PK(wehJjux@UdbQ(Jd;96nM0mv^J?iVen`^+M(E_&3HK#_l zE!RZUDLmZKz|=*X78$YEv_gr8J0k!ZuntTf_GEc``)OQE3@UvL<SpJ*K*ZjK*ioJU zvb@1#@i3XV&%m_OjZ3iYPY}14)Wc=su7T-g8;e!t3!;olUu1cMCrxr>Lb>Y2l$x*r z^2#<%QtW<;zss^lU#3ouWTbQ?x&*8JK5<Nm@6l%@1|4haq7C`LiUHJE*67RBoL1JT zUPxeSGB7M-%u>{KQrCg99XKjTOY{`sk~P}fQ1fx~gAnP4g|g5k9<gmHKKJ#GV=g%< z#Q+l=qbdg<X7SKvAlor2ga%JaVi+Lo6jaf1gQz7T3~<>gD9WlQO4j#xF_IFD!VfO- zEX$MjU4Geyrw7`R2qy+wBN~>+NVRQig;RpUOjQ%gE3Iq-ZYJ1Iy|y;3gG*_ks}igc z73;U^9Y^v?D?>nGjNDP(pG27aS4QT@;iRv~2%jgJCJcb8ZUO9M6^(|fEDAIHQy9Ot zdp9pB7=W{1?W|FeoyjWdgs3dNl99O=#;+CO^JLWQS`%;GYDD#^WE6}=j|tTYZzRJ+ z*K|76QUUR9v?flEiQF)+id;k;m%Ont6a8C<09%r#uAlUnXle1S1Nk!1I#zl{_+^wF z_ugm(epK|IL_2b#EqCht(BZ1Y4Pa|Yj2+TFZ9}x;&;2{2?c1arOGIb6?D3xVHMYL} zduaD<r-W2vKpc15RJ1zu5Q~Sd$EfoSq|!NfQWC>}HaZ1Gd4VJx1U^U%m@|%1VfOUW z$}ObOgfZIT7ME$Im2V2{b}u9Kq8!0x?eIV_+BqiNv`^&X9#vLHOR*3+glZM8XI@&Q zLeT?~>C!DU`bL%Jv{K&v#_nUnbK`7%d)BlfFW?_^f&F7f*gIrKMu+!xk>)ADeRDPs zK<>oZCnA=X5&OhEB?yw=nm#n&*V)&E2jJ=-Ki$rn@ys`B%W6sC@o5ZZL^E;sl>Pdd zpKoSPd48WUwjW~5jZ#OS+v&c)2{fHQ-ZFOL?7g-}Zv(S{^pwbS;s3Xj<MVx5*}IRS sx<rsN&^%xqFl_Lo#^j){@!V_w0owfI-wG~ir~m)}07*qoM6N<$g2~3kqW}N^ literal 0 HcmV?d00001 diff --git a/Resources/favicon.xcf b/Resources/favicon.xcf new file mode 100644 index 0000000000000000000000000000000000000000..ec49b922f6b5f4431f5371e5e4295c7fb4ea0b75 GIT binary patch literal 32098 zcmeI536vd2dGG7azG*btN76_dX=ZNU(Jqa4S(@&VEL)m;je&^8j){$J3ERlh^vJS_ z6Eq<N%p;Ewn>=E+<TwUQ5)!s|2#-TdVzwl#c7m}Ta9-Gq0dHC?&D`#OzkgNV*0ns2 zi33SEPjgOBef8DSRrP(fegCSy`>#20sQacX_jT{P=CxNFV{++(W6U&p#F@d<H1u|3 z^zb*6r;ewIXLd>}-Ynw33U|tWvR!fI!2<`bzWy+Nn{EGrs}Ems)fI=YSaJ1Dhr=tb zJbW<h-gm{3Ypy(a{mSmE!mDrWzW(49*IhljQVBY24%ZyGV*k}E_J`M8m1dGNh0(*W zd+pWTqX%Dm%~ky^g?;<4Jos8NDp+Slxc}8xbgy2y+Wt*KtVx6VN&4zZl2*dIpPNw9 zn??^^apg7F@9$nyc^AyrO~y=lD{i>{n#0}KTz}<jt{zqTgZuW4UVS(#pi)1Z<v7nw zs&+{Ka^EoK)zF9DX3XJRD%>g&N+L~b*KfFqN5M5|oCaJ;FX52{MJsM|O5>QWD!QeL zZmpu*s^}?I^i*i&Tls5*&^z>3tvQZkYTn`q6+w^U*1X@CD<0uF{>-qNCkg&v#<V_x zc7D#7x#t<v{b^(R7aOzo8e=wn$QU1O^<QMn@KR%5e62CB_$1H6#=KJHFh^VOHnDHE zrT+p^gPC{GoM)n4zWxXH|B(GZsQ)>Cn|-1kHhy4^nva`P%X{;A&+9X5Ow^zEn!UI{ zJO8|2a?y*1OT(o-X0@3Z8!iqPn~SxRzQfig{iliqDt55VL_?y||GuT&{~c*$Ta|;O zBctKSA>I;=9S&Q<_>is2Q4{CXK+E;-<@)z}{d>RueOcM&p49)@iT*FM|F_%!CHwzZ z?Eg#c|CLWF(SU2YIZL-)y!U0RdEKd+<uCuu_ka1Rq%Mh`+{{Z?nDXXe(68}h{mTvJ z{Ae&R1rzV^XZU?cArAamevg6cclzBX-tKq#3r%^*AB5<l&fWezQ{F)y-TqwD=g&c& zgMP0++eAd^^Jkj)0>6N!@PdASx>@c|Gvv9<?+|CX-)`c){z`v}S!16zv(|4_gf)JP z$qlaco6UN^$!y>u`Z~YSZ1fvUyxHF%C%~y!SvSfV#Ho|B31^F6Yqr{_MyxG<>d?Tx zd*E*MBQxX=sFKvwHiPk3`%$pVG?;P9Zw77IR{HTSLrDXFDRE7QSw?II?S9)@)6G(< z+3(LX1;v|bJVlx%XRmeUn7kjK&-k79JD>6N`SFmML#q)#b9hUS7@Y=ciKw}v7K@rE zs$0~YbE5lY7Tu}M!O&0;3<dQ;JQU=DeiQBRgXw{~+?-$u<HC+$ZqRL_?L)!*V4=wk zEeyKMk^lwR5iAMN2tViv<`^%SEixa>G|>fq;01KF9YKFEUEHO?G_xY;Fe`(06YU)e zRs>VTUWKzJXcM_QXf*>ti=hl_gJy9C0(zPsti#z5G>TjwsKW&ta5e?%ZNWyI%>i9) zDA-hSY87>JkUDg>9l@5M#+(=A6tMM-vn`;{1#5yb)u-1*v_US24IKJTgPHIdK`CjD z;edq8#cih+j2c7KE8ao3TShG*eO5~anvWibqmEagHN3#~y1WADae?i4ee%)6lxPx1 zVflc*X&;6$+POEFZ+h^rFh6Zxwf8sfy!+noKXx)X_0w<P`^AsG%@oXde*aw$$H~;B z&rG!a>K)%rW+qF_M9ukk98cyX-KM<YmhUC=5{Am2cb`tWk}gwTcE?1rFk!Tu@vbQ8 zPC89Fcjb?hB?&|3s?R6A$t)NLPbYl|vYh_0#7kzFX#IDRLNeV%`^S_1gf@76vMia( zMDRb8<q1M<_+YXknPQ^4k0dLTHu&#JR^i5PPF5!jqt_;Dk`@z{BHU&Zb^SP5n=}#o zbIAa1{FY>0(rBU!W84N4HGd~rj~l-!*^tzmD1XYj@n@2a39{asY)Wc*!@bsxFHSb& zjlY0{u+LAn$oULT&P3g@b>pMSR=n{8IFZ>xkxq_(^{%)5_M30}=z~cP)*n*%iDQ3! zL90gF&ddJFe4pBtAHTW7P=x|?bmwdhyVp|p=;W&ycjG^!Iikn6p#<@dX`$%ERx{ne ziZ7NjB=Ord{3g-Awpus7gz-0@L=V+kH$IF;^bvWUb>n-b3rYM==Nxp-#m=$nlEho_ z#g8RDj*12zLtZdDjmX6!=UTE`<UC6*a&dD(#FA#clo<UUMAgR!eV?_?3Nx|8XKrcn zCkFk&?Iv#~_6E$Ev;4)T>@&~K)8x`+=TC<E-IPF6s;1KN(4dIi;51X-29McjNNu&( zUqwgS%eqpNrDAJNV|f>yKgbjr4em0?LGxW~{A~pJM;qSfmj}0Nt}Ih5f|bfnqq~`A zNsNAI7N*0s7@X~T&!E3TLG;YsOx;jZ6=^9i@ZF>sAQ)C{O!%4*i9kp7;}TB1&G-D- z%#id@1=#6P4jC~2ZO^EkFMwjHaIUrgYg)Te1G$vCyn~UuJwTFq6f&4?1;PXdcRuu( znXj(U4JnPzGr+Sn9183JPRHJ+p<c=y?bQs$7;dMdwHmP*lg5~Tn0wA=!eOLGnKiwX zed7m58@|I!@#R?JZQ?)Cb~9-;GqG9a<*no={avkjaQ6^z>R?78;|{Z2Go>cXnPw>u zahX4yzCzjA#Fs)2F~y?2jw59ynl$x!ofb23?FBt`3<TqCt6p;Cb_Txj6Q6zK9!kIz za{0UeDNdS`iRf#;*{^}1VbA-1luS>`|2S4K-DaY$bjRb&CKK_eOEt7#dFJcBn=DAm z|9q&OCM>Ud@6!xH<)7TxME@!;x;<j*D1ZBvIeKMz)~%;CX#T@ms$H%feTwO#JpP6T z8Z|0>k&!sMZyn8AuGx2jDWN=ZBxe}g=Y5_LKl)IsMuW{iV4NTSdbhdi6oYy6;}0Z9 z8-6W`%Z%mYkKf++-ZHa7`A4H|iaYQXCXjOc&JK+@jqf1G=<ydZfJ9-;bQJwyB|}Jj z9Wzw)lYR{&e?V!X@vV#_QR7z`=Hnw2IT|Rd#NVjXF#pGpc7+|~BZm3-9*rQM7IlfJ zUrQL&kF|)L3mJb>BKl{lAKxtM<Dy<7>K&p6M7>E=pQzW1TKEE}7ITVWn-N$Wg>eSz z!Sl^ZGgagIpufxXn^_FPgNU({0dc$OF>~#PYOz_UQG6i|L+x%x_9cFo>Cu>{4HN3D zrN4$yH#`<Bt${Nd9Ab83uQ9~1%6K~@g|(qm%}PxY?Kp}w#jI9nn+EE@w6cfdg)MRh zan|{IcwG~Y=7L6}{nQ4r8q7w!ry^Os*`&hM$r;4i?8lqYiY>m#XmGEofhMFT-B*!1 z2T97DsrM%W&1q}sgOOh%Ti&g5#Y1eM5U6&1q($p}CQ4@Sz_;^*sz=w;Sg?ax1Mvjb z(Qc~W%@u<ZRp95-*`weyBWo69@muu}L#*d^&dKhVS9UC*hJq%>`yJ|W(}T!%x7ixs z^Jb1_Aa%F-4E~GF0z%c>7TKAm%PiJFKcB5r<g3H=+QgcGU}xidx|>+4DF~I6Lywb_ z&J=Vw`PvOriS2K4FjlaQT4P_-!gQm(6uCB=b((LQ%zDjLjdF(6=frG~qZw^epvU&N z&CG0@1KZhfhScXY|Iyv#qq_x|GL$&ScB+P@&IB{=5K~;GuW61=%f*`0O{6L?YcWEo z)K;p&6smobn{s_>2}9E!vtJ>gv#D5iBPEA<&^8bz4W>LjmXkW#cGA`SpwI3=A%`+j zA!nY)Klyj}e*32<pM31m`#<;5x9w+J#ekWNKl#PCT-eHhIU|`Ezvm6>CB&rs&DSl^ zn8{@LnM>=K{koF!clI}G++?B|YiHc-PRc*LG{-V~NfP~aKf>%`QhRf&(JDTAU^U`o zT6lUt;{`i{4|gy<FrhrqLtU39(IaaSDO-wTyJ!sNpVKdA(piy2<-Hn4@$X`|Vzzop zQzjGSqaFk4nk0I#T_b4{e+XwFiT5Mk0p7Akqba$!O2A2ci*=%j6_Rig{VhYH*{In{ zPV~c84Xa5!Vx8!R&B%Cj62Hbe(f@6btdsaM=iF_bC|<1LHHm*6=R|Vqt9RXY%l~-K zr@znK9p4Wt+S$UZ%Du1s67y=jfwD&%KE}Knzfp7P1>Z@QCh_N~Y1HwTnpVfBh_1O+ z^XeW6|Lts~;<rLYHJ{MD`VX{LG~;`kSdZs4vA$H?F6qK&HO1}}wNun}8jruFN%Z$c z+3EBXqBe>5C8((GB-`2eAp{bCL)4E&eN5CbQFn?O7xiwEpg!+<9tZ14zTmZcOyC7} zhTaSre|t^b<INFov5EV=dEzZ(d6W0Lyai^l$9l8hTkNqv>neClJa+e<jFJVf&!ezC zp65}cf=6z0`aK$@;4Sr-oATZ=kDbjjZw1aOuidQnn91_qDv!lW!CURIwa$BMJXSR; zytO#%JQk}3Z@^>Il=s%*Z1Aw1<h}JCYu9Do1~~;B)}~9njX0Y<w$yoV6V4WoeRQw4 z8E30~*hm+=Egr@bkA?0svd!gVuTWdaQJ69|a1?W;YKvXOi&x@M(WPFTS1(sh2%2Ur zW4ghYm%O{EDD@?^7t`_J(@VU#8?nO4Beep}1Gy41uPo?I&u~1CQBzTEfE;q`gQ)-# zE|U(Zb+g?1O>TH<5q&F|@^G<KoL=lR(Tj$Qv&BPb7Zv9h7nw!H1tuOYE-GSgDPq{# zTkI)f``lCPE%FM#m@ks=i;7;6bwY85Syse|Ttu1omWs=Y==$E_;_@O3d+hZqa90<x zZWR$!skpkxh+JfIIJ}nsykl(<IhBe7Mb@U2NA89qf+`i)7m?5f#SJ){ib$zc+=#Qe zh?Itln{c)O1jM_Go8^>n5Ykw2i=5IK2XXBs*K%oi&v5N9zTx7kVtkS0Rg6k|>7*-) z4EgO;fK3L{DixPf9ikPA@vz1zDo7Ru>RKSh1?q?;T7cN%VC6uDrDC6Av-wavA}wO> zvR=B90+Ev<ASWfvoHWcM+&wlmnUo~CVO}$vF}XZ4IucF^<58p*g`?qgp%c9(7seyu zEU_1xh!}IlUT9!02<dGhz3ql@aftjz!k!TMjfA}+@*4~DA+t+}O$*G!3#rgZ*dHRp zP%<0|mxXk-v2aBw5sq4y7%M}h7&5_z^tF+2O^Ez%3fJPS3$b#92+lqTZ!}yVBD|4s z1J0%p;f;hFaW;ntZzSA=vn51oN5aiGTSI2eu@PuQHxh2aIWI&~*Yb{<(cDPgXwIw- zqmd&Bjqv4=k{jew$iwPvH$c)3pa5zE#*5LgK<h|+u6hM(+pjQNS&#aYJTC@9RHKZ9 zeM*1?Lu^uV5Sj8sRtlgaI!=X@m)G=G0^=K8;!zys*l#A5?7QQ8Nh_oGcH^0e+?IF# zAnC9mPcOE}8z00xsUS>D=YJ+<Pbv6kfhp%oUrgpF@o{F?XzzWPC+C}}@2{{<&Nb!c zu~Y0XF?C(|FBrgPn{wS)#K_I~zTh4v0wh}c36li##%m*H6IL6!F{T~FT6?=QQ#O3W znJH^Ngr$#`dpmO#ZF$rgDfe3=WwhZmE>_BE4?8Pm?jvj<>8FRSkuq8}j*FEt_jzlr zjIXjr%4qtd&RF>_XRKTnIb-Gft+6s%GU1Gt?{-GZuG7wFd5g7HMm67cR?A(^YWXM5 zYT0(2(w&O$d*{K6cdi?#9Y865m1374x?!Gl;SSj*|K*ZeDZ_V}M$7TrTcr(snqH4z ztlqre8YeePB|b#ML?^dt<oy>-q_2XCnj_7hcS<c*K}Ao@75OqTZ&t_t4W_~9Sck|z z(OmjRNH@M))P<t%qw%BhR*~P=4Ej~{Bz{8F2Sq(C>TRORqTV3tDN(;8>Ib536ZMd& zkBa)5s0T&e0~N)c%K9(#zRN_tRn%WW(Rl)Yk<U)AOBCDL0NsIFAc{dC@Vi8zqkSxR z@XmMM$SXKMCQSwBG+WyUa~-25V{|%3t)?6sV~%6gWie(uMt#PZ<rocFl9`Utn8ldk z7)=>tx?=zyr{$RD7%f>I9gfkO#hB_CZ5gB8F{WgUDUQMNBrSTIV@%Crv^qvd#%OVj zX&Iy0F{WpXCdaTV9ors_jxjTf(cl=fGDf{)%+45fj$zkGHji4z=*&tA?kl;>&64CC zgT<5@NSk_v&v)K(V0~TA7Y*(pXo2!tWEP6I&@7U(z;w&$GTnvPGe;XP`}Zi&9w+@& zfpVIbtwr3bkLEjDk&60cj8uK<T>OgeWWg2P$r!2bG`jc|#mRyzijy%aijy%?#c6dJ zRJ0}wu4qlhsAx^bNVR6F%b=n%S#U*VGDbyZGDbyZGDfO0GhL1qeaV6=`jRm!`jRm! z`jRnHeVOZWtSCzsTu~Os$dtu-E6U=0PFWTfyUju|&=fgniX1d$m{pR~lqCVn2@ucj zfTbb$8?$1)SqKq@hoBZfu@(`t3$n(M^C4?B!Pc93kadnb7qZ@wosbPK!W_s($DIw? z<j7f&&5oQ2Y0qTTn;DR;jyoN)&5_d}?Fy;hbU?Pde5XQAbrISjI~+L$a+)LCAX!bR z=&g`5T(TC(nU32GX_r{_rU`Pk<2FLhabyEzr%P53$r?<V)j`g4WG&=;N7g`gIWo7S z(=1Rj`l&dyz8qR#4y`{LA52?cu+~BWSgZ#i7l>S7$u5!Xui&y^u(;TEUM~MU=hu?L zG3Gi(Ibp@&IGv6I>X)(RI982ainv&_9jmq~)-1>3yh@gEreoDtC7j_{4OOwGJ62;A zYno#<Rk1o8tGOz#sgBiB6|3E`-0I2|b&6xPRmEy^tSMD_wK`UNRjd}rnp(wbb}W`_ zSsj}kYg!en(XpmiW#8afGpb_MJC<ARxuVuN)~u>nwT?BridExSZe{2a<{YatjWv#_ z6A6B9%9wy5|Gd;+9(YXN`ITHZuf*47%e%-duv}SlRLccsk#$kNfpn5!g>Rw27XkpX zS~-{YTpMWB`hxYK1q0s;PyHWx^SQL=`n->YRT}ubfb!3PIiHKdK=5DEkai4t!2mPk zz`3;N+Ca0|3)X`cmRxogJLmZ4n&4a${M&~@elZ$y2LG<c!vFuFpBK-i*`E*cJs0I? z0mgq3tpqCEF9<?VG02_ZT;MD(?ggyCDd2!D7XUPtDGab<33xfdTr}|XfYl2m$$Kk7 zs|)>rCj;Vn^+4Ki9kT7fZt!9u;ajo?>u$jU<k3Lh!RB(f^;5yqLD&lxVUK+rV4^(u zxDa?CngW<SP8SaNZ$Ci2Ek-qLuPAKumSwR0dJER4fPKLM_PuA`ETsO~Xnj6K>#2`# z>%*(70Q<FGxsYF$uge1=>C^znC|ZH)a|Mu2oh<+=>c^qwELz`38!qD%f^BL5qh(c4 zeYX&zf@g7ez(b+zRx~Nf-Y;x?7;GG;WMN~A9&`GSWR9`aA<N+D7HZy!6AkZ1SU}Nx zI4dG#dXMB)Dy}S+hrygVKUkb76^o_ex?#{!VcZviaU-z`_$@fxJ%m`ieAwdU@c@nx zbOgpDjC~y#J0-M^kHY~}xWePtS#%zG?JZioeh+v(#SmnU^oBF^o|0SKUdTG~9=5PO z?_DS4ehlOuXV?MwyUzyj3D}sH)B^ZI?m@Fs0N=OBy()NK5mD{sm9?e#BC4I^2mwd1 zgxiCTw{z&ATq^3I5WxLKg8J09QE3>-(Y_Z6`aOf(_Y0@D?Jv22+V7RjENX9&c8dky zAl?XoIFdVz1L{kXy+V=?KrIDAK7%V;^cl^#Q6Ti_0SW*vJvxH)2oV`dfTQ7R8F^a) zv;}G-wu9i?g1q$rhL2h>yy6_eM|T^wV0pX`NANk4vky`m4cCFOgUvh0oo+J<0*}B( z!OC$2%Ofeh)`9hKRx99;!|QJ#HUirl4y2Gh<ylAO<hWkWhy(5qoB{3+yRxWA0`1cx zBe2ou!tTjri{Se)UJ{Rv)Q?#7o~E(IT&`wCXta=o2XfyJd5HE)p?iS*!w3(sUhqAK z01(6|&7Y$8{gTe8p!;i`xKh|YehtuQRPGbgo{4&|sKvr`qYWgFYLAQlN~2`@*TR3_ zA?mkv=4w<gx==`Q9HKta)JOpGA2dp<z5;w6eQllO`Wu3qqd(3|um#<diT=;!b5*PR zK+7lo`Qz`n<;J=j5%E!rVMksJq)kJ8$~i^rM7eK)d((;strJao6jU6MAFxhT{}7lt z_}a^@6XpIMM4e6x;Jz+F?!RT7sO~G^@AO*OYk}$GBi4y>9|Wy$1=IdA<>A!+qDKMs zDeDIuw*PAm+uwZ3Vf&rmbJ%|E=N-0xqr>*SlMdUjxyNDqqYm4j^`yi0Z*$oG;?oY> z|B%D>SH%w7f1kzlqnS@zH@?|o`cYkq?Ux+3e}}{N8y*MSKYjd@H@|d!?~=MDD9j!~ z`}bVhtR{UQZ5@4ekfx4uj|kcyKT<1ovC#fc?BU7FC9og#Yqong@{Atm7^;cymz3id zLq=_}VElJV%of}Kk&Yj7hz@2b<v1dZdAv>J?ZVY>XP0-h=CkG~CZECl(OT|H;~N}4 z+Wgrf|5HDq;owNcH+UnV$>0+Ce_pmqUHj>k@rhFaDaE2<(C-ls^leaAP-U8`!0dzm zV#fex&x7s|quVi>b;@nf=b(V%H)p||7qGUijKQ%1F~Hc<3^+X?1{izF0R9!D-Q~#X z0q3g(1OJKvz@Ekj{uKj&J!Q;sjOi|e*%e<Uc$Q;;uBY)qfRzF0dddI+76WuWWq<&S zF~{XNt>Ux6X&AV=BBx=1>hg8CnDa?D)sbD0z`@FG0VF`M$c2!gz#<nxg8z!_hHP=< zV#sDkE`e-vWDjJcBYPnMd6ldWvfhz-NPt~&J;>S$$qKIC0O=~ApMabrmqJFqE`4c9 zvYb<q%eB~ZYr7SkLo`QgI|%*xSjvBP?Hi7Zlo$0%wXaE-bf$VuTIAT^RQp<7aH@J1 zr61IB71g%Qg{4Y2#raZ=o04UeDxJmW2mMszI$Y#b-KIHTs%z7-q^Y{iaKWjrSy+D1 zPZe#J3rn?Zw)3TG#PP_qGN~qY5}bw!ich)oAXDX<=TfBlHD7C%`ihEmIbW(}3!E=i zG_ZKRDb+O~d8JF$4P;)vRO1#qU#fIVT)I^6dR$nldcDq<Y9A*Rl}oCC_B>*JMF$I1 zAXPvXNs5;0APXq@oEoyeDlxz=lf!l9q8>=jZrd{hRvBNIt(OsRN6@Q#T#R@-xK0hl zh^PD2R!`FN0x-BkAjN6YIl~>?VY9ww=Ue1_EzY;l`C6TCf%COFUzhVualZM^*Y14t zoNuc0&2_#G=j(L7Y0fvt`KCJ`cOX=<8O}G$`DQxbOy`^Bd^4PHw)0JQzB$e}&G|az z^MelOo9ld2oo}A=aludTo9}#6Dn3?G^gcgma}3TMq>NU_;PgStXmN~1E`#QZuM*tk z7#vJU<2O3S;*8PY7(FfnuFtDHJy~#_V{lS3&7jsXSh=N)8pmK!mom6rqCD(bOwK&{ zK_D*+afO$C>tjJEU#gPwpptU9CL)LJlpM5jgxhTC5cz_QxU4#}w9(cRI;OXf2|C(# zOgE7iqOT&kiM&uZks-N>ypS?C+{i5wN(qMx$(F+v=^ZJHi^*bfBI1N@dnp!2J;dTf zL>7x9A7XJLB4cs%Lo7~2WGs$=h{cJBjKwJsu{aTtWzRVfu{aTt#o{1{Se%H+SR4cq zixUwUi-RCyaUvqii-RCyaUvp%#X%6UI1!PtI0zyZCn7Qy2SLQ*L`25oAc$C;h{&?% zAc$C;h;XsaJI2+1%i=^t#^NA|VsRoOV{s5fEY3u@yhzAF5V1HGabidf4U2;yVsSJg zV{s5fEY3${EDnN*#ZieYFAjo;#i@xb76(DZ;s8a);vk4v-Bm0Of{3-aDlZO#h{f%h zEFlL$#OkSHaS%i-4uod0I0zyZD_F}Kr$UJstbR}U)NlfZ{49)Zgz|aE<Yme1vcL?@ zBE$OZT^zXfE)I$9T^#)MoF&}LSI=QIe!k0m=c3T0=UVAp6Z~Sd;2eO#7M-R&2U7ep zHUW3hbe|2gIaYFA{>OT*>uk@yz?O2V?z*LS*?LJuHia_|ruPEI8(o{Tc6tkmSnGV= zn(Nc+cG@t<E4huAE3i>f8r>kIJXlCUL;8ItLaqVkvD`Vxzycx;4B1+JClIS$FV_QE zRp%`pLZ%zpns2ILA%ZG|T41PK>3|U3KV)BC0NN1Zf?ccE0U>z@gcLv`FlrTmWmKjP z<-FX15X&l;A7==C=;xwf1mxgxA&@wEuJ=W~d9Rl%l)S6go2N{ccznlzdzNq(d5cW6 zOxG^C8U}1L&+FB@JGrPx@>$+eK7GWe`#QW;<etyv%N}<$8$B+b=@Q!+!?-S`VX#l{ zuj%5NIO$ckGq=`sdkuhtl<vyfnGCAYkc%+TBPwT#XAe1M_mI?YPq7~WWQ>JJyh~U} z>BS<4xl3qwmE^|$a#&{9Js=Ue64#=IRYY81tm7*j#MG_7^HMm(LLz%X6;e24w}nGC z>k8qnXJ8@Zwq8(3iiqs8PzX%jPlUO)0*35%V2I$4v<SM=h(TDl8!2ds%Z^x$!B8xm zBUFb<T|}i2+C_qbxUa2-d=U-FC#H50b4d}A?ZH80x@@_N%a)6TcPuDMVx{7|A`;tO z>@0d_P7!hKB~eFll{w9~5KGWaMGH{S<o4#;K6MLjq)qm)t<p6%tmb_-M&+hjdfQD7 znGnleh9k0!qre`zBnVT`hZ9!is$eu~g>~3oz`G7%t>ot5Dp~AlppkG@SRM(-)UCq$ z5#%#!@AO$*pjP3!^q1lXKoM1!0^<V~0NE@MM8E@k-%+556qpbW;w~9Rhes`fL0$v) z{vo|fD8~T@e+ctP3qtQY%*{o57bPUK<ss(c!`yIWOwj$o874QPn~=7?h{;{*>(gz? z!`$uTn`0pttVY9bZd8^JbIEc+xX6^p!jW)(h@ke3hV#N+6CD{1=Y*cUPdPJ0TGw(* zvm-=W2S-LiB6FkRQ!+8zLzQ3B&92kzb=>_icWLZl?I`b(n0_qkR#EoK_M1f6{pMRm zJ*LoiiTa_aPmB7NsPBvVyk63yoIfJ!wW5Acl<t57kzo4XEa?WKJ|=3OppZSHo`e^D z$J)rhD0})}*3dusAQ=G3ZK5u;phykUj#}-PY(6nV-Sl$;NWS+{2G#ft`iRVDcS;G? zVceg1;Psp8(1Q4z!bhI|`bX}(z3z6@;ny8LGQaGAk<U2iQiqS!K4gIr$|{&7YW$wV zM?!~>wEocHBbVZ!6`yhN2%kl>@W?F=9%=oSgGW}JbnwW29F*fuhmN#A;LwpJKeXsb zv_d!p7hV0Qfgf~tIieQS@#3d}AoRJ0C#WE3!iw)xe{iJU2Wb&H_LL9MNEiTKb%OR~ zI9~dXNP*tn`j&Cb0DN~W|0!$$d?6@z=?{}d3F^~&P@f7yeHvc%AQv_kn~D0%J{u*S z3D-fp4t_nEop4KZ!*73ng3k>t<=aGie)sQ>C+*2fb3C_j=e4)~?=L;{*kduD2|da; zY{4g*KkHel1XZ-u2vlgP+o68uc)yqh>wjn~ME4s41DPwQ$7iS<0`r(5Q#T{S5C&&Z z0eiD=2&4l*Af*@i+_X#Sg+7<;EKo3nmAP`iZpiQQK>^Y@^L_h?0-NVNpDT4Kf3DAk zx-@O4Z|~FDICFe1)unM}``oNc=~+J4>r#5Azc@=f!(Wo|r~5rA9r)Ay-i+?>?ZE-^ z5B#aVT^CupozEgL{ssONUswP{8-y!W;J5nA9J9q=?ucf8g(I3ED$yGKRgT%<uXaSe zzs3=D5S3`P{(xiFKvbAHU!O|g&X{kHU_^mj7dV(MeDLulA~E?uikzDNj+TPSm5EaY z6=kxPe`%66`VRq@&A|?io}oipf>_xv@a>&DDUi(kgwFTpJ7S*S<p}g9i{I%lbj&&a zB1g>jyB)!21s#E|WbtSCJ&rlu?-d~}>2m}s;;j2tMLfrB_w8!U>dF)!YrO?1td?YS zt8b5**f=dd%c(R@v(KT4ltx`Ly3yxwMapmR`A$Mg*ZZq88okNrT7PXuqdFO#^Vd0A z+QYKS=7R#U-f~$Eaw^19o8+KFa?l|;(xIRKZdXbGvY-(KSV1ESn8J(^mN$|vZ~_<p ztnqmWFo7F?DZMD*%3n$^4AMJ)#OKaq#_tN|X7v1kYkz6{c>(wSQhIKXUi>3%XHc=d z4+V1quK%TRW(V8>OzBxc#oj&?%nUHK>sBFYX9P>KIMajF#y%rRZR>ojR*Nw3&LFj= zcLW7iQB#9{{<rhrhE55V;cW|+^S?D%!T%Qi+o0xP72c*`HUArfHT-YjzYVGn2JqJL z-+F5UedD1f&?g>p{I|?_M|x%jA5s+)smie<!+)dAd$jeKov))UPhr64i!``)+Qwa+ z%($KFl$eR}@I|5HGN?WeeeAsFkMmsl{NmDXtt!8xk8M*qpUppR@dw9umhNZAujW4& zU7i1P#i`~$SN!MVKPzp#jYXg{&huF(lIgQfa`inYxiyBrvGft`r;q5>>6624Shwk> zb(_SU>_-B`|9SsNz-!1;&tyLmU_aQem-0ggf8pG$v~}?Q_L;m_-zz_xo9({+tSuiv zqy<iF9^^0yUjgJ(hAi>7`z<~PZ34f^espjeryKbG;UFsu7V-k)*)HwC(H9BZ%woUI z^!Tm9o3-G`1<gL+1R**=@{l2)UF3r`2Wrr7AS~XAgOy~67}21<RM;r4ys<ug+u$5> ztM?nt_&{&p?m@pU3)2@4$*JCFr0er)y@*iGMx-gT&|1@EgY4aRn`sur<|6O8P;l$B z6H?BC*W&3T@_aPmnJHuAP4Omp;Uod=UYTzIpPiu3o9gMyrc^3>e~Z^ph?mg8s+soN z7gNt-P5U?f9PH7Tqt42umpxWBi@Mf(5vO^PTA+5!%E4Jy4#iY642-BSp!UpSMOfjq zMt%m9MiTdQp6?=~LUqPUgfF~#<6>nT8f?Bd0qYE>8*@`RKX8U2S<KA4g|XEd4!-E* zEjwy?X_x_n?L)M!F`q3tn{LFARf$|sWa$niZ1ffsBFaOpWeyWr%P$AWzF1Z{Yw)bZ zT&94WpR?Z}%X<LHzHg{vFKJYGpV1*&sl&WPq_R1TVrJl%s~7Fw^PIhCxL8|!&R!(b zRJ{$Jv?sOLpvk*Ysn}ASv>TNT#;Mlg<Q=KlUYx8W4Hu^t&+bUr)EbIS!`MsC>Pzl` zbhT<XXw7(5U%?!ARup~OtD4m)d*w5`69<-buK4V(bWxGbfPbdf#0EyLRNdVoGYl^- zo!ya2a8H+{b6KB=%vf=5aa_zQp3Hp{@KP49EAbt+a;d5>sX^wH8eC_<@<_G!W*w<m z)sa+-v?Ddjq-cAT+9+i<6(Q7%nu}e-Wfg>~=!0gi4^h25Z-!mtm(8w9fcj8;F{aF{ z3+*PJ%B(Jg`O;H*4HrtGE>zymC|kp)J$En<M4N|#+8{R+YZ}rIDCo<fW-Q?7$u3P= ze!!^|{nH29u_O%zoXqiMNZJu_en+zrM|_41W*lFKePjS8tsii_Mql@Y;t+}^rD)es zzyTG_N;o!br~O3-N<p^7(!4}`PNfVfrFiwk)zfGu_8mEVAmb2NPXMOC8oo4kz<LzZ z^2z0JSa11tX%3c8bBu(ImS0=VrvXRw!!noCD(;0gdSniVtu}V9njM?4JuF`ys7<q1 za*P}1$mM=DlVeV!@r`tpELx+s3scYd8|3eJZhs?2(dxtpsuN?nJVdw1!ZC)<38Tx0 zXuz}~DC!(awND~WS&<pBJff+xwxZPECLM7NkXC+-&(=k-RC&uHxB1}&9IH+C*Eq() zF7hu8Nxdv%6ocx_5l-=@X(;1}I)e=yK`8M)%@y)e_YtYS=tgX}XW~@o(Xb(0Nd20^ z1*1`TIq@hb+o#K=A_hvutK}y$?41A=o|+uev2>LhX&)huD=;2KiY`~#%cB^9dqY#2 zW={0q{I}md{pdGtJ3mKnInjLC2OfCp_yZrmdM3T0y!eB0(vY0~+D=n*{UcE2W53l( zpNKAeg3pkbqqlx4;grw4-N+<*ahVUhM2~dSF`|{n_z8>X8?AH=Y_Oaf8dL8mVrq<z z&7^z8cO~`7c=@g)w>_FPB@_3(>Ghu@^Y|)sKB{|CzczGApRbK(e^qaPPrWg@*O3CN zFr8wds5h#ktc8lc->i8u_XSe%aWcaBH41$U9C`diygwE722sZizZ+5CwXyO2@+}kp zeo=oQ>Hw+g&C|noJ((nbMhs56OcI09V&&&ZQ&tpCzDyE@!o2vHno3bP2{TC)jRX@^ z{PdqUy+27NiBL9+pP*&OmBl2%k-3XQLldN)EU-ULrttQrwr5w6Q}6Xj`P3VOf%<`X zsx<VO4Zw%pX#4VZ4RdFO{Ht2(L-f?M15PR6$Rq(jR=};31bkgDd34q!FS$(u{pwoH zbayCqCK-M)gkwxU{dU{8%Xc<?P{PTABJ*Xe)8$XJd{6?(*k!Xb692<QM+zCA{_(qf z&+znzn?8^v8Mka|K83M+T-=Nqnd?#M33z8%O+UaoUjA^)?uyRDmm%xu2Y-4y+j4v4 zSc=581}gI2X7zjPe^6DvkIzx1e*K~rAKIhu+u&pJ9?iTq_UCe0?2jq8*X5MkKIMM3 zUipRJ+Ba`s`Em+O)-z==DE~PgK89q*u=-Q+56~cG(|WIb3d-3ZTX_WUxV(vaSl3tZ zs)`A{sN6bM$%pEV&%F9$51i0h!9Ujz&2^-Y-Hoo<aMPL%Fz8YCXW>kOtDlx*_h<ip z-n-G8$W_l|yHS-P{Tn$iB`T$*JR=uBlQ(j?{B(J(zE5ib#0IKX=a>W9pqAwn3y>N! zzKgAxA<9#OMhhYU?hTG{dI$#g?i#PW!Nsr)t<(#7qa@>&;pIT+`SgFq6BqI|`IGB? z-!in{czQ#HZuCyC+pcU$6!#VC3MYGw_3}x?CUc5_f=B+1UYYb>n|(8mS@-X*2RHS0 zRoCO9VokBUtEwJE7#=QJ#?!W>_-UU?MT)s%-SA1Wuwh#Iox0G`;J&AK+DZ^7-ow4P z)BM^WMaSE0>i{@h8g3k(AoWFBit}2z=6u@8{xAp=7!~q^=71Nn2x1b`C(1RIow$r2 zfHIu6-C^)QWk$pB6^=ncIFmcdI_T+F*cgs666V6(=&3QKv78!ih>ce~6JVdCBQ+!A zFH1wopdp-ixr^EymS0XycyHLq7haiGHB~o3@C7-op!6F&hgcg;ydp4i&9Bs%6MMdN z?A}*hcmI=rvz7MZCyzKfTi$AF%(45EN8b4DB)Rv^Pb6QW*WKNEA~|S&B}ulKHzlXd z#rK~WId+w~FiF;$Ur&<r<0Lsb@`}v=rR0%A`!1`?{Kpbae#a`&zma_VB|B?&o-BbV zD*0uM5GPND$F1i&MvCL7ZthpW{mFgX2kHh+^6QKEo=n~>#e({q<cZ(8vGK<7YfdIx z%S`L1PL3QmTmSAf6I6Wi2wkN+eQeJkSbNjJS{VOnJ^v8z(lc4lXJJtJyTJ}e_`1`F zo`1uhG&JSiaEbibiFRm5J~1Ta%&EWUuqH7aOp`@u5J$hR0fal`&+;41Yz++de#`}a z12q41MJ5{DjVRfl1UN*k^|4*{Z^T9R_O}7`+ZS~{>n;ry(R#h6*3|RV`J^a#bzZ!_ ziYE&}gYDm>bJ6%-I2NPz%E&QVFqEv$eD%6U-INBA+cK6vE*_q)YENDZBUM^2mpy|w zMTPY#1Cv8LSFYFlH8vdc$yDoAd8wfBrd5iCug{zAMQim#19<Y7!s2|PtzcocI=`N? zKy^NE*N?4PLifMLr=-(e3gAw?Dem?FJJkTZ01FP|nh_DL_YD&hCx|*dv+)-uEY`qd z7wonT@Jwk%dxjaWB7O<$8R9Ax>x%LDRXlC18;a%gDRxCVY<-)QvYOGXBSn=6_1f82 z1jcT(rWjvvMoii@X9Cn#xmsq$rczWqzap^R*d-{9lUHO)&g<l*f=w+>3}0wVhLH9Y zr*TtzcPcX3U2>%<!*!C6-Xi&6iebdJB6CNe5Shv<k<BGcFQ#^#t_dQu2SdRrRo&29 zoz&UNY<CIT?4lFxusdqR7-*BF?!`?FUz=aM>mA~!3!&7-K#-ss_*sqyuH~|=DPQ1g zKa}&?;RxFx8Fw<&*Xp&Q(MGP$OpG16R5_!c+A5=q3L9m_k47%hY+!f(+T&ZQPTQi< zur?eYxn#&$1**a#=8}4LNtX<ror-UWH?q6CL<<ATk>%FHwcyLo;<2$ifNZOy@m<jo z39%~W<g8I*+3Kny%rCW6L(>tj))bi5f&0i)!*Mva21GQR<C#b+WWCeE@};&C_zv?a z_UKaAHjQE1C`D0g*#aPF5o40L2%V!!U(5k4gbq>|xmS;co#81JBVgN{lB}s9RXC(l zKEN>|fXI<cTFg&py#61aIC=ludyLumM-QL+(cRZIvGI#u`UGp#^2E*W(K`0wbyPNb zWvtJQpIC<uM2k*xs3dy02`z~KPz%_2f@v!r*P{4ZG$5*Zl$Gqnozt6cB6R%J%j&xC z(OUnnr5&A6(N7yG2%o-&d8?=%QIAsJiJG}x`td-{7$L#m7yD{a$Hj|@dScNZKAJqZ z`k#{I3*!HtVq7KaDJb;!l_zn>A6)&-$->L#B@u<3Ab670(X_{?M)|?j?|pX8@o!R- z_@`g4`EvBZj!A;Q%v+;78>LUrED|5^=A)?bbg9)@Atw|v(L71WH&v^f6!11tS^fCH zI<0;8@7~gIOZm-Ikv7S~qMix1k*6Pmot$Bsn>q9|E@l<)JzRa6czFFcGgiDYnU<6v z-X!I-4H^Ft?=7FYb^pHBeb`NzAl1C_cl7UN=$`XFtqP3SWqwJWU!14}OT5uN`gxZ4 z9~E+)^yA6LIvy+EYs(%rJ<P`LX~`-v>rPZ?W7dg}9KMOXCmOH4`$q|vgB~3Cf4tOh A?EnA( literal 0 HcmV?d00001 diff --git a/Resources/metadata-extractor-logo-square.svg b/Resources/metadata-extractor-logo-square.svg new file mode 100644 index 0000000..4177c97 --- /dev/null +++ b/Resources/metadata-extractor-logo-square.svg @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + viewBox="0 0 109.88 107.61465" + height="109.8683" + width="109.8683" + id="svg3336" + inkscape:version="0.91 r13725" + sodipodi:docname="metadata-extractor-logo-square.svg" + inkscape:export-filename="C:\Users\Drew\Dev\metadata-extractor\Resources\favicon-48px.png" + inkscape:export-xdpi="39.319794" + inkscape:export-ydpi="39.319794"> + <metadata + id="metadata3352"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs3350" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="958" + inkscape:window-height="1048" + id="namedview3348" + showgrid="false" + inkscape:zoom="4.5873103" + inkscape:cx="54.934151" + inkscape:cy="54.934147" + inkscape:window-x="-7" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg3336" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + units="px" /> + <path + style="fill:#274c72" + inkscape:connector-curvature="0" + id="path3340" + d="m 0,50.747323 c 0,-13.81 11.21,-25 25.02,-25 1.25,1.25 15.22,15.22 17,17 l -17.05,0 -0.02,0 c -4.42,0 -8,3.58 -8,8 0,4.42 3.58,8 8,8 l 0.02,0 0,0 0.04,0 12.69,0 10.97,0 c 0,0 0,0 0,0 l 9.37,0 17,17 -26.37,0 -10.96,0 -12.69,0 -0.04,0 -0.02,0 c -4.42,0 -8,3.58 -8,8 0,4.42 3.58,8 8,8 l 0.02,0 0,0 0.05,0 23.64,0 c 0,0 0,0 0,0 l 42.36,0 17,17.000007 -83,0 c 0,0 -0.01,0 -0.02,0 -13.81,0 -25,-11.190007 -25,-25.000007 0.01,-6.08 2.23,-11.95 6.25,-16.51 -4.02,-4.56 -6.24,-10.42 -6.25,-16.49 z" /> + <path + style="fill:#404041" + inkscape:connector-curvature="0" + id="path3342" + d="m 51.88,-1.1326776 c -13.81,0 -25,11.2100016 -25,25.0200016 1.25,1.25 15.22,15.22 17,17 l 0,-17.05 0,-0.02 c 0,-4.42 3.58,-8 8,-8 4.42,0 8,3.58 8,8 l 0,0.02 0,0 0,0.04 0,12.69 0,10.97 c 0,0 0,0 0,0 l 0,9.37 17,17 0,-26.37 0,-10.96 0,-12.69 0,-0.04 0,-0.02 c 0,-4.42 3.58,-8 8,-8 4.42,0 8,3.58 8,8 l 0,0.02 0,0 0,0.05 0,23.64 c 0,0 0,0 0,0 l 0,42.36 17,17.000006 0,-83.000006 c 0,0 0,-0.01 0,-0.02 0,-13.81 -11.19,-25.0000016 -25,-25.0000016 -6.08,0.01 -11.95,2.23 -16.51,6.25 -4.55,-4.02 -10.42,-6.24 -16.49,-6.26 z" /> +</svg> diff --git a/Resources/metadata-extractor-logo.svg b/Resources/metadata-extractor-logo.svg new file mode 100644 index 0000000..4ed9017 --- /dev/null +++ b/Resources/metadata-extractor-logo.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 471.31 111.06" height="32mm" width="133mm"> + <g transform="translate(-11.118166,-287.17383)"> + <path d="m11.12 339.05c0-13.81 11.21-25 25.02-25 1.25 1.25 15.22 15.22 17 17l-17.05 0-0.02 0c-4.42 0-8 3.58-8 8 0 4.42 3.58 8 8 8l0.02 0 0 0 0.04 0 12.69 0 10.97 0c0 0 0 0 0 0l9.37 0 17 17-26.37 0-10.96 0-12.69 0-0.04 0-0.02 0c-4.42 0-8 3.58-8 8 0 4.42 3.58 8 8 8l0.02 0 0 0 0.05 0 23.64 0c0 0 0 0 0 0l42.36 0 17 17-83 0c0 0-0.01 0-0.02 0-13.81 0-25-11.19-25-25 0.01-6.08 2.23-11.95 6.25-16.51-4.02-4.56-6.24-10.42-6.25-16.49z" fill="#274c72"/> + <path d="m63 287.17c-13.81 0-25 11.21-25 25.02 1.25 1.25 15.22 15.22 17 17l0-17.05 0-0.02c0-4.42 3.58-8 8-8 4.42 0 8 3.58 8 8l0 0.02 0 0 0 0.04 0 12.69 0 10.97c0 0 0 0 0 0l0 9.37 17 17 0-26.37 0-10.96 0-12.69 0-0.04 0-0.02c0-4.42 3.58-8 8-8 4.42 0 8 3.58 8 8l0 0.02 0 0 0 0.05 0 23.64c0 0 0 0 0 0l0 42.36 17 17 0-83c0 0 0-0.01 0-0.02 0-13.81-11.19-25-25-25-6.08 0.01-11.95 2.23-16.51 6.25C74.94 289.41 69.07 287.19 63 287.17Z" fill="#404041"/> + <path d="m163.96 339.31-34.08 0 0-49.72 34.08 0 0 9.95-24.13 0 0 9.95 16.33 0 0 9.95-16.33 0 0 9.92 24.13 0 0 9.95zM192.85 339.31l-9.92 0 0-39.77-14.94 0 0-9.95 39.77 0 0 9.95-14.91 0 0 39.77zM240.84 319.45l0-9.95q0-2.05-0.8-3.85-0.76-1.84-2.11-3.19-1.35-1.35-3.19-2.11-1.8-0.8-3.85-0.8-2.05 0-3.88 0.8-1.8 0.76-3.16 2.11-1.35 1.35-2.15 3.19-0.76 1.8-0.76 3.85l0 9.95 19.9 0zm9.95 19.87-9.95 0 0-9.95-19.9 0 0 9.95-9.92 0 0-29.82q0-4.13 1.56-7.73 1.56-3.64 4.23-6.34 2.7-2.7 6.31-4.26 3.64-1.56 7.77-1.56 4.13 0 7.73 1.56 3.64 1.56 6.34 4.26 2.7 2.7 4.26 6.34 1.56 3.61 1.56 7.73l0 29.82zM298.62 314.45q0 3.43-0.9 6.62-0.87 3.16-2.5 5.93-1.63 2.74-3.88 5.03-2.25 2.25-5.03 3.88-2.77 1.63-5.96 2.53-3.16 0.87-6.59 0.87l-14.98 0 0-49.72 14.98 0q3.43 0 6.59 0.9 3.19 0.87 5.96 2.5 2.77 1.63 5.03 3.92 2.25 2.25 3.88 5.03 1.63 2.74 2.5 5.93 0.9 3.16 0.9 6.59zm-9.95 0q0-3.09-1.18-5.79-1.14-2.7-3.19-4.72-2.01-2.05-4.75-3.22-2.7-1.18-5.79-1.18l-4.96 0 0 29.82 4.96 0q3.09 0 5.79-1.14 2.74-1.18 4.75-3.19 2.05-2.05 3.19-4.75 1.18-2.74 1.18-5.82zM333.66 319.45l0-9.95q0-2.05-0.8-3.85-0.76-1.84-2.11-3.19-1.35-1.35-3.19-2.11-1.8-0.8-3.85-0.8-2.05 0-3.88 0.8-1.8 0.76-3.16 2.11-1.35 1.35-2.15 3.19-0.76 1.8-0.76 3.85l0 9.95 19.9 0zm9.95 19.87-9.95 0 0-9.95-19.9 0 0 9.95-9.92 0 0-29.82q0-4.13 1.56-7.73 1.56-3.64 4.23-6.34 2.7-2.7 6.31-4.26 3.64-1.56 7.77-1.56 4.13 0 7.73 1.56 3.64 1.56 6.34 4.26 2.7 2.7 4.26 6.34 1.56 3.61 1.56 7.73l0 29.82zM372.37 339.31l-9.92 0 0-39.77-14.94 0 0-9.95 39.77 0 0 9.95-14.91 0 0 39.77zM420.99 319.45l0-9.95q0-2.05-0.8-3.85-0.76-1.84-2.11-3.19-1.35-1.35-3.19-2.11-1.8-0.8-3.85-0.8-2.05 0-3.88 0.8-1.8 0.76-3.16 2.11-1.35 1.35-2.15 3.19-0.76 1.8-0.76 3.85l0 9.95 19.9 0zm9.95 19.87-9.95 0 0-9.95-19.9 0 0 9.95-9.92 0 0-29.82q0-4.13 1.56-7.73 1.56-3.64 4.23-6.34 2.7-2.7 6.31-4.26 3.64-1.56 7.77-1.56 4.13 0 7.73 1.56 3.64 1.56 6.34 4.26 2.7 2.7 4.26 6.34 1.56 3.61 1.56 7.73l0 29.82z" fill="#404041"/> + <path d="m171.5 397.2-11.67 0-9.25-15.52-9.29 15.52-11.6 0 14.75-24.04-14.75-24.04 11.6 0 9.29 15.52 9.25-15.52 11.67 0-14.75 24.04 14.75 24.04zM197.93 397.2l-9.59 0 0-38.45-14.45 0 0-9.62 38.45 0 0 9.62-14.42 0 0 38.45zM227.06 358.75l0 19.24 9.62 0q1.98 0 3.72-0.74 1.74-0.77 3.05-2.08 1.31-1.31 2.04-3.05 0.77-1.78 0.77-3.75 0-1.98-0.77-3.72-0.74-1.78-2.04-3.08-1.31-1.31-3.05-2.04-1.74-0.77-3.72-0.77l-9.62 0zm0 38.45-9.62 0 0-48.07 19.24 0q2.65 0 5.1 0.7 2.45 0.67 4.56 1.94 2.15 1.24 3.89 3.02 1.78 1.74 3.02 3.89 1.27 2.15 1.94 4.59 0.7 2.45 0.7 5.1 0 2.48-0.64 4.83-0.6 2.35-1.78 4.46-1.14 2.11-2.82 3.89-1.68 1.78-3.75 3.08l5.33 12.57-10.22 0-4.19-9.69-10.76 0.07 0 9.62zM290.17 377.99l0-9.62q0-1.98-0.77-3.72-0.74-1.78-2.04-3.08-1.31-1.31-3.08-2.04-1.74-0.77-3.72-0.77-1.98 0-3.75 0.77-1.74 0.74-3.05 2.04-1.31 1.31-2.08 3.08-0.74 1.74-0.74 3.72l0 9.62 19.24 0zm9.62 19.21-9.62 0 0-9.62-19.24 0 0 9.62-9.59 0 0-28.83q0-3.99 1.51-7.48 1.51-3.52 4.09-6.13 2.61-2.61 6.1-4.12 3.52-1.51 7.51-1.51 3.99 0 7.48 1.51 3.52 1.51 6.13 4.12 2.61 2.61 4.12 6.13 1.51 3.49 1.51 7.48l0 28.83zM345.47 392.27q-3.35 2.88-7.51 4.43-4.16 1.54-8.62 1.54-3.42 0-6.6-0.91-3.15-0.87-5.93-2.48-2.75-1.64-5.03-3.92-2.28-2.28-3.92-5.03-1.61-2.78-2.51-5.93-0.87-3.18-0.87-6.6 0-3.42 0.87-6.6 0.91-3.18 2.51-5.93 1.64-2.78 3.92-5.06 2.28-2.28 5.03-3.89 2.78-1.64 5.93-2.51 3.18-0.91 6.6-0.91 4.46 0 8.62 1.54 4.16 1.51 7.51 4.43l-5.1 8.38q-2.11-2.28-4.99-3.49-2.88-1.24-6.03-1.24-3.18 0-5.97 1.21-2.78 1.21-4.86 3.29-2.08 2.04-3.29 4.86-1.21 2.78-1.21 5.93 0 3.15 1.21 5.93 1.21 2.75 3.29 4.83 2.08 2.08 4.86 3.29 2.78 1.21 5.97 1.21 3.15 0 6.03-1.21 2.88-1.24 4.99-3.52l5.1 8.38zM373.01 397.2l-9.59 0 0-38.45-14.45 0 0-9.62 38.45 0 0 9.62-14.42 0 0 38.45zM437.51 373.36q0 3.42-0.91 6.6-0.87 3.15-2.48 5.93-1.61 2.75-3.89 5.03-2.28 2.28-5.03 3.92-2.75 1.61-5.93 2.48-3.18 0.91-6.6 0.91-3.42 0-6.6-0.91-3.15-0.87-5.93-2.48-2.75-1.64-5.03-3.92-2.28-2.28-3.92-5.03-1.61-2.78-2.51-5.93-0.87-3.18-0.87-6.6 0-3.42 0.87-6.6 0.91-3.18 2.51-5.93 1.64-2.75 3.92-5.03 2.28-2.28 5.03-3.89 2.78-1.61 5.93-2.48 3.18-0.91 6.6-0.91 3.42 0 6.6 0.91 3.18 0.87 5.93 2.48 2.75 1.61 5.03 3.89 2.28 2.28 3.89 5.03 1.61 2.75 2.48 5.93 0.91 3.18 0.91 6.6zm-9.55 0q0-3.15-1.21-5.93-1.21-2.82-3.29-4.86-2.04-2.08-4.86-3.29-2.78-1.21-5.93-1.21-3.18 0-5.97 1.21-2.78 1.21-4.86 3.29-2.08 2.04-3.29 4.86-1.21 2.78-1.21 5.93 0 3.15 1.21 5.93 1.21 2.75 3.29 4.83 2.08 2.08 4.86 3.29 2.78 1.21 5.97 1.21 3.15 0 5.93-1.21 2.82-1.21 4.86-3.29 2.08-2.08 3.29-4.83 1.21-2.78 1.21-5.93zM453.6 358.75l0 19.24 9.62 0q1.98 0 3.72-0.74 1.74-0.77 3.05-2.08 1.31-1.31 2.04-3.05 0.77-1.78 0.77-3.75 0-1.98-0.77-3.72-0.74-1.78-2.04-3.08-1.31-1.31-3.05-2.04-1.74-0.77-3.72-0.77l-9.62 0zm0 38.45-9.62 0 0-48.07 19.24 0q2.65 0 5.1 0.7 2.45 0.67 4.56 1.94 2.15 1.24 3.89 3.02 1.78 1.74 3.02 3.89 1.27 2.15 1.94 4.59 0.7 2.45 0.7 5.1 0 2.48-0.64 4.83-0.6 2.35-1.78 4.46-1.14 2.11-2.82 3.89-1.68 1.78-3.75 3.08l5.33 12.57-10.22 0-4.19-9.69-10.76 0.07 0 9.62z" fill="#274c72"/> + </g> +</svg> diff --git a/Samples/com/drew/metadata/GeoTagMapBuilder.java b/Samples/com/drew/metadata/GeoTagMapBuilder.java index 769bbb6..cd4de5a 100644 --- a/Samples/com/drew/metadata/GeoTagMapBuilder.java +++ b/Samples/com/drew/metadata/GeoTagMapBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,21 +66,28 @@ public class GeoTagMapBuilder } }); + if (files == null) + { + System.err.println("No matching files found."); + System.exit(1); + } + Collection<PhotoLocation> photoLocations = new ArrayList<PhotoLocation>(); for (File file : files) { // Read all metadata from the image Metadata metadata = ImageMetadataReader.readMetadata(file); // See whether it has GPS data - GpsDirectory gpsDirectory = metadata.getDirectory(GpsDirectory.class); - if (gpsDirectory == null) - continue; - // Try to read out the location, making sure it's non-zero - GeoLocation geoLocation = gpsDirectory.getGeoLocation(); - if (geoLocation == null || geoLocation.isZero()) - continue; - // Add to our collection for use below - photoLocations.add(new PhotoLocation(geoLocation, file)); + Collection<GpsDirectory> gpsDirectories = metadata.getDirectoriesOfType(GpsDirectory.class); + for (GpsDirectory gpsDirectory : gpsDirectories) { + // Try to read out the location, making sure it's non-zero + GeoLocation geoLocation = gpsDirectory.getGeoLocation(); + if (geoLocation != null && !geoLocation.isZero()) { + // Add to our collection for use below + photoLocations.add(new PhotoLocation(geoLocation, file)); + break; + } + } } // Write output to the console. diff --git a/Samples/com/drew/metadata/SampleUsage.java b/Samples/com/drew/metadata/SampleUsage.java index db52f0b..74e295a 100644 --- a/Samples/com/drew/metadata/SampleUsage.java +++ b/Samples/com/drew/metadata/SampleUsage.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,7 +34,7 @@ import java.util.Arrays; /** * Showcases the most popular ways of using the metadata-extractor library. - * <p/> + * <p> * For more information, see the project wiki: https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted * * @author Drew Noakes https://drewnoakes.com diff --git a/Samples/com/drew/metadata/XmpSample.java b/Samples/com/drew/metadata/XmpSample.java new file mode 100644 index 0000000..f52459c --- /dev/null +++ b/Samples/com/drew/metadata/XmpSample.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPIterator; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.properties.XMPPropertyInfo; +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.xmp.XmpDirectory; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Shows basic extraction and iteration of XMP data. + * <p> + * For more information, see the project wiki: https://github.com/drewnoakes/metadata-extractor/wiki/GettingStarted + * + * @author Drew Noakes https://drewnoakes.com + */ +public class XmpSample +{ + private static void xmpSample(InputStream imageStream) throws XMPException, ImageProcessingException, IOException + { + // Extract metadata from the image + Metadata metadata = ImageMetadataReader.readMetadata(imageStream); + + // Iterate through any XMP directories we may have received + for (XmpDirectory xmpDirectory : metadata.getDirectoriesOfType(XmpDirectory.class)) { + + // Usually with metadata-extractor, you iterate a directory's tags. However XMP has + // a complex structure with many potentially unknown properties. This doesn't map + // well to metadata-extractor's directory-and-tag model. + // + // If you need to use XMP data, access the XMPMeta object directly. + XMPMeta xmpMeta = xmpDirectory.getXMPMeta(); + + XMPIterator itr = xmpMeta.iterator(); + + // Iterate XMP properties + while (itr.hasNext()) { + + XMPPropertyInfo property = (XMPPropertyInfo) itr.next(); + + // Print details of the property + System.out.println(property.getPath() + ": " + property.getValue()); + } + } + } +} diff --git a/Source/com/drew/imaging/FileType.java b/Source/com/drew/imaging/FileType.java index c9b2796..07e6f13 100644 --- a/Source/com/drew/imaging/FileType.java +++ b/Source/com/drew/imaging/FileType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,8 @@ public enum FileType Bmp, Gif, Ico, + Pcx, + Riff, /** Sony camera raw. */ Arw, diff --git a/Source/com/drew/imaging/FileTypeDetector.java b/Source/com/drew/imaging/FileTypeDetector.java index 2e379c3..d5a7b83 100644 --- a/Source/com/drew/imaging/FileTypeDetector.java +++ b/Source/com/drew/imaging/FileTypeDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,12 +49,19 @@ public class FileTypeDetector _root.addPath(FileType.Gif, "GIF87a".getBytes()); _root.addPath(FileType.Gif, "GIF89a".getBytes()); _root.addPath(FileType.Ico, new byte[]{0x00, 0x00, 0x01, 0x00}); + _root.addPath(FileType.Pcx, new byte[]{0x0A, 0x00, 0x01}); // multiple PCX versions, explicitly listed + _root.addPath(FileType.Pcx, new byte[]{0x0A, 0x02, 0x01}); + _root.addPath(FileType.Pcx, new byte[]{0x0A, 0x03, 0x01}); + _root.addPath(FileType.Pcx, new byte[]{0x0A, 0x05, 0x01}); + _root.addPath(FileType.Riff, "RIFF".getBytes()); _root.addPath(FileType.Arw, "II".getBytes(), new byte[]{0x2a, 0x00, 0x08, 0x00}); _root.addPath(FileType.Crw, "II".getBytes(), new byte[]{0x1a, 0x00, 0x00, 0x00}, "HEAPCCDR".getBytes()); _root.addPath(FileType.Cr2, "II".getBytes(), new byte[]{0x2a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x43, 0x52}); _root.addPath(FileType.Nef, "MM".getBytes(), new byte[]{0x00, 0x2a, 0x00, 0x00, 0x00, (byte)0x80, 0x00}); _root.addPath(FileType.Orf, "IIRO".getBytes(), new byte[]{(byte)0x08, 0x00}); + _root.addPath(FileType.Orf, "MMOR".getBytes(), new byte[]{(byte)0x00, 0x00}); + _root.addPath(FileType.Orf, "IIRS".getBytes(), new byte[]{(byte)0x08, 0x00}); _root.addPath(FileType.Raf, "FUJIFILMCCD-RAW".getBytes()); _root.addPath(FileType.Rw2, "II".getBytes(), new byte[]{0x55, 0x00}); } @@ -77,6 +84,9 @@ public class FileTypeDetector @NotNull public static FileType detectFileType(@NotNull final BufferedInputStream inputStream) throws IOException { + if (!inputStream.markSupported()) + throw new IOException("Stream must support mark/reset"); + int maxByteCount = _root.getMaxDepth(); inputStream.mark(maxByteCount); diff --git a/Source/com/drew/imaging/ImageMetadataReader.java b/Source/com/drew/imaging/ImageMetadataReader.java index 5461be2..1d154a7 100644 --- a/Source/com/drew/imaging/ImageMetadataReader.java +++ b/Source/com/drew/imaging/ImageMetadataReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,10 +22,15 @@ package com.drew.imaging; import com.drew.imaging.bmp.BmpMetadataReader; import com.drew.imaging.gif.GifMetadataReader; +import com.drew.imaging.ico.IcoMetadataReader; import com.drew.imaging.jpeg.JpegMetadataReader; +import com.drew.imaging.pcx.PcxMetadataReader; import com.drew.imaging.png.PngMetadataReader; import com.drew.imaging.psd.PsdMetadataReader; +import com.drew.imaging.raf.RafMetadataReader; import com.drew.imaging.tiff.TiffMetadataReader; +import com.drew.imaging.webp.WebpMetadataReader; +import com.drew.lang.RandomAccessStreamReader; import com.drew.lang.StringUtil; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Directory; @@ -33,7 +38,7 @@ import com.drew.metadata.Metadata; import com.drew.metadata.MetadataException; import com.drew.metadata.Tag; import com.drew.metadata.exif.ExifIFD0Directory; -import com.drew.metadata.exif.ExifThumbnailDirectory; +import com.drew.metadata.file.FileMetadataReader; import java.io.*; import java.util.ArrayList; @@ -41,17 +46,25 @@ import java.util.Arrays; import java.util.Collection; /** - * Obtains {@link Metadata} from all supported file formats. + * Reads metadata from any supported file format. * <p> - * This class a lightweight wrapper around specific file type processors: + * This class a lightweight wrapper around other, specific metadata processors. + * During extraction, the file type is determined from the first few bytes of the file. + * Parsing is then delegated to one of: + * * <ul> * <li>{@link JpegMetadataReader} for JPEG files</li> * <li>{@link TiffMetadataReader} for TIFF and (most) RAW files</li> * <li>{@link PsdMetadataReader} for Photoshop files</li> - * <li>{@link PngMetadataReader} for BMP files</li> + * <li>{@link PngMetadataReader} for PNG files</li> * <li>{@link BmpMetadataReader} for BMP files</li> * <li>{@link GifMetadataReader} for GIF files</li> + * <li>{@link IcoMetadataReader} for ICO files</li> + * <li>{@link PcxMetadataReader} for PCX files</li> + * <li>{@link WebpMetadataReader} for WebP files</li> + * <li>{@link RafMetadataReader} for RAF files</li> * </ul> + * * If you know the file type you're working with, you may use one of the above processors directly. * For most scenarios it is simpler, more convenient and more robust to use this class. * <p> @@ -64,17 +77,6 @@ public class ImageMetadataReader { /** * Reads metadata from an {@link InputStream}. - * <p> - * The file type is determined by inspecting the leading bytes of the stream, and parsing of the file - * is delegated to one of: - * <ul> - * <li>{@link JpegMetadataReader} for JPEG files</li> - * <li>{@link TiffMetadataReader} for TIFF and (most) RAW files</li> - * <li>{@link PsdMetadataReader} for Photoshop files</li> - * <li>{@link PngMetadataReader} for PNG files</li> - * <li>{@link BmpMetadataReader} for BMP files</li> - * <li>{@link GifMetadataReader} for GIF files</li> - * </ul> * * @param inputStream a stream from which the file data may be read. The stream must be positioned at the * beginning of the file's data. @@ -83,6 +85,21 @@ public class ImageMetadataReader */ @NotNull public static Metadata readMetadata(@NotNull final InputStream inputStream) throws ImageProcessingException, IOException + { + return readMetadata(inputStream, -1); + } + + /** + * Reads metadata from an {@link InputStream} of known length. + * + * @param inputStream a stream from which the file data may be read. The stream must be positioned at the + * beginning of the file's data. + * @param streamLength the length of the stream, if known, otherwise -1. + * @return a populated {@link Metadata} object containing directories of tags with values and any processing errors. + * @throws ImageProcessingException if the file type is unknown, or for general processing errors. + */ + @NotNull + public static Metadata readMetadata(@NotNull final InputStream inputStream, final long streamLength) throws ImageProcessingException, IOException { BufferedInputStream bufferedInputStream = inputStream instanceof BufferedInputStream ? (BufferedInputStream)inputStream @@ -99,7 +116,7 @@ public class ImageMetadataReader fileType == FileType.Nef || fileType == FileType.Orf || fileType == FileType.Rw2) - return TiffMetadataReader.readMetadata(bufferedInputStream); + return TiffMetadataReader.readMetadata(new RandomAccessStreamReader(bufferedInputStream, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, streamLength)); if (fileType == FileType.Psd) return PsdMetadataReader.readMetadata(bufferedInputStream); @@ -113,22 +130,23 @@ public class ImageMetadataReader if (fileType == FileType.Gif) return GifMetadataReader.readMetadata(bufferedInputStream); + if (fileType == FileType.Ico) + return IcoMetadataReader.readMetadata(bufferedInputStream); + + if (fileType == FileType.Pcx) + return PcxMetadataReader.readMetadata(bufferedInputStream); + + if (fileType == FileType.Riff) + return WebpMetadataReader.readMetadata(bufferedInputStream); + + if (fileType == FileType.Raf) + return RafMetadataReader.readMetadata(bufferedInputStream); + throw new ImageProcessingException("File format is not supported"); } /** * Reads {@link Metadata} from a {@link File} object. - * <p> - * The file type is determined by inspecting the leading bytes of the stream, and parsing of the file - * is delegated to one of: - * <ul> - * <li>{@link JpegMetadataReader} for JPEG files</li> - * <li>{@link TiffMetadataReader} for TIFF and (most) RAW files</li> - * <li>{@link PsdMetadataReader} for Photoshop files</li> - * <li>{@link PngMetadataReader} for PNG files</li> - * <li>{@link BmpMetadataReader} for BMP files</li> - * <li>{@link GifMetadataReader} for GIF files</li> - * </ul> * * @param file a file from which the image data may be read. * @return a populated {@link Metadata} object containing directories of tags with values and any processing errors. @@ -138,11 +156,14 @@ public class ImageMetadataReader public static Metadata readMetadata(@NotNull final File file) throws ImageProcessingException, IOException { InputStream inputStream = new FileInputStream(file); + Metadata metadata; try { - return readMetadata(inputStream); + metadata = readMetadata(inputStream, file.length()); } finally { inputStream.close(); } + new FileMetadataReader().read(file, metadata); + return metadata; } private ImageMetadataReader() throws Exception @@ -166,7 +187,6 @@ public class ImageMetadataReader public static void main(@NotNull String[] args) throws MetadataException, IOException { Collection<String> argList = new ArrayList<String>(Arrays.asList(args)); - boolean thumbRequested = argList.remove("-thumb"); boolean markdownFormat = argList.remove("-markdown"); boolean showHex = argList.remove("-hex"); @@ -183,7 +203,7 @@ public class ImageMetadataReader File file = new File(filePath); if (!markdownFormat && argList.size()>1) - System.out.printf("\n***** PROCESSING: %s\n%n", filePath); + System.out.printf("\n***** PROCESSING: %s%n%n", filePath); Metadata metadata = null; try { @@ -198,8 +218,8 @@ public class ImageMetadataReader if (markdownFormat) { String fileName = file.getName(); - String urlName = StringUtil.urlEncode(fileName); - ExifIFD0Directory exifIFD0Directory = metadata.getDirectory(ExifIFD0Directory.class); + String urlName = StringUtil.urlEncode(filePath); + ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); String make = exifIFD0Directory == null ? "" : exifIFD0Directory.getString(ExifIFD0Directory.TAG_MAKE); String model = exifIFD0Directory == null ? "" : exifIFD0Directory.getString(ExifIFD0Directory.TAG_MODEL); System.out.println(); @@ -234,9 +254,8 @@ public class ImageMetadataReader Integer.toHexString(tag.getTagType()), tagName, description); - } - else - { + } else { + // simple formatting if (showHex) { System.out.printf("[%s - %s] %s = %s%n", directoryName, tag.getTagTypeHex(), tagName, description); } else { @@ -249,16 +268,6 @@ public class ImageMetadataReader for (String error : directory.getErrors()) System.err.println("ERROR: " + error); } - - if (args.length > 1 && thumbRequested) { - ExifThumbnailDirectory directory = metadata.getDirectory(ExifThumbnailDirectory.class); - if (directory!=null && directory.hasThumbnailData()) { - System.out.println("Writing thumbnail..."); - directory.writeThumbnail(args[0].trim() + ".thumb.jpg"); - } else { - System.out.println("No thumbnail data exists in this image"); - } - } } } } diff --git a/Source/com/drew/imaging/ImageProcessingException.java b/Source/com/drew/imaging/ImageProcessingException.java index ebfa440..0d15eba 100644 --- a/Source/com/drew/imaging/ImageProcessingException.java +++ b/Source/com/drew/imaging/ImageProcessingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/imaging/PhotographicConversions.java b/Source/com/drew/imaging/PhotographicConversions.java index 913acac..19a3a5c 100644 --- a/Source/com/drew/imaging/PhotographicConversions.java +++ b/Source/com/drew/imaging/PhotographicConversions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/imaging/bmp/BmpMetadataReader.java b/Source/com/drew/imaging/bmp/BmpMetadataReader.java index d672283..9c94ca8 100644 --- a/Source/com/drew/imaging/bmp/BmpMetadataReader.java +++ b/Source/com/drew/imaging/bmp/BmpMetadataReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/imaging/bmp/package-info.java b/Source/com/drew/imaging/bmp/package-info.java new file mode 100644 index 0000000..eabaca6 --- /dev/null +++ b/Source/com/drew/imaging/bmp/package-info.java @@ -0,0 +1,6 @@ +/** + * Contains classes for working with BMP files. + * + * @since 2.7.0 + */ +package com.drew.imaging.bmp; diff --git a/Source/com/drew/imaging/bmp/package.html b/Source/com/drew/imaging/bmp/package.html deleted file mode 100644 index 8a26207..0000000 --- a/Source/com/drew/imaging/bmp/package.html +++ /dev/null @@ -1,34 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for working with BMP files. - -<!-- Put @see and @since tags down here. --> -@since 2.7.0 - -</body> -</html> diff --git a/Source/com/drew/imaging/gif/GifMetadataReader.java b/Source/com/drew/imaging/gif/GifMetadataReader.java index b2a09b6..964d6b0 100644 --- a/Source/com/drew/imaging/gif/GifMetadataReader.java +++ b/Source/com/drew/imaging/gif/GifMetadataReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ package com.drew.imaging.gif; import com.drew.lang.StreamReader; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Metadata; +import com.drew.metadata.file.FileMetadataReader; import com.drew.metadata.gif.GifReader; import java.io.File; @@ -41,15 +42,15 @@ public class GifMetadataReader @NotNull public static Metadata readMetadata(@NotNull File file) throws IOException { - FileInputStream stream = null; + InputStream inputStream = new FileInputStream(file); + Metadata metadata; try { - stream = new FileInputStream(file); - return readMetadata(stream); + metadata = readMetadata(inputStream); } finally { - if (stream != null) { - stream.close(); - } + inputStream.close(); } + new FileMetadataReader().read(file, metadata); + return metadata; } @NotNull diff --git a/Source/com/drew/imaging/gif/package-info.java b/Source/com/drew/imaging/gif/package-info.java new file mode 100644 index 0000000..2004e18 --- /dev/null +++ b/Source/com/drew/imaging/gif/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for working with GIF files. + */ +package com.drew.imaging.gif; diff --git a/Source/com/drew/imaging/gif/package.html b/Source/com/drew/imaging/gif/package.html deleted file mode 100644 index 8e1f885..0000000 --- a/Source/com/drew/imaging/gif/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for working with GIF files. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/imaging/ico/IcoMetadataReader.java b/Source/com/drew/imaging/ico/IcoMetadataReader.java new file mode 100644 index 0000000..8372a73 --- /dev/null +++ b/Source/com/drew/imaging/ico/IcoMetadataReader.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.ico; + +import com.drew.lang.StreamReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; +import com.drew.metadata.file.FileMetadataReader; +import com.drew.metadata.ico.IcoReader; + +import java.io.*; + +/** + * Obtains metadata from ICO (Windows Icon) files. + * + * @author Drew Noakes https://drewnoakes.com + */ +public class IcoMetadataReader +{ + @NotNull + public static Metadata readMetadata(@NotNull File file) throws IOException + { + InputStream inputStream = new FileInputStream(file); + Metadata metadata; + try { + metadata = readMetadata(inputStream); + } finally { + inputStream.close(); + } + new FileMetadataReader().read(file, metadata); + return metadata; + } + + @NotNull + public static Metadata readMetadata(@NotNull InputStream inputStream) + { + Metadata metadata = new Metadata(); + new IcoReader().extract(new StreamReader(inputStream), metadata); + return metadata; + } +} diff --git a/Source/com/drew/imaging/ico/package-info.java b/Source/com/drew/imaging/ico/package-info.java new file mode 100644 index 0000000..762b1c2 --- /dev/null +++ b/Source/com/drew/imaging/ico/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for working with ICO (Windows Icon) files. + */ +package com.drew.imaging.ico; diff --git a/Source/com/drew/imaging/jpeg/JpegMetadataReader.java b/Source/com/drew/imaging/jpeg/JpegMetadataReader.java index 20e3f7f..e4f0918 100644 --- a/Source/com/drew/imaging/jpeg/JpegMetadataReader.java +++ b/Source/com/drew/imaging/jpeg/JpegMetadataReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,11 +26,16 @@ import com.drew.lang.annotations.Nullable; import com.drew.metadata.Metadata; import com.drew.metadata.adobe.AdobeJpegReader; import com.drew.metadata.exif.ExifReader; +import com.drew.metadata.file.FileMetadataReader; import com.drew.metadata.icc.IccReader; import com.drew.metadata.iptc.IptcReader; import com.drew.metadata.jfif.JfifReader; +import com.drew.metadata.jfxx.JfxxReader; import com.drew.metadata.jpeg.JpegCommentReader; +import com.drew.metadata.jpeg.JpegDhtReader; +import com.drew.metadata.jpeg.JpegDnlReader; import com.drew.metadata.jpeg.JpegReader; +import com.drew.metadata.photoshop.DuckyReader; import com.drew.metadata.photoshop.PhotoshopReader; import com.drew.metadata.xmp.XmpReader; @@ -53,12 +58,16 @@ public class JpegMetadataReader new JpegReader(), new JpegCommentReader(), new JfifReader(), + new JfxxReader(), new ExifReader(), new XmpReader(), new IccReader(), new PhotoshopReader(), + new DuckyReader(), new IptcReader(), - new AdobeJpegReader() + new AdobeJpegReader(), + new JpegDhtReader(), + new JpegDnlReader() ); @NotNull @@ -78,15 +87,15 @@ public class JpegMetadataReader @NotNull public static Metadata readMetadata(@NotNull File file, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException { - InputStream inputStream = null; - try - { - inputStream = new FileInputStream(file); - return readMetadata(inputStream, readers); + InputStream inputStream = new FileInputStream(file); + Metadata metadata; + try { + metadata = readMetadata(inputStream, readers); } finally { - if (inputStream != null) - inputStream.close(); + inputStream.close(); } + new FileMetadataReader().read(file, metadata); + return metadata; } @NotNull @@ -122,11 +131,7 @@ public class JpegMetadataReader // Pass the appropriate byte arrays to each reader. for (JpegSegmentMetadataReader reader : readers) { for (JpegSegmentType segmentType : reader.getSegmentTypes()) { - for (byte[] segmentBytes : segmentData.getSegments(segmentType)) { - if (reader.canProcess(segmentBytes, segmentType)) { - reader.extract(segmentBytes, metadata, segmentType); - } - } + reader.readJpegSegments(segmentData.getSegments(segmentType), metadata, segmentType); } } } diff --git a/Source/com/drew/imaging/jpeg/JpegProcessingException.java b/Source/com/drew/imaging/jpeg/JpegProcessingException.java index 78fd61a..61e2b09 100644 --- a/Source/com/drew/imaging/jpeg/JpegProcessingException.java +++ b/Source/com/drew/imaging/jpeg/JpegProcessingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/imaging/jpeg/JpegSegmentData.java b/Source/com/drew/imaging/jpeg/JpegSegmentData.java index 70695e5..11c367e 100644 --- a/Source/com/drew/imaging/jpeg/JpegSegmentData.java +++ b/Source/com/drew/imaging/jpeg/JpegSegmentData.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java b/Source/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java index eca8a3a..e1d2aef 100644 --- a/Source/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java +++ b/Source/com/drew/imaging/jpeg/JpegSegmentMetadataReader.java @@ -12,21 +12,15 @@ public interface JpegSegmentMetadataReader * Gets the set of JPEG segment types that this reader is interested in. */ @NotNull - public Iterable<JpegSegmentType> getSegmentTypes(); + Iterable<JpegSegmentType> getSegmentTypes(); /** - * Gets a value indicating whether the supplied byte data can be processed by this reader. This is not a guarantee - * that no errors will occur, but rather a best-effort indication of whether the parse is likely to succeed. - * Implementations are expected to check things such as the opening bytes, data length, etc. - */ - public boolean canProcess(@NotNull final byte[] segmentBytes, @NotNull final JpegSegmentType segmentType); - - /** - * Extracts metadata from a JPEG segment's byte array and merges it into the specified {@link Metadata} object. + * Extracts metadata from all instances of a particular JPEG segment type. * - * @param segmentBytes The byte array from which the metadata should be extracted. + * @param segments A sequence of byte arrays from which the metadata should be extracted. These are in the order + * encountered in the original file. * @param metadata The {@link Metadata} object into which extracted values should be merged. * @param segmentType The {@link JpegSegmentType} being read. */ - public void extract(@NotNull final byte[] segmentBytes, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType); + void readJpegSegments(@NotNull final Iterable<byte[]> segments, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType); } diff --git a/Source/com/drew/imaging/jpeg/JpegSegmentReader.java b/Source/com/drew/imaging/jpeg/JpegSegmentReader.java index d8e6876..a6b0b5c 100644 --- a/Source/com/drew/imaging/jpeg/JpegSegmentReader.java +++ b/Source/com/drew/imaging/jpeg/JpegSegmentReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,11 @@ import java.util.Set; */ public class JpegSegmentReader { + /** + * The 0xFF byte that signals the start of a segment. + */ + private static final byte SEGMENT_IDENTIFIER = (byte) 0xFF; + /** * Private, because this segment crashes my algorithm, and searching for it doesn't work (yet). */ @@ -111,19 +116,14 @@ public class JpegSegmentReader // Find the segment marker. Markers are zero or more 0xFF bytes, followed // by a 0xFF and then a byte not equal to 0x00 or 0xFF. - final short segmentIdentifier = reader.getUInt8(); - - // We must have at least one 0xFF byte - if (segmentIdentifier != 0xFF) - throw new JpegProcessingException("Expected JPEG segment start identifier 0xFF, not 0x" + Integer.toHexString(segmentIdentifier).toUpperCase()); - - // Read until we have a non-0xFF byte. This identifies the segment type. + byte segmentIdentifier = reader.getInt8(); byte segmentType = reader.getInt8(); - while (segmentType == (byte)0xFF) - segmentType = reader.getInt8(); - if (segmentType == 0) - throw new JpegProcessingException("Expected non-zero byte as part of JPEG marker identifier"); + // Read until we have a 0xFF byte followed by a byte that is not 0xFF or 0x00 + while (segmentIdentifier != SEGMENT_IDENTIFIER || segmentType == SEGMENT_IDENTIFIER || segmentType == 0) { + segmentIdentifier = segmentType; + segmentType = reader.getInt8(); + } if (segmentType == SEGMENT_SOS) { // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would diff --git a/Source/com/drew/imaging/jpeg/JpegSegmentType.java b/Source/com/drew/imaging/jpeg/JpegSegmentType.java index d76414e..521e478 100644 --- a/Source/com/drew/imaging/jpeg/JpegSegmentType.java +++ b/Source/com/drew/imaging/jpeg/JpegSegmentType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,17 +29,22 @@ import java.util.List; /** * An enumeration of the known segment types found in JPEG files. * + * <ul> + * <li>http://www.ozhiker.com/electronics/pjmt/jpeg_info/app_segments.html</li> + * <li>http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html</li> + * </ul> + * * @author Drew Noakes https://drewnoakes.com */ public enum JpegSegmentType { - /** APP0 JPEG segment identifier -- JFIF data (also JFXX apparently). */ + /** APP0 JPEG segment identifier. Commonly contains JFIF, JFXX. */ APP0((byte)0xE0, true), - /** APP1 JPEG segment identifier -- where Exif data is kept. XMP data is also kept in here, though usually in a second instance. */ + /** APP1 JPEG segment identifier. Commonly contains Exif. XMP data is also kept in here, though usually in a second instance. */ APP1((byte)0xE1, true), - /** APP2 JPEG segment identifier. */ + /** APP2 JPEG segment identifier. Commonly contains ICC. */ APP2((byte)0xE2, true), /** APP3 JPEG segment identifier. */ @@ -63,7 +68,7 @@ public enum JpegSegmentType /** APP9 JPEG segment identifier. */ APP9((byte)0xE9, true), - /** APPA (App10) JPEG segment identifier -- can hold Unicode comments. */ + /** APPA (App10) JPEG segment identifier. Can contain Unicode comments, though {@link JpegSegmentType#COM} is more commonly used for comments. */ APPA((byte)0xEA, true), /** APPB (App11) JPEG segment identifier. */ @@ -72,10 +77,10 @@ public enum JpegSegmentType /** APPC (App12) JPEG segment identifier. */ APPC((byte)0xEC, true), - /** APPD (App13) JPEG segment identifier -- IPTC data in here. */ + /** APPD (App13) JPEG segment identifier. Commonly contains IPTC, Photoshop data. */ APPD((byte)0xED, true), - /** APPE (App14) JPEG segment identifier. */ + /** APPE (App14) JPEG segment identifier. Commonly contains Adobe data. */ APPE((byte)0xEE, true), /** APPF (App15) JPEG segment identifier. */ @@ -87,58 +92,73 @@ public enum JpegSegmentType /** Define Quantization Table segment identifier. */ DQT((byte)0xDB, false), + /** Define Number of Lines segment identifier. */ + DNL((byte)0xDC, false), + + /** Define Restart Interval segment identifier. */ + DRI((byte)0xDD, false), + + /** Define Hierarchical Progression segment identifier. */ + DHP((byte)0xDE, false), + + /** EXPand reference component(s) segment identifier. */ + EXP((byte)0xDF, false), + /** Define Huffman Table segment identifier. */ DHT((byte)0xC4, false), - /** Start-of-Frame (0) segment identifier. */ + /** Define Arithmetic Coding conditioning segment identifier. */ + DAC((byte)0xCC, false), + + /** Start-of-Frame (0) segment identifier for Baseline DCT. */ SOF0((byte)0xC0, true), - /** Start-of-Frame (1) segment identifier. */ + /** Start-of-Frame (1) segment identifier for Extended sequential DCT. */ SOF1((byte)0xC1, true), - /** Start-of-Frame (2) segment identifier. */ + /** Start-of-Frame (2) segment identifier for Progressive DCT. */ SOF2((byte)0xC2, true), - /** Start-of-Frame (3) segment identifier. */ + /** Start-of-Frame (3) segment identifier for Lossless (sequential). */ SOF3((byte)0xC3, true), // /** Start-of-Frame (4) segment identifier. */ // SOF4((byte)0xC4, true), - /** Start-of-Frame (5) segment identifier. */ + /** Start-of-Frame (5) segment identifier for Differential sequential DCT. */ SOF5((byte)0xC5, true), - /** Start-of-Frame (6) segment identifier. */ + /** Start-of-Frame (6) segment identifier for Differential progressive DCT. */ SOF6((byte)0xC6, true), - /** Start-of-Frame (7) segment identifier. */ + /** Start-of-Frame (7) segment identifier for Differential lossless (sequential). */ SOF7((byte)0xC7, true), - /** Start-of-Frame (8) segment identifier. */ - SOF8((byte)0xC8, true), + /** Reserved for JPEG extensions. */ + JPG((byte)0xC8, true), - /** Start-of-Frame (9) segment identifier. */ + /** Start-of-Frame (9) segment identifier for Extended sequential DCT. */ SOF9((byte)0xC9, true), - /** Start-of-Frame (10) segment identifier. */ + /** Start-of-Frame (10) segment identifier for Progressive DCT. */ SOF10((byte)0xCA, true), - /** Start-of-Frame (11) segment identifier. */ + /** Start-of-Frame (11) segment identifier for Lossless (sequential). */ SOF11((byte)0xCB, true), // /** Start-of-Frame (12) segment identifier. */ // SOF12((byte)0xCC, true), - /** Start-of-Frame (13) segment identifier. */ + /** Start-of-Frame (13) segment identifier for Differential sequential DCT. */ SOF13((byte)0xCD, true), - /** Start-of-Frame (14) segment identifier. */ + /** Start-of-Frame (14) segment identifier for Differential progressive DCT. */ SOF14((byte)0xCE, true), - /** Start-of-Frame (15) segment identifier. */ + /** Start-of-Frame (15) segment identifier for Differential lossless (sequential). */ SOF15((byte)0xCF, true), - /** JPEG comment segment identifier. */ + /** JPEG comment segment identifier for comments. */ COM((byte)0xFE, true); public static final Collection<JpegSegmentType> canContainMetadataTypes; diff --git a/Source/com/drew/imaging/jpeg/package-info.java b/Source/com/drew/imaging/jpeg/package-info.java new file mode 100644 index 0000000..5a1911f --- /dev/null +++ b/Source/com/drew/imaging/jpeg/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for working with JPEG files. + */ +package com.drew.imaging.jpeg; diff --git a/Source/com/drew/imaging/jpeg/package.html b/Source/com/drew/imaging/jpeg/package.html deleted file mode 100644 index d65ff55..0000000 --- a/Source/com/drew/imaging/jpeg/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for working with JPEG files. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/imaging/package-info.java b/Source/com/drew/imaging/package-info.java new file mode 100644 index 0000000..5899a9a --- /dev/null +++ b/Source/com/drew/imaging/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains classes for working with image file formats and photographic conversions. + * <!-- Put @see and @since tags down here. --> + */ +package com.drew.imaging; diff --git a/Source/com/drew/imaging/package.html b/Source/com/drew/imaging/package.html deleted file mode 100644 index af33269..0000000 --- a/Source/com/drew/imaging/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for working with image file formats and photographic conversions. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/imaging/pcx/PcxMetadataReader.java b/Source/com/drew/imaging/pcx/PcxMetadataReader.java new file mode 100644 index 0000000..b1e297a --- /dev/null +++ b/Source/com/drew/imaging/pcx/PcxMetadataReader.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.pcx; + +import com.drew.lang.StreamReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; +import com.drew.metadata.file.FileMetadataReader; +import com.drew.metadata.pcx.PcxReader; + +import java.io.*; + +/** + * Obtains metadata from PCX image files. + * + * @author Drew Noakes https://drewnoakes.com + */ +public class PcxMetadataReader +{ + @NotNull + public static Metadata readMetadata(@NotNull File file) throws IOException + { + InputStream inputStream = new FileInputStream(file); + Metadata metadata; + try { + metadata = readMetadata(inputStream); + } finally { + inputStream.close(); + } + new FileMetadataReader().read(file, metadata); + return metadata; + } + + @NotNull + public static Metadata readMetadata(@NotNull InputStream inputStream) + { + Metadata metadata = new Metadata(); + new PcxReader().extract(new StreamReader(inputStream), metadata); + return metadata; + } +} diff --git a/Source/com/drew/imaging/pcx/package-info.java b/Source/com/drew/imaging/pcx/package-info.java new file mode 100644 index 0000000..39d214e --- /dev/null +++ b/Source/com/drew/imaging/pcx/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for working with PCX image files. + */ +package com.drew.imaging.pcx; diff --git a/Source/com/drew/imaging/png/PngChromaticities.java b/Source/com/drew/imaging/png/PngChromaticities.java index 07228ef..094a89e 100644 --- a/Source/com/drew/imaging/png/PngChromaticities.java +++ b/Source/com/drew/imaging/png/PngChromaticities.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.lang.SequentialByteArrayReader; diff --git a/Source/com/drew/imaging/png/PngChunk.java b/Source/com/drew/imaging/png/PngChunk.java index 9a6750f..cbc4bb3 100644 --- a/Source/com/drew/imaging/png/PngChunk.java +++ b/Source/com/drew/imaging/png/PngChunk.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.lang.annotations.NotNull; diff --git a/Source/com/drew/imaging/png/PngChunkReader.java b/Source/com/drew/imaging/png/PngChunkReader.java index e82b7a3..b794514 100644 --- a/Source/com/drew/imaging/png/PngChunkReader.java +++ b/Source/com/drew/imaging/png/PngChunkReader.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.lang.SequentialReader; diff --git a/Source/com/drew/imaging/png/PngChunkType.java b/Source/com/drew/imaging/png/PngChunkType.java index 4bec72f..0a97219 100644 --- a/Source/com/drew/imaging/png/PngChunkType.java +++ b/Source/com/drew/imaging/png/PngChunkType.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.lang.annotations.NotNull; @@ -33,7 +53,7 @@ public class PngChunkType * <li><b>interlace method</b> 1 byte, indicates the transmission order of image data, currently only 0 (no interlace) and 1 (Adam7 interlace) are in the standard</li> * </ul> */ - public static final PngChunkType IHDR = new PngChunkType("IHDR"); + public static final PngChunkType IHDR; /** * Denotes a critical {@link PngChunk} that contains palette entries. @@ -48,25 +68,25 @@ public class PngChunkType * </ul> * The number of entries is determined by the chunk length. A chunk length indivisible by three is an error. */ - public static final PngChunkType PLTE = new PngChunkType("PLTE"); - public static final PngChunkType IDAT = new PngChunkType("IDAT", true); - public static final PngChunkType IEND = new PngChunkType("IEND"); + public static final PngChunkType PLTE; + public static final PngChunkType IDAT; + public static final PngChunkType IEND; // // Standard ancillary chunks // - public static final PngChunkType cHRM = new PngChunkType("cHRM"); - public static final PngChunkType gAMA = new PngChunkType("gAMA"); - public static final PngChunkType iCCP = new PngChunkType("iCCP"); - public static final PngChunkType sBIT = new PngChunkType("sBIT"); - public static final PngChunkType sRGB = new PngChunkType("sRGB"); - public static final PngChunkType bKGD = new PngChunkType("bKGD"); - public static final PngChunkType hIST = new PngChunkType("hIST"); - public static final PngChunkType tRNS = new PngChunkType("tRNS"); - public static final PngChunkType pHYs = new PngChunkType("pHYs"); - public static final PngChunkType sPLT = new PngChunkType("sPLT", true); - public static final PngChunkType tIME = new PngChunkType("tIME"); - public static final PngChunkType iTXt = new PngChunkType("iTXt", true); + public static final PngChunkType cHRM; + public static final PngChunkType gAMA; + public static final PngChunkType iCCP; + public static final PngChunkType sBIT; + public static final PngChunkType sRGB; + public static final PngChunkType bKGD; + public static final PngChunkType hIST; + public static final PngChunkType tRNS; + public static final PngChunkType pHYs; + public static final PngChunkType sPLT; + public static final PngChunkType tIME; + public static final PngChunkType iTXt; /** * Denotes an ancillary {@link PngChunk} that contains textual data, having first a keyword and then a value. @@ -81,18 +101,43 @@ public class PngChunkType * Text is interpreted according to the Latin-1 character set [ISO-8859-1]. * Newlines should be represented by a single linefeed character (0x9). */ - public static final PngChunkType tEXt = new PngChunkType("tEXt", true); - public static final PngChunkType zTXt = new PngChunkType("zTXt", true); + public static final PngChunkType tEXt; + public static final PngChunkType zTXt; + + static { + try { + IHDR = new PngChunkType("IHDR"); + PLTE = new PngChunkType("PLTE"); + IDAT = new PngChunkType("IDAT", true); + IEND = new PngChunkType("IEND"); + cHRM = new PngChunkType("cHRM"); + gAMA = new PngChunkType("gAMA"); + iCCP = new PngChunkType("iCCP"); + sBIT = new PngChunkType("sBIT"); + sRGB = new PngChunkType("sRGB"); + bKGD = new PngChunkType("bKGD"); + hIST = new PngChunkType("hIST"); + tRNS = new PngChunkType("tRNS"); + pHYs = new PngChunkType("pHYs"); + sPLT = new PngChunkType("sPLT", true); + tIME = new PngChunkType("tIME"); + iTXt = new PngChunkType("iTXt", true); + tEXt = new PngChunkType("tEXt", true); + zTXt = new PngChunkType("zTXt", true); + } catch (PngProcessingException e) { + throw new IllegalArgumentException(e); + } + } private final byte[] _bytes; private final boolean _multipleAllowed; - public PngChunkType(@NotNull String identifier) + public PngChunkType(@NotNull String identifier) throws PngProcessingException { this(identifier, false); } - public PngChunkType(@NotNull String identifier, boolean multipleAllowed) + public PngChunkType(@NotNull String identifier, boolean multipleAllowed) throws PngProcessingException { _multipleAllowed = multipleAllowed; @@ -105,22 +150,22 @@ public class PngChunkType } } - public PngChunkType(@NotNull byte[] bytes) + public PngChunkType(@NotNull byte[] bytes) throws PngProcessingException { validateBytes(bytes); _bytes = bytes; _multipleAllowed = _identifiersAllowingMultiples.contains(getIdentifier()); } - private static void validateBytes(byte[] bytes) + private static void validateBytes(byte[] bytes) throws PngProcessingException { if (bytes.length != 4) { - throw new IllegalArgumentException("PNG chunk type identifier must be four bytes in length"); + throw new PngProcessingException("PNG chunk type identifier must be four bytes in length"); } for (byte b : bytes) { if (!isValidByte(b)) { - throw new IllegalArgumentException("PNG chunk type identifier may only contain alphabet characters"); + throw new PngProcessingException("PNG chunk type identifier may only contain alphabet characters"); } } } diff --git a/Source/com/drew/imaging/png/PngColorType.java b/Source/com/drew/imaging/png/PngColorType.java index 5927ee9..6d02bed 100644 --- a/Source/com/drew/imaging/png/PngColorType.java +++ b/Source/com/drew/imaging/png/PngColorType.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.lang.annotations.NotNull; diff --git a/Source/com/drew/imaging/png/PngHeader.java b/Source/com/drew/imaging/png/PngHeader.java index 12ed220..81fef4f 100644 --- a/Source/com/drew/imaging/png/PngHeader.java +++ b/Source/com/drew/imaging/png/PngHeader.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.lang.SequentialByteArrayReader; diff --git a/Source/com/drew/imaging/png/PngMetadataReader.java b/Source/com/drew/imaging/png/PngMetadataReader.java index 8cfe31b..4068818 100644 --- a/Source/com/drew/imaging/png/PngMetadataReader.java +++ b/Source/com/drew/imaging/png/PngMetadataReader.java @@ -1,14 +1,37 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.lang.*; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Metadata; +import com.drew.metadata.StringValue; +import com.drew.metadata.file.FileMetadataReader; import com.drew.metadata.icc.IccReader; import com.drew.metadata.png.PngChromaticitiesDirectory; import com.drew.metadata.png.PngDirectory; import com.drew.metadata.xmp.XmpReader; import java.io.*; +import java.nio.charset.Charset; import java.util.*; import java.util.zip.InflaterInputStream; @@ -17,24 +40,25 @@ import java.util.zip.InflaterInputStream; */ public class PngMetadataReader { - @NotNull - public static Metadata readMetadata(@NotNull File file) throws PngProcessingException, IOException - { - InputStream inputStream = null; - try { - inputStream = new FileInputStream(file); - return readMetadata(inputStream); - } finally { - if (inputStream != null) - inputStream.close(); - } - } + private static Set<PngChunkType> _desiredChunkTypes; - @NotNull - public static Metadata readMetadata(@NotNull InputStream inputStream) throws PngProcessingException, IOException + /** + * The PNG spec states that ISO_8859_1 (Latin-1) encoding should be used for: + * <ul> + * <li>"tEXt" and "zTXt" chunks, both for keys and values (https://www.w3.org/TR/PNG/#11tEXt)</li> + * <li>"iCCP" chunks, for the profile name (https://www.w3.org/TR/PNG/#11iCCP)</li> + * <li>"sPLT" chunks, for the palette name (https://www.w3.org/TR/PNG/#11sPLT)</li> + * </ul> + * Note that "iTXt" chunks use UTF-8 encoding (https://www.w3.org/TR/PNG/#11iTXt). + * <p/> + * For more guidance: http://www.w3.org/TR/PNG-Decoders.html#D.Text-chunk-processing + */ + private static Charset _latin1Encoding = Charsets.ISO_8859_1; + + static { - // TODO keep a single static hash of these Set<PngChunkType> desiredChunkTypes = new HashSet<PngChunkType>(); + desiredChunkTypes.add(PngChunkType.IHDR); desiredChunkTypes.add(PngChunkType.PLTE); desiredChunkTypes.add(PngChunkType.tRNS); @@ -44,127 +68,262 @@ public class PngMetadataReader desiredChunkTypes.add(PngChunkType.iCCP); desiredChunkTypes.add(PngChunkType.bKGD); desiredChunkTypes.add(PngChunkType.tEXt); + desiredChunkTypes.add(PngChunkType.zTXt); desiredChunkTypes.add(PngChunkType.iTXt); desiredChunkTypes.add(PngChunkType.tIME); + desiredChunkTypes.add(PngChunkType.pHYs); + desiredChunkTypes.add(PngChunkType.sBIT); - Iterable<PngChunk> chunks = new PngChunkReader().extract(new StreamReader(inputStream), desiredChunkTypes); + _desiredChunkTypes = Collections.unmodifiableSet(desiredChunkTypes); + } + + @NotNull + public static Metadata readMetadata(@NotNull File file) throws PngProcessingException, IOException + { + InputStream inputStream = new FileInputStream(file); + Metadata metadata; + try { + metadata = readMetadata(inputStream); + } finally { + inputStream.close(); + } + new FileMetadataReader().read(file, metadata); + return metadata; + } + + @NotNull + public static Metadata readMetadata(@NotNull InputStream inputStream) throws PngProcessingException, IOException + { + Iterable<PngChunk> chunks = new PngChunkReader().extract(new StreamReader(inputStream), _desiredChunkTypes); Metadata metadata = new Metadata(); - List<KeyValuePair> textPairs = new ArrayList<KeyValuePair>(); for (PngChunk chunk : chunks) { - PngChunkType chunkType = chunk.getType(); - byte[] bytes = chunk.getBytes(); - - if (chunkType.equals(PngChunkType.IHDR)) { - PngHeader header = new PngHeader(bytes); - PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class); - directory.setInt(PngDirectory.TAG_IMAGE_WIDTH, header.getImageWidth()); - directory.setInt(PngDirectory.TAG_IMAGE_HEIGHT, header.getImageHeight()); - directory.setInt(PngDirectory.TAG_BITS_PER_SAMPLE, header.getBitsPerSample()); - directory.setInt(PngDirectory.TAG_COLOR_TYPE, header.getColorType().getNumericValue()); - directory.setInt(PngDirectory.TAG_COMPRESSION_TYPE, header.getCompressionType()); - directory.setInt(PngDirectory.TAG_FILTER_METHOD, header.getFilterMethod()); - directory.setInt(PngDirectory.TAG_INTERLACE_METHOD, header.getInterlaceMethod()); - } else if (chunkType.equals(PngChunkType.PLTE)) { - PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class); - directory.setInt(PngDirectory.TAG_PALETTE_SIZE, bytes.length / 3); - } else if (chunkType.equals(PngChunkType.tRNS)) { - PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class); - directory.setInt(PngDirectory.TAG_PALETTE_HAS_TRANSPARENCY, 1); - } else if (chunkType.equals(PngChunkType.sRGB)) { - int srgbRenderingIntent = new SequentialByteArrayReader(bytes).getInt8(); - PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class); - directory.setInt(PngDirectory.TAG_SRGB_RENDERING_INTENT, srgbRenderingIntent); - } else if (chunkType.equals(PngChunkType.cHRM)) { - PngChromaticities chromaticities = new PngChromaticities(bytes); - PngChromaticitiesDirectory directory = metadata.getOrCreateDirectory(PngChromaticitiesDirectory.class); - directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_X, chromaticities.getWhitePointX()); - directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_X, chromaticities.getWhitePointX()); - directory.setInt(PngChromaticitiesDirectory.TAG_RED_X, chromaticities.getRedX()); - directory.setInt(PngChromaticitiesDirectory.TAG_RED_Y, chromaticities.getRedY()); - directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_X, chromaticities.getGreenX()); - directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_Y, chromaticities.getGreenY()); - directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_X, chromaticities.getBlueX()); - directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_Y, chromaticities.getBlueY()); - } else if (chunkType.equals(PngChunkType.gAMA)) { - int gammaInt = new SequentialByteArrayReader(bytes).getInt32(); - PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class); - directory.setDouble(PngDirectory.TAG_GAMMA, gammaInt / 100000.0); - } else if (chunkType.equals(PngChunkType.iCCP)) { - SequentialReader reader = new SequentialByteArrayReader(bytes); - String profileName = reader.getNullTerminatedString(79); - PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class); - directory.setString(PngDirectory.TAG_ICC_PROFILE_NAME, profileName); - byte compressionMethod = reader.getInt8(); - if (compressionMethod == 0) { - // Only compression method allowed by the spec is zero: deflate - // This assumes 1-byte-per-char, which it is by spec. - int bytesLeft = bytes.length - profileName.length() - 2; - byte[] compressedProfile = reader.getBytes(bytesLeft); + try { + processChunk(metadata, chunk); + } catch (Exception e) { + e.printStackTrace(System.err); + } + } + + return metadata; + } + + private static void processChunk(@NotNull Metadata metadata, @NotNull PngChunk chunk) throws PngProcessingException, IOException + { + PngChunkType chunkType = chunk.getType(); + byte[] bytes = chunk.getBytes(); + + if (chunkType.equals(PngChunkType.IHDR)) { + PngHeader header = new PngHeader(bytes); + PngDirectory directory = new PngDirectory(PngChunkType.IHDR); + directory.setInt(PngDirectory.TAG_IMAGE_WIDTH, header.getImageWidth()); + directory.setInt(PngDirectory.TAG_IMAGE_HEIGHT, header.getImageHeight()); + directory.setInt(PngDirectory.TAG_BITS_PER_SAMPLE, header.getBitsPerSample()); + directory.setInt(PngDirectory.TAG_COLOR_TYPE, header.getColorType().getNumericValue()); + directory.setInt(PngDirectory.TAG_COMPRESSION_TYPE, header.getCompressionType() & 0xFF); // make sure it's unsigned + directory.setInt(PngDirectory.TAG_FILTER_METHOD, header.getFilterMethod()); + directory.setInt(PngDirectory.TAG_INTERLACE_METHOD, header.getInterlaceMethod()); + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.PLTE)) { + PngDirectory directory = new PngDirectory(PngChunkType.PLTE); + directory.setInt(PngDirectory.TAG_PALETTE_SIZE, bytes.length / 3); + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.tRNS)) { + PngDirectory directory = new PngDirectory(PngChunkType.tRNS); + directory.setInt(PngDirectory.TAG_PALETTE_HAS_TRANSPARENCY, 1); + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.sRGB)) { + int srgbRenderingIntent = bytes[0]; + PngDirectory directory = new PngDirectory(PngChunkType.sRGB); + directory.setInt(PngDirectory.TAG_SRGB_RENDERING_INTENT, srgbRenderingIntent); + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.cHRM)) { + PngChromaticities chromaticities = new PngChromaticities(bytes); + PngChromaticitiesDirectory directory = new PngChromaticitiesDirectory(); + directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_X, chromaticities.getWhitePointX()); + directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_Y, chromaticities.getWhitePointY()); + directory.setInt(PngChromaticitiesDirectory.TAG_RED_X, chromaticities.getRedX()); + directory.setInt(PngChromaticitiesDirectory.TAG_RED_Y, chromaticities.getRedY()); + directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_X, chromaticities.getGreenX()); + directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_Y, chromaticities.getGreenY()); + directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_X, chromaticities.getBlueX()); + directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_Y, chromaticities.getBlueY()); + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.gAMA)) { + int gammaInt = ByteConvert.toInt32BigEndian(bytes); + new SequentialByteArrayReader(bytes).getInt32(); + PngDirectory directory = new PngDirectory(PngChunkType.gAMA); + directory.setDouble(PngDirectory.TAG_GAMMA, gammaInt / 100000.0); + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.iCCP)) { + SequentialReader reader = new SequentialByteArrayReader(bytes); + + // Profile Name is 1-79 bytes, followed by the 1 byte null character + byte[] profileNameBytes = reader.getNullTerminatedBytes(79 + 1); + PngDirectory directory = new PngDirectory(PngChunkType.iCCP); + directory.setStringValue(PngDirectory.TAG_ICC_PROFILE_NAME, new StringValue(profileNameBytes, _latin1Encoding)); + byte compressionMethod = reader.getInt8(); + // Only compression method allowed by the spec is zero: deflate + if (compressionMethod == 0) { + // bytes left for compressed text is: + // total bytes length - (profilenamebytes length + null byte + compression method byte) + int bytesLeft = bytes.length - (profileNameBytes.length + 1 + 1); + byte[] compressedProfile = reader.getBytes(bytesLeft); + + try { InflaterInputStream inflateStream = new InflaterInputStream(new ByteArrayInputStream(compressedProfile)); - new IccReader().extract(new RandomAccessStreamReader(inflateStream), metadata); + new IccReader().extract(new RandomAccessStreamReader(inflateStream), metadata, directory); inflateStream.close(); + } catch(java.util.zip.ZipException zex) { + directory.addError(String.format("Exception decompressing PNG iCCP chunk : %s", zex.getMessage())); + metadata.addDirectory(directory); } - } else if (chunkType.equals(PngChunkType.bKGD)) { - PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class); - directory.setByteArray(PngDirectory.TAG_BACKGROUND_COLOR, bytes); - } else if (chunkType.equals(PngChunkType.tEXt)) { - SequentialReader reader = new SequentialByteArrayReader(bytes); - String keyword = reader.getNullTerminatedString(79); - int bytesLeft = bytes.length - keyword.length() - 1; - String value = reader.getNullTerminatedString(bytesLeft); - textPairs.add(new KeyValuePair(keyword, value)); - } else if (chunkType.equals(PngChunkType.iTXt)) { - SequentialReader reader = new SequentialByteArrayReader(bytes); - String keyword = reader.getNullTerminatedString(79); - byte compressionFlag = reader.getInt8(); - byte compressionMethod = reader.getInt8(); - String languageTag = reader.getNullTerminatedString(bytes.length); - String translatedKeyword = reader.getNullTerminatedString(bytes.length); - int bytesLeft = bytes.length - keyword.length() - 1 - 1 - 1 - languageTag.length() - 1 - translatedKeyword.length() - 1; - String text = null; - if (compressionFlag == 0) { - text = reader.getNullTerminatedString(bytesLeft); - } else if (compressionFlag == 1) { - if (compressionMethod == 0) { - text = StringUtil.fromStream(new InflaterInputStream(new ByteArrayInputStream(bytes, bytes.length - bytesLeft, bytesLeft))); - } else { - metadata.getOrCreateDirectory(PngDirectory.class).addError("Invalid compression method value"); - } + } else { + directory.addError("Invalid compression method value"); + } + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.bKGD)) { + PngDirectory directory = new PngDirectory(PngChunkType.bKGD); + directory.setByteArray(PngDirectory.TAG_BACKGROUND_COLOR, bytes); + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.tEXt)) { + SequentialReader reader = new SequentialByteArrayReader(bytes); + + // Keyword is 1-79 bytes, followed by the 1 byte null character + StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _latin1Encoding); + String keyword = keywordsv.toString(); + + // bytes left for text is: + // total bytes length - (Keyword length + null byte) + int bytesLeft = bytes.length - (keywordsv.getBytes().length + 1); + StringValue value = reader.getNullTerminatedStringValue(bytesLeft, _latin1Encoding); + List<KeyValuePair> textPairs = new ArrayList<KeyValuePair>(); + textPairs.add(new KeyValuePair(keyword, value)); + PngDirectory directory = new PngDirectory(PngChunkType.tEXt); + directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs); + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.zTXt)) { + SequentialReader reader = new SequentialByteArrayReader(bytes); + + // Keyword is 1-79 bytes, followed by the 1 byte null character + StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _latin1Encoding); + String keyword = keywordsv.toString(); + byte compressionMethod = reader.getInt8(); + + // bytes left for compressed text is: + // total bytes length - (Keyword length + null byte + compression method byte) + int bytesLeft = bytes.length - (keywordsv.getBytes().length + 1 + 1); + byte[] textBytes = null; + if (compressionMethod == 0) { + try { + textBytes = StreamUtil.readAllBytes(new InflaterInputStream(new ByteArrayInputStream(bytes, bytes.length - bytesLeft, bytesLeft))); + } catch(java.util.zip.ZipException zex) { + textBytes = null; + PngDirectory directory = new PngDirectory(PngChunkType.zTXt); + directory.addError(String.format("Exception decompressing PNG zTXt chunk with keyword \"%s\": %s", keyword, zex.getMessage())); + metadata.addDirectory(directory); + } + } else { + PngDirectory directory = new PngDirectory(PngChunkType.zTXt); + directory.addError("Invalid compression method value"); + metadata.addDirectory(directory); + } + if (textBytes != null) { + if (keyword.equals("XML:com.adobe.xmp")) { + // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary + new XmpReader().extract(textBytes, metadata); } else { - metadata.getOrCreateDirectory(PngDirectory.class).addError("Invalid compression flag value"); + List<KeyValuePair> textPairs = new ArrayList<KeyValuePair>(); + textPairs.add(new KeyValuePair(keyword, new StringValue(textBytes, _latin1Encoding))); + PngDirectory directory = new PngDirectory(PngChunkType.zTXt); + directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs); + metadata.addDirectory(directory); } + } + } else if (chunkType.equals(PngChunkType.iTXt)) { + SequentialReader reader = new SequentialByteArrayReader(bytes); + + // Keyword is 1-79 bytes, followed by the 1 byte null character + StringValue keywordsv = reader.getNullTerminatedStringValue(79 + 1, _latin1Encoding); + String keyword = keywordsv.toString(); + byte compressionFlag = reader.getInt8(); + byte compressionMethod = reader.getInt8(); + // TODO we currently ignore languageTagBytes and translatedKeywordBytes + byte[] languageTagBytes = reader.getNullTerminatedBytes(bytes.length); + byte[] translatedKeywordBytes = reader.getNullTerminatedBytes(bytes.length); - if (text != null) { - if (keyword.equals("XML:com.adobe.xmp")) { - // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary - new XmpReader().extract(text, metadata); - } else { - textPairs.add(new KeyValuePair(keyword, text)); + // bytes left for compressed text is: + // total bytes length - (Keyword length + null byte + comp flag byte + comp method byte + lang length + null byte + translated length + null byte) + int bytesLeft = bytes.length - (keywordsv.getBytes().length + 1 + 1 + 1 + languageTagBytes.length + 1 + translatedKeywordBytes.length + 1); + byte[] textBytes = null; + if (compressionFlag == 0) { + textBytes = reader.getNullTerminatedBytes(bytesLeft); + } else if (compressionFlag == 1) { + if (compressionMethod == 0) { + try { + textBytes = StreamUtil.readAllBytes(new InflaterInputStream(new ByteArrayInputStream(bytes, bytes.length - bytesLeft, bytesLeft))); + } catch(java.util.zip.ZipException zex) { + textBytes = null; + PngDirectory directory = new PngDirectory(PngChunkType.iTXt); + directory.addError(String.format("Exception decompressing PNG iTXt chunk with keyword \"%s\": %s", keyword, zex.getMessage())); + metadata.addDirectory(directory); } + } else { + PngDirectory directory = new PngDirectory(PngChunkType.iTXt); + directory.addError("Invalid compression method value"); + metadata.addDirectory(directory); } - } else if (chunkType.equals(PngChunkType.tIME)) { - SequentialByteArrayReader reader = new SequentialByteArrayReader(bytes); - int year = reader.getUInt16(); - int month = reader.getUInt8() - 1; - int day = reader.getUInt8(); - int hour = reader.getUInt8(); - int minute = reader.getUInt8(); - int second = reader.getUInt8(); - Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - //noinspection MagicConstant - calendar.set(year, month, day, hour, minute, second); - PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class); - directory.setDate(PngDirectory.TAG_LAST_MODIFICATION_TIME, calendar.getTime()); + } else { + PngDirectory directory = new PngDirectory(PngChunkType.iTXt); + directory.addError("Invalid compression flag value"); + metadata.addDirectory(directory); } - } - if (textPairs.size() != 0) { - PngDirectory directory = metadata.getOrCreateDirectory(PngDirectory.class); - directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs); + if (textBytes != null) { + if (keyword.equals("XML:com.adobe.xmp")) { + // NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary + new XmpReader().extract(textBytes, metadata); + } else { + List<KeyValuePair> textPairs = new ArrayList<KeyValuePair>(); + textPairs.add(new KeyValuePair(keyword, new StringValue(textBytes, _latin1Encoding))); + PngDirectory directory = new PngDirectory(PngChunkType.iTXt); + directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs); + metadata.addDirectory(directory); + } + } + } else if (chunkType.equals(PngChunkType.tIME)) { + SequentialByteArrayReader reader = new SequentialByteArrayReader(bytes); + int year = reader.getUInt16(); + int month = reader.getUInt8(); + int day = reader.getUInt8(); + int hour = reader.getUInt8(); + int minute = reader.getUInt8(); + int second = reader.getUInt8(); + PngDirectory directory = new PngDirectory(PngChunkType.tIME); + if (DateUtil.isValidDate(year, month - 1, day) && DateUtil.isValidTime(hour, minute, second)) { + String dateString = String.format("%04d:%02d:%02d %02d:%02d:%02d", year, month, day, hour, minute, second); + directory.setString(PngDirectory.TAG_LAST_MODIFICATION_TIME, dateString); + } else { + directory.addError(String.format( + "PNG tIME data describes an invalid date/time: year=%d month=%d day=%d hour=%d minute=%d second=%d", + year, month, day, hour, minute, second)); + } + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.pHYs)) { + SequentialByteArrayReader reader = new SequentialByteArrayReader(bytes); + int pixelsPerUnitX = reader.getInt32(); + int pixelsPerUnitY = reader.getInt32(); + byte unitSpecifier = reader.getInt8(); + PngDirectory directory = new PngDirectory(PngChunkType.pHYs); + directory.setInt(PngDirectory.TAG_PIXELS_PER_UNIT_X, pixelsPerUnitX); + directory.setInt(PngDirectory.TAG_PIXELS_PER_UNIT_Y, pixelsPerUnitY); + directory.setInt(PngDirectory.TAG_UNIT_SPECIFIER, unitSpecifier); + metadata.addDirectory(directory); + } else if (chunkType.equals(PngChunkType.sBIT)) { + PngDirectory directory = new PngDirectory(PngChunkType.sBIT); + directory.setByteArray(PngDirectory.TAG_SIGNIFICANT_BITS, bytes); + metadata.addDirectory(directory); } - - return metadata; } } diff --git a/Source/com/drew/imaging/png/PngProcessingException.java b/Source/com/drew/imaging/png/PngProcessingException.java index 19baab4..3095069 100644 --- a/Source/com/drew/imaging/png/PngProcessingException.java +++ b/Source/com/drew/imaging/png/PngProcessingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/imaging/png/package-info.java b/Source/com/drew/imaging/png/package-info.java new file mode 100644 index 0000000..eb1e3d4 --- /dev/null +++ b/Source/com/drew/imaging/png/package-info.java @@ -0,0 +1,6 @@ +/** + * Contains classes for working with PNG (Portable Network Graphic) files. + * + * @since 2.7.0 + */ +package com.drew.imaging.png; diff --git a/Source/com/drew/imaging/png/package.html b/Source/com/drew/imaging/png/package.html deleted file mode 100644 index d0885b3..0000000 --- a/Source/com/drew/imaging/png/package.html +++ /dev/null @@ -1,34 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for working with PNG (Portable Network Graphic) files. - -<!-- Put @see and @since tags down here. --> -@since 2.7.0 - -</body> -</html> diff --git a/Source/com/drew/imaging/psd/PsdMetadataReader.java b/Source/com/drew/imaging/psd/PsdMetadataReader.java index f46e9b6..44feffe 100644 --- a/Source/com/drew/imaging/psd/PsdMetadataReader.java +++ b/Source/com/drew/imaging/psd/PsdMetadataReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,10 @@ package com.drew.imaging.psd; -import com.drew.lang.RandomAccessFileReader; -import com.drew.lang.RandomAccessStreamReader; +import com.drew.lang.StreamReader; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Metadata; +import com.drew.metadata.file.FileMetadataReader; import com.drew.metadata.photoshop.PsdReader; import java.io.*; @@ -40,15 +40,13 @@ public class PsdMetadataReader public static Metadata readMetadata(@NotNull File file) throws IOException { Metadata metadata = new Metadata(); - - RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); - + InputStream stream = new FileInputStream(file); try { - new PsdReader().extract(new RandomAccessFileReader(randomAccessFile), metadata); + new PsdReader().extract(new StreamReader(stream), metadata); } finally { - randomAccessFile.close(); + stream.close(); } - + new FileMetadataReader().read(file, metadata); return metadata; } @@ -56,7 +54,7 @@ public class PsdMetadataReader public static Metadata readMetadata(@NotNull InputStream inputStream) { Metadata metadata = new Metadata(); - new PsdReader().extract(new RandomAccessStreamReader(inputStream), metadata); + new PsdReader().extract(new StreamReader(inputStream), metadata); return metadata; } } diff --git a/Source/com/drew/imaging/psd/package-info.java b/Source/com/drew/imaging/psd/package-info.java new file mode 100644 index 0000000..77a0afe --- /dev/null +++ b/Source/com/drew/imaging/psd/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for working with PSD (PhotoShop Document) files. + */ +package com.drew.imaging.psd; diff --git a/Source/com/drew/imaging/psd/package.html b/Source/com/drew/imaging/psd/package.html deleted file mode 100644 index 315f536..0000000 --- a/Source/com/drew/imaging/psd/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for working with PSD (PhotoShop Document) files. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/imaging/raf/RafMetadataReader.java b/Source/com/drew/imaging/raf/RafMetadataReader.java new file mode 100644 index 0000000..2a3b5ae --- /dev/null +++ b/Source/com/drew/imaging/raf/RafMetadataReader.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.raf; + +import com.drew.imaging.jpeg.JpegMetadataReader; +import com.drew.imaging.jpeg.JpegProcessingException; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Obtains metadata from RAF (Fujifilm camera raw) image files. + * + * @author TSGames https://github.com/TSGames + * @author Drew Noakes https://drewnoakes.com + */ +public class RafMetadataReader +{ + @NotNull + public static Metadata readMetadata(@NotNull InputStream inputStream) throws JpegProcessingException, IOException + { + if (!inputStream.markSupported()) + throw new IOException("Stream must support mark/reset"); + + inputStream.mark(512); + + byte[] data = new byte[512]; + int bytesRead = inputStream.read(data); + + if (bytesRead == -1) + throw new IOException("Stream is empty"); + + inputStream.reset(); + + for (int i = 0; i < bytesRead - 2; i++) { + // Look for the first three bytes of a JPEG encoded file + if (data[i] == (byte) 0xff && data[i + 1] == (byte) 0xd8 && data[i + 2] == (byte) 0xff) { + long bytesSkipped = inputStream.skip(i); + if (bytesSkipped != i) + throw new IOException("Skipping stream bytes failed"); + break; + } + } + + return JpegMetadataReader.readMetadata(inputStream); + } + + private RafMetadataReader() throws Exception + { + throw new Exception("Not intended for instantiation"); + } +} diff --git a/Source/com/drew/imaging/raf/package-info.java b/Source/com/drew/imaging/raf/package-info.java new file mode 100644 index 0000000..17e130f --- /dev/null +++ b/Source/com/drew/imaging/raf/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for working with RAF (Fujifilm camera raw) format files. + */ +package com.drew.imaging.raf; diff --git a/Source/com/drew/imaging/riff/RiffHandler.java b/Source/com/drew/imaging/riff/RiffHandler.java new file mode 100644 index 0000000..f6b9819 --- /dev/null +++ b/Source/com/drew/imaging/riff/RiffHandler.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.riff; + +import com.drew.lang.annotations.NotNull; + +/** + * Interface of an class capable of handling events raised during the reading of a RIFF file + * via {@link RiffReader}. + * + * @author Drew Noakes https://drewnoakes.com + */ +public interface RiffHandler +{ + /** + * Gets whether the specified RIFF identifier is of interest to this handler. + * Returning <code>false</code> causes processing to stop after reading only + * the first twelve bytes of data. + * + * @param identifier The four character code identifying the type of RIFF data + * @return true if processing should continue, otherwise false + */ + boolean shouldAcceptRiffIdentifier(@NotNull String identifier); + + /** + * Gets whether this handler is interested in the specific chunk type. + * Returns <code>true</code> if the data should be copied into an array and passed + * to {@link RiffHandler#processChunk(String, byte[])}, or <code>false</code> to avoid + * the copy and skip to the next chunk in the file, if any. + * + * @param fourCC the four character code of this chunk + * @return true if {@link RiffHandler#processChunk(String, byte[])} should be called, otherwise false + */ + boolean shouldAcceptChunk(@NotNull String fourCC); + + /** + * Perform whatever processing is necessary for the type of chunk with its + * payload. + * + * This is only called if a previous call to {@link RiffHandler#shouldAcceptChunk(String)} + * with the same <code>fourCC</code> returned <code>true</code>. + * + * @param fourCC the four character code of the chunk + * @param payload they payload of the chunk as a byte array + */ + void processChunk(@NotNull String fourCC, @NotNull byte[] payload); +} diff --git a/Source/com/drew/imaging/riff/RiffProcessingException.java b/Source/com/drew/imaging/riff/RiffProcessingException.java new file mode 100644 index 0000000..8ffc035 --- /dev/null +++ b/Source/com/drew/imaging/riff/RiffProcessingException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.riff; + +import com.drew.imaging.ImageProcessingException; +import com.drew.lang.annotations.Nullable; + +/** + * An exception class thrown upon unexpected and fatal conditions while processing a RIFF file. + * + * @author Drew Noakes https://drewnoakes.com + */ +public class RiffProcessingException extends ImageProcessingException +{ + private static final long serialVersionUID = -1658134596321487960L; + + public RiffProcessingException(@Nullable String message) + { + super(message); + } + + public RiffProcessingException(@Nullable String message, @Nullable Throwable cause) + { + super(message, cause); + } + + public RiffProcessingException(@Nullable Throwable cause) + { + super(cause); + } +} diff --git a/Source/com/drew/imaging/riff/RiffReader.java b/Source/com/drew/imaging/riff/RiffReader.java new file mode 100644 index 0000000..6decdec --- /dev/null +++ b/Source/com/drew/imaging/riff/RiffReader.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.riff; + +import com.drew.lang.SequentialReader; +import com.drew.lang.annotations.NotNull; + +import java.io.IOException; + +/** + * Processes RIFF-formatted data, calling into client code via that {@link RiffHandler} interface. + * <p></p> + * For information on this file format, see: + * <ul> + * <li>http://en.wikipedia.org/wiki/Resource_Interchange_File_Format</li> + * <li>https://developers.google.com/speed/webp/docs/riff_container</li> + * <li>https://www.daubnet.com/en/file-format-riff</li> + * </ul> + * @author Drew Noakes https://drewnoakes.com + */ +public class RiffReader +{ + /** + * Processes a RIFF data sequence. + * + * @param reader the {@link SequentialReader} from which the data should be read + * @param handler the {@link RiffHandler} that will coordinate processing and accept read values + * @throws RiffProcessingException if an error occurred during the processing of RIFF data that could not be + * ignored or recovered from + * @throws IOException an error occurred while accessing the required data + */ + public void processRiff(@NotNull final SequentialReader reader, + @NotNull final RiffHandler handler) throws RiffProcessingException, IOException + { + // RIFF files are always little-endian + reader.setMotorolaByteOrder(false); + + // PROCESS FILE HEADER + + final String fileFourCC = reader.getString(4); + + if (!fileFourCC.equals("RIFF")) + throw new RiffProcessingException("Invalid RIFF header: " + fileFourCC); + + // The total size of the chunks that follow plus 4 bytes for the 'WEBP' FourCC + final int fileSize = reader.getInt32(); + int sizeLeft = fileSize; + + final String identifier = reader.getString(4); + sizeLeft -= 4; + + if (!handler.shouldAcceptRiffIdentifier(identifier)) + return; + + // PROCESS CHUNKS + + while (sizeLeft != 0) { + final String chunkFourCC = reader.getString(4); + final int chunkSize = reader.getInt32(); + sizeLeft -= 8; + + // NOTE we fail a negative chunk size here (greater than 0x7FFFFFFF) as Java cannot + // allocate arrays larger than this. + if (chunkSize < 0 || sizeLeft < chunkSize) + throw new RiffProcessingException("Invalid RIFF chunk size"); + + if (handler.shouldAcceptChunk(chunkFourCC)) { + // TODO is it feasible to avoid copying the chunk here, and to pass the sequential reader to the handler? + handler.processChunk(chunkFourCC, reader.getBytes(chunkSize)); + } else { + reader.skip(chunkSize); + } + + sizeLeft -= chunkSize; + + // Skip any padding byte added to keep chunks aligned to even numbers of bytes + if (chunkSize % 2 == 1) { + reader.getInt8(); + sizeLeft--; + } + } + } +} diff --git a/Source/com/drew/imaging/riff/package-info.java b/Source/com/drew/imaging/riff/package-info.java new file mode 100644 index 0000000..1fd4a4b --- /dev/null +++ b/Source/com/drew/imaging/riff/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for working with RIFF format files, such as WebP. + */ +package com.drew.imaging.riff; diff --git a/Source/com/drew/imaging/tiff/TiffDataFormat.java b/Source/com/drew/imaging/tiff/TiffDataFormat.java index 765cd71..bf99dc4 100644 --- a/Source/com/drew/imaging/tiff/TiffDataFormat.java +++ b/Source/com/drew/imaging/tiff/TiffDataFormat.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/imaging/tiff/TiffHandler.java b/Source/com/drew/imaging/tiff/TiffHandler.java index 94b2512..fd24d06 100644 --- a/Source/com/drew/imaging/tiff/TiffHandler.java +++ b/Source/com/drew/imaging/tiff/TiffHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,16 @@ package com.drew.imaging.tiff; import com.drew.lang.RandomAccessReader; import com.drew.lang.Rational; import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.StringValue; import java.io.IOException; import java.util.Set; /** + * Interface of an class capable of handling events raised during the reading of a TIFF file + * via {@link TiffReader}. + * * @author Drew Noakes https://drewnoakes.com */ public interface TiffHandler @@ -42,12 +47,13 @@ public interface TiffHandler */ void setTiffMarker(int marker) throws TiffProcessingException; - boolean isTagIfdPointer(int tagType); + boolean tryEnterSubIfd(int tagId); boolean hasFollowerIfd(); void endingIFD(); - void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset); + @Nullable + Long tryCustomProcessFormat(int tagId, int formatCode, long componentCount); boolean customProcessTag(int tagOffset, @NotNull Set<Integer> processedIfdOffsets, @@ -60,7 +66,7 @@ public interface TiffHandler void error(@NotNull String message); void setByteArray(int tagId, @NotNull byte[] bytes); - void setString(int tagId, @NotNull String string); + void setString(int tagId, @NotNull StringValue string); void setRational(int tagId, @NotNull Rational rational); void setRationalArray(int tagId, @NotNull Rational[] array); void setFloat(int tagId, float float32); diff --git a/Source/com/drew/imaging/tiff/TiffMetadataReader.java b/Source/com/drew/imaging/tiff/TiffMetadataReader.java index d56ee5f..aaf4dc5 100644 --- a/Source/com/drew/imaging/tiff/TiffMetadataReader.java +++ b/Source/com/drew/imaging/tiff/TiffMetadataReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,15 +21,14 @@ package com.drew.imaging.tiff; import com.drew.lang.RandomAccessFileReader; +import com.drew.lang.RandomAccessReader; import com.drew.lang.RandomAccessStreamReader; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Metadata; import com.drew.metadata.exif.ExifTiffHandler; +import com.drew.metadata.file.FileMetadataReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; +import java.io.*; /** * Obtains all available metadata from TIFF formatted files. Note that TIFF files include many digital camera RAW @@ -47,12 +46,14 @@ public class TiffMetadataReader RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); try { - ExifTiffHandler handler = new ExifTiffHandler(metadata, false); + ExifTiffHandler handler = new ExifTiffHandler(metadata, null); new TiffReader().processTiff(new RandomAccessFileReader(randomAccessFile), handler, 0); } finally { randomAccessFile.close(); } + new FileMetadataReader().read(file, metadata); + return metadata; } @@ -63,9 +64,15 @@ public class TiffMetadataReader // InputStream does not support seeking backwards, so we wrap it with RandomAccessStreamReader, which // buffers data from the stream as we seek forward. + return readMetadata(new RandomAccessStreamReader(inputStream)); + } + + @NotNull + public static Metadata readMetadata(@NotNull RandomAccessReader reader) throws IOException, TiffProcessingException + { Metadata metadata = new Metadata(); - ExifTiffHandler handler = new ExifTiffHandler(metadata, false); - new TiffReader().processTiff(new RandomAccessStreamReader(inputStream), handler, 0); + ExifTiffHandler handler = new ExifTiffHandler(metadata, null); + new TiffReader().processTiff(reader, handler, 0); return metadata; } } diff --git a/Source/com/drew/imaging/tiff/TiffProcessingException.java b/Source/com/drew/imaging/tiff/TiffProcessingException.java index 5d90c9c..123cc21 100644 --- a/Source/com/drew/imaging/tiff/TiffProcessingException.java +++ b/Source/com/drew/imaging/tiff/TiffProcessingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/imaging/tiff/TiffReader.java b/Source/com/drew/imaging/tiff/TiffReader.java index 497a64e..dd4aefe 100644 --- a/Source/com/drew/imaging/tiff/TiffReader.java +++ b/Source/com/drew/imaging/tiff/TiffReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,8 +76,6 @@ public class TiffReader Set<Integer> processedIfdOffsets = new HashSet<Integer>(); processIfd(handler, reader, processedIfdOffsets, firstIfdOffset, tiffHeaderOffset); - - handler.completed(reader, tiffHeaderOffset); } /** @@ -109,6 +107,7 @@ public class TiffReader final int ifdOffset, final int tiffHeaderOffset) throws IOException { + Boolean resetByteOrder = null; try { // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist if (processedIfdOffsets.contains(Integer.valueOf(ifdOffset))) { @@ -126,6 +125,16 @@ public class TiffReader // First two bytes in the IFD are the number of tags in this directory int dirTagCount = reader.getUInt16(ifdOffset); + // Some software modifies the byte order of the file, but misses some IFDs (such as makernotes). + // The entire test image repository doesn't contain a single IFD with more than 255 entries. + // Here we detect switched bytes that suggest this problem, and temporarily swap the byte order. + // This was discussed in GitHub issue #136. + if (dirTagCount > 0xFF && (dirTagCount & 0xFF) == 0) { + resetByteOrder = reader.isMotorolaByteOrder(); + dirTagCount >>= 8; + reader.setMotorolaByteOrder(!reader.isMotorolaByteOrder()); + } + int dirLength = (2 + (12 * dirTagCount) + 4); if (dirLength + ifdOffset > reader.getLength()) { handler.error("Illegally sized IFD"); @@ -146,31 +155,32 @@ public class TiffReader final int formatCode = reader.getUInt16(tagOffset + 2); final TiffDataFormat format = TiffDataFormat.fromTiffFormatCode(formatCode); + // 4 bytes dictate the number of components in this tag's data + final long componentCount = reader.getUInt32(tagOffset + 4); + + final long byteCount; if (format == null) { - // This error suggests that we are processing at an incorrect index and will generate - // rubbish until we go out of bounds (which may be a while). Exit now. - handler.error("Invalid TIFF tag format code: " + formatCode); - // TODO specify threshold as a parameter, or provide some other external control over this behaviour - if (++invalidTiffFormatCodeCount > 5) { - handler.error("Stopping processing as too many errors seen in TIFF IFD"); - return; + Long byteCountOverride = handler.tryCustomProcessFormat(tagId, formatCode, componentCount); + if (byteCountOverride == null) { + // This error suggests that we are processing at an incorrect index and will generate + // rubbish until we go out of bounds (which may be a while). Exit now. + handler.error(String.format("Invalid TIFF tag format code %d for tag 0x%04X", formatCode, tagId)); + // TODO specify threshold as a parameter, or provide some other external control over this behaviour + if (++invalidTiffFormatCodeCount > 5) { + handler.error("Stopping processing as too many errors seen in TIFF IFD"); + return; + } + continue; } - continue; - } - - // 4 bytes dictate the number of components in this tag's data - final int componentCount = reader.getInt32(tagOffset + 4); - if (componentCount < 0) { - handler.error("Negative TIFF tag component count"); - continue; + byteCount = byteCountOverride; + } else { + byteCount = componentCount * format.getComponentSizeBytes(); } - final int byteCount = componentCount * format.getComponentSizeBytes(); - - final int tagValueOffset; + final long tagValueOffset; if (byteCount > 4) { // If it's bigger than 4 bytes, the dir entry contains an offset. - final int offsetVal = reader.getInt32(tagOffset + 8); + final long offsetVal = reader.getUInt32(tagOffset + 8); if (offsetVal + byteCount > reader.getLength()) { // Bogus pointer offset and / or byteCount value handler.error("Illegal TIFF tag pointer offset"); @@ -194,17 +204,23 @@ public class TiffReader continue; } - // - // Special handling for tags that point to other IFDs - // - if (byteCount == 4 && handler.isTagIfdPointer(tagId)) { - final int subDirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset); - processIfd(handler, reader, processedIfdOffsets, subDirOffset, tiffHeaderOffset); - } else { - if (!handler.customProcessTag(tagValueOffset, processedIfdOffsets, tiffHeaderOffset, reader, tagId, byteCount)) { - processTag(handler, tagId, tagValueOffset, componentCount, formatCode, reader); + // Some tags point to one or more additional IFDs to process + boolean isIfdPointer = false; + if (byteCount == 4 * componentCount) { + for (int i = 0; i < componentCount; i++) { + if (handler.tryEnterSubIfd(tagId)) { + isIfdPointer = true; + int subDirOffset = tiffHeaderOffset + reader.getInt32((int) (tagValueOffset + i * 4)); + processIfd(handler, reader, processedIfdOffsets, subDirOffset, tiffHeaderOffset); + } } } + + // If it wasn't an IFD pointer, allow custom tag processing to occur + if (!isIfdPointer && !handler.customProcessTag((int) tagValueOffset, processedIfdOffsets, tiffHeaderOffset, reader, tagId, (int) byteCount)) { + // If no custom processing occurred, process the tag in the standard fashion + processTag(handler, tagId, (int) tagValueOffset, (int) componentCount, formatCode, reader); + } } // at the end of each IFD is an optional link to the next IFD @@ -228,6 +244,8 @@ public class TiffReader } } finally { handler.endingIFD(); + if (resetByteOrder != null) + reader.setMotorolaByteOrder(resetByteOrder); } } @@ -244,7 +262,7 @@ public class TiffReader handler.setByteArray(tagId, reader.getBytes(tagValueOffset, componentCount)); break; case TiffDataFormat.CODE_STRING: - handler.setString(tagId, reader.getNullTerminatedString(tagValueOffset, componentCount)); + handler.setString(tagId, reader.getNullTerminatedStringValue(tagValueOffset, componentCount, null)); break; case TiffDataFormat.CODE_RATIONAL_S: if (componentCount == 1) { @@ -349,7 +367,7 @@ public class TiffReader } break; default: - handler.error(String.format("Unknown format code %d for tag %d", formatCode, tagId)); + handler.error(String.format("Invalid TIFF tag format code %d for tag 0x%04X", formatCode, tagId)); } } diff --git a/Source/com/drew/imaging/tiff/package-info.java b/Source/com/drew/imaging/tiff/package-info.java new file mode 100644 index 0000000..0bd2d4f --- /dev/null +++ b/Source/com/drew/imaging/tiff/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for working with TIFF format files. + */ +package com.drew.imaging.tiff; diff --git a/Source/com/drew/imaging/tiff/package.html b/Source/com/drew/imaging/tiff/package.html deleted file mode 100644 index ce5b2e6..0000000 --- a/Source/com/drew/imaging/tiff/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for working with TIFF format files. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/imaging/webp/WebpMetadataReader.java b/Source/com/drew/imaging/webp/WebpMetadataReader.java new file mode 100644 index 0000000..4c2c191 --- /dev/null +++ b/Source/com/drew/imaging/webp/WebpMetadataReader.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.webp; + +import com.drew.imaging.riff.RiffProcessingException; +import com.drew.imaging.riff.RiffReader; +import com.drew.lang.StreamReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; +import com.drew.metadata.file.FileMetadataReader; +import com.drew.metadata.webp.WebpRiffHandler; + +import java.io.*; + +/** + * Obtains metadata from WebP files. + * + * @author Drew Noakes https://drewnoakes.com + */ +public class WebpMetadataReader +{ + @NotNull + public static Metadata readMetadata(@NotNull File file) throws IOException, RiffProcessingException + { + InputStream inputStream = new FileInputStream(file); + Metadata metadata; + try { + metadata = readMetadata(inputStream); + } finally { + inputStream.close(); + } + new FileMetadataReader().read(file, metadata); + return metadata; + } + + @NotNull + public static Metadata readMetadata(@NotNull InputStream inputStream) throws IOException, RiffProcessingException + { + Metadata metadata = new Metadata(); + new RiffReader().processRiff(new StreamReader(inputStream), new WebpRiffHandler(metadata)); + return metadata; + } +} diff --git a/Source/com/drew/imaging/webp/package-info.java b/Source/com/drew/imaging/webp/package-info.java new file mode 100644 index 0000000..18cf66f --- /dev/null +++ b/Source/com/drew/imaging/webp/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for working with WebP format files. + */ +package com.drew.imaging.webp; diff --git a/Source/com/drew/lang/BufferBoundsException.java b/Source/com/drew/lang/BufferBoundsException.java index b7ecab4..6045a0a 100644 --- a/Source/com/drew/lang/BufferBoundsException.java +++ b/Source/com/drew/lang/BufferBoundsException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/lang/ByteArrayReader.java b/Source/com/drew/lang/ByteArrayReader.java index 397d8ae..f5e58f0 100644 --- a/Source/com/drew/lang/ByteArrayReader.java +++ b/Source/com/drew/lang/ByteArrayReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,34 +38,52 @@ public class ByteArrayReader extends RandomAccessReader { @NotNull private final byte[] _buffer; + private final int _baseOffset; @SuppressWarnings({ "ConstantConditions" }) @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent") public ByteArrayReader(@NotNull byte[] buffer) + { + this(buffer, 0); + } + + @SuppressWarnings({ "ConstantConditions" }) + @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent") + public ByteArrayReader(@NotNull byte[] buffer, int baseOffset) { if (buffer == null) throw new NullPointerException(); + if (baseOffset < 0) + throw new IllegalArgumentException("Must be zero or greater"); _buffer = buffer; + _baseOffset = baseOffset; + } + + @Override + public int toUnshiftedOffset(int localOffset) + { + return localOffset + _baseOffset; } @Override public long getLength() { - return _buffer.length; + return _buffer.length - _baseOffset; } @Override - protected byte getByte(int index) throws IOException + public byte getByte(int index) throws IOException { - return _buffer[index]; + validateIndex(index, 1); + return _buffer[index + _baseOffset]; } @Override protected void validateIndex(int index, int bytesRequested) throws IOException { if (!isValidIndex(index, bytesRequested)) - throw new BufferBoundsException(index, bytesRequested, _buffer.length); + throw new BufferBoundsException(toUnshiftedOffset(index), bytesRequested, _buffer.length); } @Override @@ -73,7 +91,7 @@ public class ByteArrayReader extends RandomAccessReader { return bytesRequested >= 0 && index >= 0 - && (long)index + (long)bytesRequested - 1L < (long)_buffer.length; + && (long)index + (long)bytesRequested - 1L < getLength(); } @Override @@ -83,7 +101,7 @@ public class ByteArrayReader extends RandomAccessReader validateIndex(index, count); byte[] bytes = new byte[count]; - System.arraycopy(_buffer, index, bytes, 0, count); + System.arraycopy(_buffer, index + _baseOffset, bytes, 0, count); return bytes; } } diff --git a/Source/com/drew/lang/ByteConvert.java b/Source/com/drew/lang/ByteConvert.java new file mode 100644 index 0000000..014e34b --- /dev/null +++ b/Source/com/drew/lang/ByteConvert.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.lang; + +import com.drew.lang.annotations.NotNull; + +/** + * @author Drew Noakes http://drewnoakes.com + */ +public class ByteConvert +{ + public static int toInt32BigEndian(@NotNull byte[] bytes) + { + return (bytes[0] << 24 & 0xFF000000) | + (bytes[1] << 16 & 0xFF0000) | + (bytes[2] << 8 & 0xFF00) | + (bytes[3] & 0xFF); + } + + public static int toInt32LittleEndian(@NotNull byte[] bytes) + { + return (bytes[0] & 0xFF) | + (bytes[1] << 8 & 0xFF00) | + (bytes[2] << 16 & 0xFF0000) | + (bytes[3] << 24 & 0xFF000000); + } +} diff --git a/Source/com/drew/lang/ByteTrie.java b/Source/com/drew/lang/ByteTrie.java index 3855bda..68ba410 100644 --- a/Source/com/drew/lang/ByteTrie.java +++ b/Source/com/drew/lang/ByteTrie.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/lang/Charsets.java b/Source/com/drew/lang/Charsets.java new file mode 100644 index 0000000..dcb2efd --- /dev/null +++ b/Source/com/drew/lang/Charsets.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.lang; + +import java.nio.charset.Charset; + +/** + * Holds a set of commonly used character encodings. + * + * Newer JDKs include java.nio.charset.StandardCharsets, but we cannot use that in this library. + * + * @author Drew Noakes https://drewnoakes.com + */ +public final class Charsets +{ + public static final Charset UTF_8 = Charset.forName("UTF-8"); + public static final Charset UTF_16 = Charset.forName("UTF-16"); + public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + public static final Charset ASCII = Charset.forName("US-ASCII"); + public static final Charset UTF_16BE = Charset.forName("UTF-16BE"); + public static final Charset UTF_16LE = Charset.forName("UTF-16LE"); +} diff --git a/Source/com/drew/lang/CompoundException.java b/Source/com/drew/lang/CompoundException.java index a7ed181..d065fec 100644 --- a/Source/com/drew/lang/CompoundException.java +++ b/Source/com/drew/lang/CompoundException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/lang/DateUtil.java b/Source/com/drew/lang/DateUtil.java new file mode 100644 index 0000000..56484df --- /dev/null +++ b/Source/com/drew/lang/DateUtil.java @@ -0,0 +1,32 @@ +package com.drew.lang; + +/** + * @author Drew Noakes http://drewnoakes.com + */ +public class DateUtil +{ + private static int[] _daysInMonth365 = new int[] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + + public static boolean isValidDate(int year, int month, int day) + { + if (year < 1 || year > 9999 || month < 0 || month > 11) + return false; + + int daysInMonth = _daysInMonth365[month]; + if (month == 1) + { + boolean isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + if (isLeapYear) + daysInMonth++; + } + + return day >= 1 && day <= daysInMonth; + } + + public static boolean isValidTime(int hours, int minutes, int seconds) + { + return hours >= 0 && hours < 24 + && minutes >= 0 && minutes < 60 + && seconds >= 0 && seconds < 60; + } +} diff --git a/Source/com/drew/lang/GeoLocation.java b/Source/com/drew/lang/GeoLocation.java index a006a59..a346484 100644 --- a/Source/com/drew/lang/GeoLocation.java +++ b/Source/com/drew/lang/GeoLocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,7 +83,7 @@ public final class GeoLocation { double[] dms = decimalToDegreesMinutesSeconds(decimal); DecimalFormat format = new DecimalFormat("0.##"); - return String.format("%s° %s' %s\"", format.format(dms[0]), format.format(dms[1]), format.format(dms[2])); + return String.format("%s\u00B0 %s' %s\"", format.format(dms[0]), format.format(dms[1]), format.format(dms[2])); } /** diff --git a/Source/com/drew/lang/KeyValuePair.java b/Source/com/drew/lang/KeyValuePair.java index 313a611..bb22868 100644 --- a/Source/com/drew/lang/KeyValuePair.java +++ b/Source/com/drew/lang/KeyValuePair.java @@ -1,18 +1,39 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.lang; import com.drew.lang.annotations.NotNull; +import com.drew.metadata.StringValue; /** - * Models a key/value pair, where both are non-null {@link String} objects. + * Models a key/value pair, where both are non-null {@link StringValue} objects. * * @author Drew Noakes https://drewnoakes.com */ public class KeyValuePair { private final String _key; - private final String _value; + private final StringValue _value; - public KeyValuePair(@NotNull String key, @NotNull String value) + public KeyValuePair(@NotNull String key, @NotNull StringValue value) { _key = key; _value = value; @@ -25,7 +46,7 @@ public class KeyValuePair } @NotNull - public String getValue() + public StringValue getValue() { return _value; } diff --git a/Source/com/drew/lang/NullOutputStream.java b/Source/com/drew/lang/NullOutputStream.java index 3e9b583..4be4933 100644 --- a/Source/com/drew/lang/NullOutputStream.java +++ b/Source/com/drew/lang/NullOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/lang/RandomAccessFileReader.java b/Source/com/drew/lang/RandomAccessFileReader.java index 76c5407..5f23fbe 100644 --- a/Source/com/drew/lang/RandomAccessFileReader.java +++ b/Source/com/drew/lang/RandomAccessFileReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,17 +39,33 @@ public class RandomAccessFileReader extends RandomAccessReader private final long _length; private int _currentIndex; + private final int _baseOffset; + @SuppressWarnings({ "ConstantConditions" }) @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent") public RandomAccessFileReader(@NotNull RandomAccessFile file) throws IOException + { + this(file, 0); + } + + @SuppressWarnings({ "ConstantConditions" }) + @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent") + public RandomAccessFileReader(@NotNull RandomAccessFile file, int baseOffset) throws IOException { if (file == null) throw new NullPointerException(); _file = file; + _baseOffset = baseOffset; _length = _file.length(); } + @Override + public int toUnshiftedOffset(int localOffset) + { + return localOffset + _baseOffset; + } + @Override public long getLength() { @@ -57,7 +73,7 @@ public class RandomAccessFileReader extends RandomAccessReader } @Override - protected byte getByte(int index) throws IOException + public byte getByte(int index) throws IOException { if (index != _currentIndex) seek(index); diff --git a/Source/com/drew/lang/RandomAccessReader.java b/Source/com/drew/lang/RandomAccessReader.java index 4e64865..7e5987a 100644 --- a/Source/com/drew/lang/RandomAccessReader.java +++ b/Source/com/drew/lang/RandomAccessReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,12 @@ package com.drew.lang; import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.StringValue; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; /** * Base class for random access data reading operations of common data types. @@ -44,6 +47,8 @@ public abstract class RandomAccessReader { private boolean _isMotorolaByteOrder = true; + public abstract int toUnshiftedOffset(int localOffset); + /** * Gets the byte value at the specified byte <code>index</code>. * <p> @@ -52,11 +57,11 @@ public abstract class RandomAccessReader * * @param index The index from which to read the byte * @return The read byte value - * @throws IllegalArgumentException <code>index</code> or <code>count</code> are negative + * @throws IllegalArgumentException <code>index</code> is negative * @throws BufferBoundsException if the requested byte is beyond the end of the underlying data source * @throws IOException if the byte is unable to be read */ - protected abstract byte getByte(int index) throws IOException; + public abstract byte getByte(int index) throws IOException; /** * Returns the required number of bytes from the specified index from the underlying source. @@ -125,6 +130,24 @@ public abstract class RandomAccessReader return _isMotorolaByteOrder; } + /** + * Gets whether a bit at a specific index is set or not. + * + * @param index the number of bits at which to test + * @return true if the bit is set, otherwise false + * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative + */ + public boolean getBit(int index) throws IOException + { + int byteIndex = index / 8; + int bitIndex = index % 8; + + validateIndex(byteIndex, 1); + + byte b = getByte(byteIndex); + return ((b >> bitIndex) & 1) == 1; + } + /** * Returns an unsigned 8-bit int calculated from one byte of data at the specified index. * @@ -197,6 +220,30 @@ public abstract class RandomAccessReader } } + /** + * Get a 24-bit unsigned integer from the buffer, returning it as an int. + * + * @param index position within the data buffer to read first byte + * @return the unsigned 24-bit int value as a long, between 0x00000000 and 0x00FFFFFF + * @throws IOException the buffer does not contain enough bytes to service the request, or index is negative + */ + public int getInt24(int index) throws IOException + { + validateIndex(index, 3); + + if (_isMotorolaByteOrder) { + // Motorola - MSB first (big endian) + return (((int)getByte(index )) << 16 & 0xFF0000) | + (((int)getByte(index + 1)) << 8 & 0xFF00) | + (((int)getByte(index + 2)) & 0xFF); + } else { + // Intel ordering - LSB first (little endian) + return (((int)getByte(index + 2)) << 16 & 0xFF0000) | + (((int)getByte(index + 1)) << 8 & 0xFF00) | + (((int)getByte(index )) & 0xFF); + } + } + /** * Get a 32-bit unsigned integer from the buffer, returning it as a long. * @@ -322,13 +369,19 @@ public abstract class RandomAccessReader } @NotNull - public String getString(int index, int bytesRequested) throws IOException + public StringValue getStringValue(int index, int bytesRequested, @Nullable Charset charset) throws IOException { - return new String(getBytes(index, bytesRequested)); + return new StringValue(getBytes(index, bytesRequested), charset); } @NotNull - public String getString(int index, int bytesRequested, String charset) throws IOException + public String getString(int index, int bytesRequested, @NotNull Charset charset) throws IOException + { + return new String(getBytes(index, bytesRequested), charset.name()); + } + + @NotNull + public String getString(int index, int bytesRequested, @NotNull String charset) throws IOException { byte[] bytes = getBytes(index, bytesRequested); try { @@ -349,17 +402,44 @@ public abstract class RandomAccessReader * @throws IOException The buffer does not contain enough bytes to satisfy this request. */ @NotNull - public String getNullTerminatedString(int index, int maxLengthBytes) throws IOException + public String getNullTerminatedString(int index, int maxLengthBytes, @NotNull Charset charset) throws IOException { - // NOTE currently only really suited to single-byte character strings + return new String(getNullTerminatedBytes(index, maxLengthBytes), charset.name()); + } - byte[] bytes = getBytes(index, maxLengthBytes); + @NotNull + public StringValue getNullTerminatedStringValue(int index, int maxLengthBytes, @Nullable Charset charset) throws IOException + { + byte[] bytes = getNullTerminatedBytes(index, maxLengthBytes); + + return new StringValue(bytes, charset); + } + + /** + * Returns the sequence of bytes punctuated by a <code>\0</code> value. + * + * @param index The index within the buffer at which to start reading the string. + * @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit, + * the returned array will be <code>maxLengthBytes</code> long. + * @return The read byte array, excluding the null terminator. + * @throws IOException The buffer does not contain enough bytes to satisfy this request. + */ + @NotNull + public byte[] getNullTerminatedBytes(int index, int maxLengthBytes) throws IOException + { + byte[] buffer = getBytes(index, maxLengthBytes); // Count the number of non-null bytes int length = 0; - while (length < bytes.length && bytes[length] != '\0') + while (length < buffer.length && buffer[length] != 0) length++; - return new String(bytes, 0, length); + if (length == maxLengthBytes) + return buffer; + + byte[] bytes = new byte[length]; + if (length > 0) + System.arraycopy(buffer, 0, bytes, 0, length); + return bytes; } } diff --git a/Source/com/drew/lang/RandomAccessStreamReader.java b/Source/com/drew/lang/RandomAccessStreamReader.java index 5da26ed..e9c4056 100644 --- a/Source/com/drew/lang/RandomAccessStreamReader.java +++ b/Source/com/drew/lang/RandomAccessStreamReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ import java.util.ArrayList; */ public class RandomAccessStreamReader extends RandomAccessReader { - private final static int DEFAULT_CHUNK_LENGTH = 2 * 1024; + public final static int DEFAULT_CHUNK_LENGTH = 2 * 1024; @NotNull private final InputStream _stream; @@ -41,15 +41,19 @@ public class RandomAccessStreamReader extends RandomAccessReader private final ArrayList<byte[]> _chunks = new ArrayList<byte[]>(); private boolean _isStreamFinished; - private int _streamLength; + private long _streamLength; public RandomAccessStreamReader(@NotNull InputStream stream) { - this(stream, DEFAULT_CHUNK_LENGTH); + this(stream, DEFAULT_CHUNK_LENGTH, -1); } - @SuppressWarnings("ConstantConditions") public RandomAccessStreamReader(@NotNull InputStream stream, int chunkLength) + { + this(stream, chunkLength, -1); + } + + public RandomAccessStreamReader(@NotNull InputStream stream, int chunkLength, long streamLength) { if (stream == null) throw new NullPointerException(); @@ -58,6 +62,7 @@ public class RandomAccessStreamReader extends RandomAccessReader _chunkLength = chunkLength; _stream = stream; + _streamLength = streamLength; } /** @@ -69,6 +74,10 @@ public class RandomAccessStreamReader extends RandomAccessReader @Override public long getLength() throws IOException { + if (_streamLength != -1) { + return _streamLength; + } + isValidIndex(Integer.MAX_VALUE, 1); assert(_isStreamFinished); return _streamLength; @@ -77,7 +86,7 @@ public class RandomAccessStreamReader extends RandomAccessReader /** * Ensures that the buffered bytes extend to cover the specified index. If not, an attempt is made * to read to that point. - * <p/> + * <p> * If the stream ends before the point is reached, a {@link BufferBoundsException} is raised. * * @param index the index from which the required bytes start @@ -134,7 +143,12 @@ public class RandomAccessStreamReader extends RandomAccessReader if (bytesRead == -1) { // the stream has ended, which may be ok _isStreamFinished = true; - _streamLength = _chunks.size() * _chunkLength + totalBytesRead; + int observedStreamLength = _chunks.size() * _chunkLength + totalBytesRead; + if (_streamLength == -1) { + _streamLength = observedStreamLength; + } else if (_streamLength != observedStreamLength) { + assert(false); + } // check we have enough bytes for the requested index if (endIndex >= _streamLength) { @@ -153,7 +167,13 @@ public class RandomAccessStreamReader extends RandomAccessReader } @Override - protected byte getByte(int index) throws IOException + public int toUnshiftedOffset(int localOffset) + { + return localOffset; + } + + @Override + public byte getByte(int index) throws IOException { assert(index >= 0); diff --git a/Source/com/drew/lang/Rational.java b/Source/com/drew/lang/Rational.java index 5b80617..8a83002 100644 --- a/Source/com/drew/lang/Rational.java +++ b/Source/com/drew/lang/Rational.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,8 @@ import java.io.Serializable; * * @author Drew Noakes https://drewnoakes.com */ -public class Rational extends java.lang.Number implements Serializable +@SuppressWarnings("WeakerAccess") +public class Rational extends java.lang.Number implements Comparable<Rational>, Serializable { private static final long serialVersionUID = 510688928138848770L; @@ -174,6 +175,12 @@ public class Rational extends java.lang.Number implements Serializable (_denominator == 0 && _numerator == 0); } + /** Checks if either the numerator or denominator are zero. */ + public boolean isZero() + { + return _numerator == 0 || _denominator == 0; + } + /** * Returns a string representation of the object of form <code>numerator/denominator</code>. * @@ -211,16 +218,47 @@ public class Rational extends java.lang.Number implements Serializable } /** - * Decides whether a brute-force simplification calculation should be avoided - * by comparing the maximum number of possible calculations with some threshold. + * Compares two {@link Rational} instances, returning true if they are mathematically + * equivalent (in consistence with {@link Rational#equals(Object)} method). * - * @return true if the simplification should be performed, otherwise false + * @param that the {@link Rational} to compare this instance to. + * @return the value {@code 0} if this {@link Rational} is + * equal to the argument {@link Rational} mathematically; a value less + * than {@code 0} if this {@link Rational} is less + * than the argument {@link Rational}; and a value greater + * than {@code 0} if this {@link Rational} is greater than the argument + * {@link Rational}. */ - private boolean tooComplexForSimplification() - { - double maxPossibleCalculations = (((double) (Math.min(_denominator, _numerator) - 1) / 5d) + 2); - final int maxSimplificationCalculations = 1000; - return maxPossibleCalculations > maxSimplificationCalculations; + public int compareTo(@NotNull Rational that) { + return Double.compare(this.doubleValue(), that.doubleValue()); + } + + /** + * Indicates whether this instance and <code>other</code> are numerically equal, + * even if their representations differ. + * + * For example, 1/2 is equal to 10/20 by this method. + * Similarly, 1/0 is equal to 100/0 by this method. + * To test equal representations, use EqualsExact. + * + * @param other The rational value to compare with + */ + public boolean equals(Rational other) { + return other.doubleValue() == doubleValue(); + } + + /** + * Indicates whether this instance and <code>other</code> have identical + * Numerator and Denominator. + * <p> + * For example, 1/2 is not equal to 10/20 by this method. + * Similarly, 1/0 is not equal to 100/0 by this method. + * To test numerically equivalence, use Equals(Rational).</p> + * + * @param other The rational value to compare with + */ + public boolean equalsExact(Rational other) { + return getDenominator() == other.getDenominator() && getNumerator() == other.getNumerator(); } /** @@ -248,49 +286,38 @@ public class Rational extends java.lang.Number implements Serializable /** * <p> - * Simplifies the {@link Rational} number.</p> - * <p> - * Prime number series: 1, 2, 3, 5, 7, 9, 11, 13, 17</p> - * <p> - * To reduce a rational, need to see if both numerator and denominator are divisible - * by a common factor. Using the prime number series in ascending order guarantees - * the minimum number of checks required.</p> + * Simplifies the representation of this {@link Rational} number.</p> * <p> - * However, generating the prime number series seems to be a hefty task. Perhaps - * it's simpler to check if both d & n are divisible by all numbers from 2 {@literal ->} - * (Math.min(denominator, numerator) / 2). In doing this, one can check for 2 - * and 5 once, then ignore all even numbers, and all numbers ending in 0 or 5. - * This leaves four numbers from every ten to check.</p> + * For example, 5/10 simplifies to 1/2 because both Numerator + * and Denominator share a common factor of 5.</p> * <p> - * Therefore, the max number of pairs of modulus divisions required will be:</p> - * <pre><code> - * 4 Math.min(denominator, numerator) - 1 - * -- * ------------------------------------ + 2 - * 10 2 + * Uses the Euclidean Algorithm to find the greatest common divisor.</p> * - * Math.min(denominator, numerator) - 1 - * = ------------------------------------ + 2 - * 5 - * </code></pre> - * - * @return a simplified instance, or if the Rational could not be simplified, - * returns itself (unchanged) + * @return A simplified instance if one exists, otherwise a copy of the original value. */ @NotNull public Rational getSimplifiedInstance() { - if (tooComplexForSimplification()) { - return this; - } - for (int factor = 2; factor <= Math.min(_denominator, _numerator); factor++) { - if ((factor % 2 == 0 && factor > 2) || (factor % 5 == 0 && factor > 5)) { - continue; - } - if (_denominator % factor == 0 && _numerator % factor == 0) { - // found a common factor - return new Rational(_numerator / factor, _denominator / factor); - } + long gcd = GCD(_numerator, _denominator); + + return new Rational(_numerator / gcd, _denominator / gcd); + } + + private static long GCD(long a, long b) + { + if (a < 0) + a = -a; + if (b < 0) + b = -b; + + while (a != 0 && b != 0) + { + if (a > b) + a %= b; + else + b %= a; } - return this; + + return a == 0 ? b : a; } } diff --git a/Source/com/drew/lang/SequentialByteArrayReader.java b/Source/com/drew/lang/SequentialByteArrayReader.java index 3b18c84..846efeb 100644 --- a/Source/com/drew/lang/SequentialByteArrayReader.java +++ b/Source/com/drew/lang/SequentialByteArrayReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,18 +36,29 @@ public class SequentialByteArrayReader extends SequentialReader private final byte[] _bytes; private int _index; - @SuppressWarnings("ConstantConditions") + @Override + public long getPosition() + { + return _index; + } + public SequentialByteArrayReader(@NotNull byte[] bytes) + { + this(bytes, 0); + } + + @SuppressWarnings("ConstantConditions") + public SequentialByteArrayReader(@NotNull byte[] bytes, int baseIndex) { if (bytes == null) throw new NullPointerException(); _bytes = bytes; - _index = 0; + _index = baseIndex; } @Override - protected byte getByte() throws IOException + public byte getByte() throws IOException { if (_index >= _bytes.length) { throw new EOFException("End of data reached."); @@ -70,6 +81,17 @@ public class SequentialByteArrayReader extends SequentialReader return bytes; } + @Override + public void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException + { + if (_index + count > _bytes.length) { + throw new EOFException("End of data reached."); + } + + System.arraycopy(_bytes, _index, buffer, offset, count); + _index += count; + } + @Override public void skip(long n) throws IOException { @@ -100,4 +122,9 @@ public class SequentialByteArrayReader extends SequentialReader return true; } + + @Override + public int available() { + return _bytes.length - _index; + } } diff --git a/Source/com/drew/lang/SequentialReader.java b/Source/com/drew/lang/SequentialReader.java index a1bcdb5..d172aa9 100644 --- a/Source/com/drew/lang/SequentialReader.java +++ b/Source/com/drew/lang/SequentialReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,26 +22,32 @@ package com.drew.lang; import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.StringValue; import java.io.EOFException; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; /** * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public abstract class SequentialReader { // TODO review whether the masks are needed (in both this and RandomAccessReader) private boolean _isMotorolaByteOrder = true; + public abstract long getPosition() throws IOException; + /** * Gets the next byte in the sequence. * * @return The read byte value */ - protected abstract byte getByte() throws IOException; + public abstract byte getByte() throws IOException; /** * Returns the required number of bytes from the sequence. @@ -52,6 +58,14 @@ public abstract class SequentialReader @NotNull public abstract byte[] getBytes(int count) throws IOException; + /** + * Retrieves bytes, writing them into a caller-provided buffer. + * @param buffer The array to write bytes to. + * @param offset The starting position within buffer to write to. + * @param count The number of bytes to be written. + */ + public abstract void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException; + /** * Skips forward in the sequence. If the sequence ends, an {@link EOFException} is thrown. * @@ -70,6 +84,24 @@ public abstract class SequentialReader */ public abstract boolean trySkip(long n) throws IOException; + /** + * Returns an estimate of the number of bytes that can be read (or skipped + * over) from this {@link SequentialReader} without blocking by the next + * invocation of a method for this input stream. A single read or skip of + * this many bytes will not block, but may read or skip fewer bytes. + * <p> + * Note that while some implementations of {@link SequentialReader} like + * {@link SequentialByteArrayReader} will return the total remaining number + * of bytes in the stream, others will not. It is never correct to use the + * return value of this method to allocate a buffer intended to hold all + * data in this stream. + * + * @return an estimate of the number of bytes that can be read (or skipped + * over) from this {@link SequentialReader} without blocking or + * {@code 0} when it reaches the end of the input stream. + */ + public abstract int available(); + /** * Sets the endianness of this reader. * <ul> @@ -283,6 +315,19 @@ public abstract class SequentialReader } } + @NotNull + public String getString(int bytesRequested, @NotNull Charset charset) throws IOException + { + byte[] bytes = getBytes(bytesRequested); + return new String(bytes, charset); + } + + @NotNull + public StringValue getStringValue(int bytesRequested, @Nullable Charset charset) throws IOException + { + return new StringValue(getBytes(bytesRequested), charset); + } + /** * Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>. * @@ -292,17 +337,53 @@ public abstract class SequentialReader * @throws IOException The buffer does not contain enough bytes to satisfy this request. */ @NotNull - public String getNullTerminatedString(int maxLengthBytes) throws IOException + public String getNullTerminatedString(int maxLengthBytes, Charset charset) throws IOException + { + return getNullTerminatedStringValue(maxLengthBytes, charset).toString(); + } + + /** + * Creates a String from the stream, ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>. + * + * @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit, + * reading will stop and the string will be truncated to this length. + * @param charset The <code>Charset</code> to register with the returned <code>StringValue</code>, or <code>null</code> if the encoding + * is unknown + * @return The read string. + * @throws IOException The buffer does not contain enough bytes to satisfy this request. + */ + @NotNull + public StringValue getNullTerminatedStringValue(int maxLengthBytes, Charset charset) throws IOException { - // NOTE currently only really suited to single-byte character strings + byte[] bytes = getNullTerminatedBytes(maxLengthBytes); - byte[] bytes = new byte[maxLengthBytes]; + return new StringValue(bytes, charset); + } + + /** + * Returns the sequence of bytes punctuated by a <code>\0</code> value. + * + * @param maxLengthBytes The maximum number of bytes to read. If a <code>\0</code> byte is not reached within this limit, + * the returned array will be <code>maxLengthBytes</code> long. + * @return The read byte array, excluding the null terminator. + * @throws IOException The buffer does not contain enough bytes to satisfy this request. + */ + @NotNull + public byte[] getNullTerminatedBytes(int maxLengthBytes) throws IOException + { + byte[] buffer = new byte[maxLengthBytes]; // Count the number of non-null bytes int length = 0; - while (length < bytes.length && (bytes[length] = getByte()) != '\0') + while (length < buffer.length && (buffer[length] = getByte()) != 0) length++; - return new String(bytes, 0, length); + if (length == maxLengthBytes) + return buffer; + + byte[] bytes = new byte[length]; + if (length > 0) + System.arraycopy(buffer, 0, bytes, 0, length); + return bytes; } } diff --git a/Source/com/drew/lang/StreamReader.java b/Source/com/drew/lang/StreamReader.java index c55db50..1e30b12 100644 --- a/Source/com/drew/lang/StreamReader.java +++ b/Source/com/drew/lang/StreamReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,14 @@ public class StreamReader extends SequentialReader @NotNull private final InputStream _stream; + private long _pos; + + @Override + public long getPosition() + { + return _pos; + } + @SuppressWarnings("ConstantConditions") public StreamReader(@NotNull InputStream stream) { @@ -43,14 +51,16 @@ public class StreamReader extends SequentialReader throw new NullPointerException(); _stream = stream; + _pos = 0; } @Override - protected byte getByte() throws IOException + public byte getByte() throws IOException { int value = _stream.read(); if (value == -1) throw new EOFException("End of data reached."); + _pos++; return (byte)value; } @@ -59,17 +69,23 @@ public class StreamReader extends SequentialReader public byte[] getBytes(int count) throws IOException { byte[] bytes = new byte[count]; - int totalBytesRead = 0; + getBytes(bytes, 0, count); + return bytes; + } - while (totalBytesRead != count) { - final int bytesRead = _stream.read(bytes, totalBytesRead, count - totalBytesRead); + @Override + public void getBytes(@NotNull byte[] buffer, int offset, int count) throws IOException + { + int totalBytesRead = 0; + while (totalBytesRead != count) + { + final int bytesRead = _stream.read(buffer, offset + totalBytesRead, count - totalBytesRead); if (bytesRead == -1) throw new EOFException("End of data reached."); totalBytesRead += bytesRead; assert(totalBytesRead <= count); } - - return bytes; + _pos += totalBytesRead; } @Override @@ -93,6 +109,15 @@ public class StreamReader extends SequentialReader return skipInternal(n) == n; } + @Override + public int available() { + try { + return _stream.available(); + } catch (IOException e) { + return 0; + } + } + private long skipInternal(long n) throws IOException { // It seems that for some streams, such as BufferedInputStream, that skip can return @@ -109,6 +134,7 @@ public class StreamReader extends SequentialReader if (skipped == 0) break; } + _pos += skippedTotal; return skippedTotal; } } diff --git a/Source/com/drew/lang/StreamUtil.java b/Source/com/drew/lang/StreamUtil.java new file mode 100644 index 0000000..d8dea16 --- /dev/null +++ b/Source/com/drew/lang/StreamUtil.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.lang; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author Drew Noakes https://drewnoakes.com + */ +public final class StreamUtil +{ + public static byte[] readAllBytes(InputStream stream) throws IOException + { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + byte[] buffer = new byte[1024]; + while (true) { + int bytesRead = stream.read(buffer); + if (bytesRead == -1) + break; + outputStream.write(buffer, 0, bytesRead); + } + + return outputStream.toByteArray(); + } +} diff --git a/Source/com/drew/lang/StringUtil.java b/Source/com/drew/lang/StringUtil.java index 35c3a9d..9423c35 100644 --- a/Source/com/drew/lang/StringUtil.java +++ b/Source/com/drew/lang/StringUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ import java.util.Iterator; /** * @author Drew Noakes https://drewnoakes.com */ -public class StringUtil +public final class StringUtil { @NotNull public static String join(@NotNull Iterable<? extends CharSequence> strings, @NotNull String delimiter) diff --git a/Source/com/drew/lang/annotations/NotNull.java b/Source/com/drew/lang/annotations/NotNull.java index 4bba728..0c48378 100644 --- a/Source/com/drew/lang/annotations/NotNull.java +++ b/Source/com/drew/lang/annotations/NotNull.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/lang/annotations/Nullable.java b/Source/com/drew/lang/annotations/Nullable.java index e37e45f..017d280 100644 --- a/Source/com/drew/lang/annotations/Nullable.java +++ b/Source/com/drew/lang/annotations/Nullable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/lang/annotations/package-info.java b/Source/com/drew/lang/annotations/package-info.java new file mode 100644 index 0000000..5e2f5a5 --- /dev/null +++ b/Source/com/drew/lang/annotations/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains annotations used to extend the signatures of methods and fields, allowing tools such as IntelliJ IDEA + * to provide design-time warnings about potential run-time errors. + */ +package com.drew.lang.annotations; diff --git a/Source/com/drew/lang/annotations/package.html b/Source/com/drew/lang/annotations/package.html deleted file mode 100644 index c648601..0000000 --- a/Source/com/drew/lang/annotations/package.html +++ /dev/null @@ -1,34 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains annotations used to extend the signatures of methods and fields, allowing tools such as IntelliJ IDEA -to provide design-time warnings about potential run-time errors. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/lang/package-info.java b/Source/com/drew/lang/package-info.java new file mode 100644 index 0000000..6c4e041 --- /dev/null +++ b/Source/com/drew/lang/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes of generic utility. + */ +package com.drew.lang; diff --git a/Source/com/drew/lang/package.html b/Source/com/drew/lang/package.html deleted file mode 100644 index aa6d0d0..0000000 --- a/Source/com/drew/lang/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes of generic utility. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/metadata/Age.java b/Source/com/drew/metadata/Age.java index 4022214..03d6cd6 100644 --- a/Source/com/drew/metadata/Age.java +++ b/Source/com/drew/metadata/Age.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,9 +50,6 @@ public class Age @Nullable public static Age fromPanasonicString(@NotNull String s) { - if (s == null) - throw new NullPointerException(); - if (s.length() != 19 || s.startsWith("9999:99:99")) return null; @@ -142,7 +139,7 @@ public class Age } @Override - public boolean equals(Object o) + public boolean equals(@Nullable Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; diff --git a/Source/com/drew/metadata/Directory.java b/Source/com/drew/metadata/Directory.java index 44f5628..5ab379a 100644 --- a/Source/com/drew/metadata/Directory.java +++ b/Source/com/drew/metadata/Directory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,9 +28,12 @@ import com.drew.lang.annotations.SuppressWarnings; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; import java.text.DateFormat; +import java.text.DecimalFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Abstract base class for all directory implementations, having methods for getting and setting tag values of various @@ -38,8 +41,11 @@ import java.util.*; * * @author Drew Noakes https://drewnoakes.com */ +@java.lang.SuppressWarnings("WeakerAccess") public abstract class Directory { + private static final String _floatFormatPattern = "0.###"; + /** Map of values hashed by type identifiers. */ @NotNull protected final Map<Integer, Object> _tagMap = new HashMap<Integer, Object>(); @@ -58,6 +64,9 @@ public abstract class Directory /** The descriptor used to interpret tag values. */ protected TagDescriptor _descriptor; + @Nullable + private Directory _parent; + // ABSTRACT METHODS /** @@ -81,6 +90,14 @@ public abstract class Directory // VARIOUS METHODS + /** + * Gets a value indicating whether the directory is empty, meaning it contains no errors and no tag values. + */ + public boolean isEmpty() + { + return _errorList.isEmpty() && _definedTagList.isEmpty(); + } + /** * Indicates whether the specified tag type has been set. * @@ -164,6 +181,17 @@ public abstract class Directory return _errorList.size(); } + @Nullable + public Directory getParent() + { + return _parent; + } + + public void setParent(@NotNull Directory parent) + { + _parent = parent; + } + // TAG SETTERS /** @@ -232,6 +260,20 @@ public abstract class Directory setObjectArray(tagType, doubles); } + /** + * Sets a <code>StringValue</code> value for the specified tag. + * + * @param tagType the tag's value as an int + * @param value the value for the specified tag as a StringValue + */ + @java.lang.SuppressWarnings({ "ConstantConditions" }) + public void setStringValue(int tagType, @NotNull StringValue value) + { + if (value == null) + throw new NullPointerException("cannot set a null StringValue"); + setObject(tagType, value); + } + /** * Sets a <code>String</code> value for the specified tag. * @@ -257,6 +299,17 @@ public abstract class Directory setObjectArray(tagType, strings); } + /** + * Sets a <code>StringValue[]</code> (array) for the specified tag. + * + * @param tagType the tag identifier + * @param strings the StringValue array to store + */ + public void setStringValueArray(int tagType, @NotNull StringValue[] strings) + { + setObjectArray(tagType, strings); + } + /** * Sets a <code>boolean</code> value for the specified tag. * @@ -413,12 +466,12 @@ public abstract class Directory if (o instanceof Number) { return ((Number)o).intValue(); - } else if (o instanceof String) { + } else if (o instanceof String || o instanceof StringValue) { try { - return Integer.parseInt((String)o); + return Integer.parseInt(o.toString()); } catch (NumberFormatException nfe) { // convert the char array to an int - String s = (String)o; + String s = o.toString(); byte[] bytes = s.getBytes(); long val = 0; for (byte aByte : bytes) { @@ -439,13 +492,17 @@ public abstract class Directory int[] ints = (int[])o; if (ints.length == 1) return ints[0]; + } else if (o instanceof short[]) { + short[] shorts = (short[])o; + if (shorts.length == 1) + return (int)shorts[0]; } return null; } /** * Gets the specified tag's value as a String array, if possible. Only supported - * where the tag is set as String[], String, int[], byte[] or Rational[]. + * where the tag is set as StringValue[], String[], StringValue, String, int[], byte[] or Rational[]. * * @param tagType the tag identifier * @return the tag's value as an array of Strings. If the value is unset or cannot be converted, <code>null</code> is returned. @@ -460,19 +517,30 @@ public abstract class Directory return (String[])o; if (o instanceof String) return new String[] { (String)o }; + if (o instanceof StringValue) + return new String[] { o.toString() }; + if (o instanceof StringValue[]) { + StringValue[] stringValues = (StringValue[])o; + String[] strings = new String[stringValues.length]; + for (int i = 0; i < strings.length; i++) + strings[i] = stringValues[i].toString(); + return strings; + } if (o instanceof int[]) { int[] ints = (int[])o; String[] strings = new String[ints.length]; for (int i = 0; i < strings.length; i++) strings[i] = Integer.toString(ints[i]); return strings; - } else if (o instanceof byte[]) { + } + if (o instanceof byte[]) { byte[] bytes = (byte[])o; String[] strings = new String[bytes.length]; for (int i = 0; i < strings.length; i++) strings[i] = Byte.toString(bytes[i]); return strings; - } else if (o instanceof Rational[]) { + } + if (o instanceof Rational[]) { Rational[] rationals = (Rational[])o; String[] strings = new String[rationals.length]; for (int i = 0; i < strings.length; i++) @@ -482,6 +550,26 @@ public abstract class Directory return null; } + /** + * Gets the specified tag's value as a StringValue array, if possible. + * Only succeeds if the tag is set as StringValue[], or StringValue. + * + * @param tagType the tag identifier + * @return the tag's value as an array of StringValues. If the value is unset or cannot be converted, <code>null</code> is returned. + */ + @Nullable + public StringValue[] getStringValueArray(int tagType) + { + Object o = getObject(tagType); + if (o == null) + return null; + if (o instanceof StringValue[]) + return (StringValue[])o; + if (o instanceof StringValue) + return new StringValue[] {(StringValue) o}; + return null; + } + /** * Gets the specified tag's value as an int array, if possible. Only supported * where the tag is set as String, Integer, int[], byte[] or Rational[]. @@ -548,6 +636,8 @@ public abstract class Directory Object o = getObject(tagType); if (o == null) { return null; + } else if (o instanceof StringValue) { + return ((StringValue)o).getBytes(); } else if (o instanceof Rational[]) { Rational[] rationals = (Rational[])o; byte[] bytes = new byte[rationals.length]; @@ -603,9 +693,9 @@ public abstract class Directory Object o = getObject(tagType); if (o == null) return null; - if (o instanceof String) { + if (o instanceof String || o instanceof StringValue) { try { - return Double.parseDouble((String)o); + return Double.parseDouble(o.toString()); } catch (NumberFormatException nfe) { return null; } @@ -635,9 +725,9 @@ public abstract class Directory Object o = getObject(tagType); if (o == null) return null; - if (o instanceof String) { + if (o instanceof String || o instanceof StringValue) { try { - return Float.parseFloat((String)o); + return Float.parseFloat(o.toString()); } catch (NumberFormatException nfe) { return null; } @@ -651,7 +741,7 @@ public abstract class Directory public long getLong(int tagType) throws MetadataException { Long value = getLongObject(tagType); - if (value!=null) + if (value != null) return value; Object o = getObject(tagType); if (o == null) @@ -666,9 +756,9 @@ public abstract class Directory Object o = getObject(tagType); if (o == null) return null; - if (o instanceof String) { + if (o instanceof String || o instanceof StringValue) { try { - return Long.parseLong((String)o); + return Long.parseLong(o.toString()); } catch (NumberFormatException nfe) { return null; } @@ -682,7 +772,7 @@ public abstract class Directory public boolean getBoolean(int tagType) throws MetadataException { Boolean value = getBooleanObject(tagType); - if (value!=null) + if (value != null) return value; Object o = getObject(tagType); if (o == null) @@ -700,9 +790,9 @@ public abstract class Directory return null; if (o instanceof Boolean) return (Boolean)o; - if (o instanceof String) { + if (o instanceof String || o instanceof StringValue) { try { - return Boolean.getBoolean((String)o); + return Boolean.getBoolean(o.toString()); } catch (NumberFormatException nfe) { return null; } @@ -716,12 +806,12 @@ public abstract class Directory * Returns the specified tag's value as a java.util.Date. If the value is unset or cannot be converted, <code>null</code> is returned. * <p> * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in - * the current {@link TimeZone}. If the {@link TimeZone} is known, call the overload that accepts one as an argument. + * the GMT {@link TimeZone}. If the {@link TimeZone} is known, call the overload that accepts one as an argument. */ @Nullable public java.util.Date getDate(int tagType) { - return getDate(tagType, null); + return getDate(tagType, null, null); } /** @@ -729,21 +819,41 @@ public abstract class Directory * <p> * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in * the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null). Note that this parameter - * is only considered if the underlying value is a string and parsing occurs, otherwise it has no effect. + * is only considered if the underlying value is a string and it has no time zone information, otherwise it has no effect. */ @Nullable public java.util.Date getDate(int tagType, @Nullable TimeZone timeZone) { - Object o = getObject(tagType); + return getDate(tagType, null, timeZone); + } - if (o == null) - return null; + /** + * Returns the specified tag's value as a java.util.Date. If the value is unset or cannot be converted, <code>null</code> is returned. + * <p> + * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in + * the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null). Note that this parameter + * is only considered if the underlying value is a string and it has no time zone information, otherwise it has no effect. + * In addition, the {@code subsecond} parameter, which specifies the number of digits after the decimal point in the seconds, + * is set to the returned Date. This parameter is only considered if the underlying value is a string and is has + * no subsecond information, otherwise it has no effect. + * + * @param tagType the tag identifier + * @param subsecond the subsecond value for the Date + * @param timeZone the time zone to use + * @return a Date representing the time value + */ + @Nullable + public java.util.Date getDate(int tagType, @Nullable String subsecond, @Nullable TimeZone timeZone) + { + Object o = getObject(tagType); if (o instanceof java.util.Date) return (java.util.Date)o; - if (o instanceof String) { - // This seems to cover all known Exif date strings + java.util.Date date = null; + + if ((o instanceof String) || (o instanceof StringValue)) { + // This seems to cover all known Exif and Xmp date strings // Note that " : : : : " is a valid date string according to the Exif spec (which means 'unknown date'): http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/datetimeoriginal.html String datePatterns[] = { "yyyy:MM:dd HH:mm:ss", @@ -751,20 +861,66 @@ public abstract class Directory "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy.MM.dd HH:mm:ss", - "yyyy.MM.dd HH:mm" }; - String dateString = (String)o; + "yyyy.MM.dd HH:mm", + "yyyy-MM-dd'T'HH:mm:ss", + "yyyy-MM-dd'T'HH:mm", + "yyyy-MM-dd", + "yyyy-MM", + "yyyyMMdd", // as used in IPTC data + "yyyy" }; + + String dateString = o.toString(); + + // if the date string has subsecond information, it supersedes the subsecond parameter + Pattern subsecondPattern = Pattern.compile("(\\d\\d:\\d\\d:\\d\\d)(\\.\\d+)"); + Matcher subsecondMatcher = subsecondPattern.matcher(dateString); + if (subsecondMatcher.find()) { + subsecond = subsecondMatcher.group(2).substring(1); + dateString = subsecondMatcher.replaceAll("$1"); + } + + // if the date string has time zone information, it supersedes the timeZone parameter + Pattern timeZonePattern = Pattern.compile("(Z|[+-]\\d\\d:\\d\\d)$"); + Matcher timeZoneMatcher = timeZonePattern.matcher(dateString); + if (timeZoneMatcher.find()) { + timeZone = TimeZone.getTimeZone("GMT" + timeZoneMatcher.group().replaceAll("Z", "")); + dateString = timeZoneMatcher.replaceAll(""); + } + for (String datePattern : datePatterns) { try { DateFormat parser = new SimpleDateFormat(datePattern); if (timeZone != null) parser.setTimeZone(timeZone); - return parser.parse(dateString); + else + parser.setTimeZone(TimeZone.getTimeZone("GMT")); // don't interpret zone time + + date = parser.parse(dateString); + break; } catch (ParseException ex) { // simply try the next pattern } } } - return null; + + if (date == null) + return null; + + if (subsecond == null) + return date; + + try { + int millisecond = (int) (Double.parseDouble("." + subsecond) * 1000); + if (millisecond >= 0 && millisecond < 1000) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(date); + calendar.set(Calendar.MILLISECOND, millisecond); + return calendar.getTime(); + } + return date; + } catch (NumberFormatException e) { + return date; + } } /** Returns the specified tag's value as a Rational. If the value is unset or cannot be converted, <code>null</code> is returned. */ @@ -823,37 +979,65 @@ public abstract class Directory // handle arrays of objects and primitives int arrayLength = Array.getLength(o); final Class<?> componentType = o.getClass().getComponentType(); - boolean isObjectArray = Object.class.isAssignableFrom(componentType); - boolean isFloatArray = componentType.getName().equals("float"); - boolean isDoubleArray = componentType.getName().equals("double"); - boolean isIntArray = componentType.getName().equals("int"); - boolean isLongArray = componentType.getName().equals("long"); - boolean isByteArray = componentType.getName().equals("byte"); - boolean isShortArray = componentType.getName().equals("short"); + StringBuilder string = new StringBuilder(); - for (int i = 0; i < arrayLength; i++) { - if (i != 0) - string.append(' '); - if (isObjectArray) + + if (Object.class.isAssignableFrom(componentType)) { + // object array + for (int i = 0; i < arrayLength; i++) { + if (i != 0) + string.append(' '); string.append(Array.get(o, i).toString()); - else if (isIntArray) + } + } else if (componentType.getName().equals("int")) { + for (int i = 0; i < arrayLength; i++) { + if (i != 0) + string.append(' '); string.append(Array.getInt(o, i)); - else if (isShortArray) + } + } else if (componentType.getName().equals("short")) { + for (int i = 0; i < arrayLength; i++) { + if (i != 0) + string.append(' '); string.append(Array.getShort(o, i)); - else if (isLongArray) + } + } else if (componentType.getName().equals("long")) { + for (int i = 0; i < arrayLength; i++) { + if (i != 0) + string.append(' '); string.append(Array.getLong(o, i)); - else if (isFloatArray) - string.append(Array.getFloat(o, i)); - else if (isDoubleArray) - string.append(Array.getDouble(o, i)); - else if (isByteArray) - string.append(Array.getByte(o, i)); - else - addError("Unexpected array component type: " + componentType.getName()); + } + } else if (componentType.getName().equals("float")) { + for (int i = 0; i < arrayLength; i++) { + if (i != 0) + string.append(' '); + string.append(new DecimalFormat(_floatFormatPattern).format(Array.getFloat(o, i))); + } + } else if (componentType.getName().equals("double")) { + for (int i = 0; i < arrayLength; i++) { + if (i != 0) + string.append(' '); + string.append(new DecimalFormat(_floatFormatPattern).format(Array.getDouble(o, i))); + } + } else if (componentType.getName().equals("byte")) { + for (int i = 0; i < arrayLength; i++) { + if (i != 0) + string.append(' '); + string.append(Array.getByte(o, i) & 0xff); + } + } else { + addError("Unexpected array component type: " + componentType.getName()); } + return string.toString(); } + if (o instanceof Double) + return new DecimalFormat(_floatFormatPattern).format(((Double)o).doubleValue()); + + if (o instanceof Float) + return new DecimalFormat(_floatFormatPattern).format(((Float)o).floatValue()); + // Note that several cameras leave trailing spaces (Olympus, Nikon) but this library is intended to show // the actual data within the file. It is not inconceivable that whitespace may be significant here, so we // do not trim. Also, if support is added for writing data back to files, this may cause issues. @@ -874,6 +1058,15 @@ public abstract class Directory } } + @Nullable + public StringValue getStringValue(int tagType) + { + Object o = getObject(tagType); + if (o instanceof StringValue) + return (StringValue)o; + return null; + } + /** * Returns the object hashed for the particular tag type specified, if available. * diff --git a/Source/com/drew/metadata/DefaultTagDescriptor.java b/Source/com/drew/metadata/ErrorDirectory.java similarity index 51% rename from Source/com/drew/metadata/DefaultTagDescriptor.java rename to Source/com/drew/metadata/ErrorDirectory.java index ed157bc..7412421 100644 --- a/Source/com/drew/metadata/DefaultTagDescriptor.java +++ b/Source/com/drew/metadata/ErrorDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,31 +21,55 @@ package com.drew.metadata; import com.drew.lang.annotations.NotNull; +import java.util.*; /** - * A default implementation of the abstract TagDescriptor. As this class is not coded with awareness of any metadata - * tags, it simply reports tag names using the format 'Unknown tag 0x00' (with the corresponding tag number in hex) - * and gives descriptions using the default string representation of the value. + * A directory to use for the reporting of errors. No values may be added to this directory, only warnings and errors. * * @author Drew Noakes https://drewnoakes.com */ -public class DefaultTagDescriptor extends TagDescriptor<Directory> + +public final class ErrorDirectory extends Directory { - public DefaultTagDescriptor(@NotNull Directory directory) + + public ErrorDirectory() + {} + + public ErrorDirectory(String error) { - super(directory); + super.addError(error); } - /** - * Gets a best-effort tag name using the format 'Unknown tag 0x00' (with the corresponding tag type in hex). - * @param tagType the tag type identifier. - * @return a string representation of the tag name. - */ + @Override + @NotNull + public String getName() + { + return "Error"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return new HashMap<Integer, String>(); + } + + @Override @NotNull public String getTagName(int tagType) { - String hex = Integer.toHexString(tagType).toUpperCase(); - while (hex.length() < 4) hex = "0" + hex; - return "Unknown tag 0x" + hex; + return ""; + } + + @Override + public boolean hasTagName(int tagType) + { + return false; + } + + @Override + public void setObject(int tagType, @NotNull Object value) + { + throw new UnsupportedOperationException(String.format("Cannot add value to %s.", ErrorDirectory.class.getName())); } } diff --git a/Source/com/drew/metadata/Face.java b/Source/com/drew/metadata/Face.java index 1cf5a4a..4178adb 100644 --- a/Source/com/drew/metadata/Face.java +++ b/Source/com/drew/metadata/Face.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/metadata/Metadata.java b/Source/com/drew/metadata/Metadata.java index 425be42..d8c9e25 100644 --- a/Source/com/drew/metadata/Metadata.java +++ b/Source/com/drew/metadata/Metadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,97 +35,88 @@ import java.util.*; */ public final class Metadata { - @NotNull - private final Map<Class<? extends Directory>,Directory> _directoryByClass = new HashMap<Class<? extends Directory>, Directory>(); - /** - * List of Directory objects set against this object. Keeping a list handy makes - * creation of an Iterator and counting tags simple. + * The list of {@link Directory} instances in this container, in the order they were added. */ @NotNull - private final Collection<Directory> _directoryList = new ArrayList<Directory>(); + private final List<Directory> _directories = new ArrayList<Directory>(); /** - * Returns an objects for iterating over Directory objects in the order in which they were added. + * Returns an iterable set of the {@link Directory} instances contained in this metadata collection. * - * @return an iterable collection of directories + * @return an iterable set of directories */ @NotNull public Iterable<Directory> getDirectories() { - return Collections.unmodifiableCollection(_directoryList); + return _directories; + } + + @NotNull + @SuppressWarnings("unchecked") + public <T extends Directory> Collection<T> getDirectoriesOfType(Class<T> type) + { + List<T> directories = new ArrayList<T>(); + for (Directory dir : _directories) { + if (type.isAssignableFrom(dir.getClass())) { + directories.add((T)dir); + } + } + return directories; } /** - * Returns a count of unique directories in this metadata collection. + * Returns the count of directories in this metadata collection. * * @return the number of unique directory types set for this metadata collection */ public int getDirectoryCount() { - return _directoryList.size(); + return _directories.size(); } /** - * Returns a {@link Directory} of specified type. If this {@link Metadata} object already contains - * such a directory, it is returned. Otherwise a new instance of this directory will be created and stored within - * this {@link Metadata} object. + * Adds a directory to this metadata collection. * - * @param type the type of the Directory implementation required. - * @return a directory of the specified type. + * @param directory the {@link Directory} to add into this metadata collection. */ - @NotNull - @SuppressWarnings("unchecked") - public <T extends Directory> T getOrCreateDirectory(@NotNull Class<T> type) + public <T extends Directory> void addDirectory(@NotNull T directory) { - // We suppress the warning here as the code asserts a map signature of Class<T>,T. - // So after get(Class<T>) it is for sure the result is from type T. - - // check if we've already issued this type of directory - if (_directoryByClass.containsKey(type)) - return (T)_directoryByClass.get(type); - - T directory; - try { - directory = type.newInstance(); - } catch (Exception e) { - throw new RuntimeException("Cannot instantiate provided Directory type: " + type.toString()); - } - // store the directory - _directoryByClass.put(type, directory); - _directoryList.add(directory); - - return directory; + _directories.add(directory); } /** - * If this {@link Metadata} object contains a {@link Directory} of the specified type, it is returned. - * Otherwise <code>null</code> is returned. + * Gets the first {@link Directory} of the specified type contained within this metadata collection. + * If no instances of this type are present, <code>null</code> is returned. * * @param type the Directory type * @param <T> the Directory type - * @return a Directory of type T if it exists in this {@link Metadata} object, otherwise <code>null</code>. + * @return the first Directory of type T in this metadata collection, or <code>null</code> if none exist */ @Nullable @SuppressWarnings("unchecked") - public <T extends Directory> T getDirectory(@NotNull Class<T> type) + public <T extends Directory> T getFirstDirectoryOfType(@NotNull Class<T> type) { - // We suppress the warning here as the code asserts a map signature of Class<T>,T. - // So after get(Class<T>) it is for sure the result is from type T. - - return (T)_directoryByClass.get(type); + for (Directory dir : _directories) { + if (type.isAssignableFrom(dir.getClass())) + return (T)dir; + } + return null; } /** - * Indicates whether a given directory type has been created in this metadata - * repository. Directories are created by calling {@link Metadata#getOrCreateDirectory(Class)}. + * Indicates whether an instance of the given directory type exists in this Metadata instance. * * @param type the {@link Directory} type - * @return true if the {@link Directory} has been created + * @return <code>true</code> if a {@link Directory} of the specified type exists, otherwise <code>false</code> */ - public boolean containsDirectory(Class<? extends Directory> type) + public boolean containsDirectoryOfType(Class<? extends Directory> type) { - return _directoryByClass.containsKey(type); + for (Directory dir : _directories) { + if (type.isAssignableFrom(dir.getClass())) + return true; + } + return false; } /** @@ -136,7 +127,7 @@ public final class Metadata */ public boolean hasErrors() { - for (Directory directory : _directoryList) { + for (Directory directory : getDirectories()) { if (directory.hasErrors()) return true; } @@ -146,9 +137,10 @@ public final class Metadata @Override public String toString() { + int count = getDirectoryCount(); return String.format("Metadata (%d %s)", - _directoryList.size(), - _directoryList.size() == 1 + count, + count == 1 ? "directory" : "directories"); } diff --git a/Source/com/drew/metadata/MetadataException.java b/Source/com/drew/metadata/MetadataException.java index 72109de..25ba8bb 100644 --- a/Source/com/drew/metadata/MetadataException.java +++ b/Source/com/drew/metadata/MetadataException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/metadata/MetadataReader.java b/Source/com/drew/metadata/MetadataReader.java index 5201149..9459fd3 100644 --- a/Source/com/drew/metadata/MetadataReader.java +++ b/Source/com/drew/metadata/MetadataReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,5 +38,5 @@ public interface MetadataReader * @param reader The {@link RandomAccessReader} from which the metadata should be extracted. * @param metadata The {@link Metadata} object into which extracted values should be merged. */ - public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata); + void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata); } diff --git a/Source/com/drew/metadata/Schema.java b/Source/com/drew/metadata/Schema.java new file mode 100644 index 0000000..13cfa91 --- /dev/null +++ b/Source/com/drew/metadata/Schema.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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; + +import com.drew.lang.annotations.NotNull; + +public class Schema +{ + /** + * XMP tag namespace. TODO the older "xap", "xapBJ", "xapMM" or "xapRights" namespace prefixes should be translated to the newer "xmp", "xmpBJ", + * "xmpMM" and "xmpRights" prefixes for use in family 1 group names + */ + @NotNull + public static final String XMP_PROPERTIES = "http://ns.adobe.com/xap/1.0/"; + @NotNull + public static final String EXIF_SPECIFIC_PROPERTIES = "http://ns.adobe.com/exif/1.0/"; + @NotNull + public static final String EXIF_ADDITIONAL_PROPERTIES = "http://ns.adobe.com/exif/1.0/aux/"; + @NotNull + public static final String EXIF_TIFF_PROPERTIES = "http://ns.adobe.com/tiff/1.0/"; + @NotNull + public static final String DUBLIN_CORE_SPECIFIC_PROPERTIES = "http://purl.org/dc/elements/1.1/"; +} diff --git a/Source/com/drew/metadata/StringValue.java b/Source/com/drew/metadata/StringValue.java new file mode 100644 index 0000000..8c656e9 --- /dev/null +++ b/Source/com/drew/metadata/StringValue.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +/** + * @author Drew Noakes https://drewnoakes.com + */ +public final class StringValue +{ + @NotNull + private final byte[] _bytes; + + @Nullable + private final Charset _charset; + + public StringValue(@NotNull byte[] bytes, @Nullable Charset charset) + { + _bytes = bytes; + _charset = charset; + } + + @NotNull + public byte[] getBytes() + { + return _bytes; + } + + @Nullable + public Charset getCharset() + { + return _charset; + } + + @Override + public String toString() + { + return toString(_charset); + } + + public String toString(@Nullable Charset charset) + { + if (charset != null) { + try { + return new String(_bytes, charset.name()); + } catch (UnsupportedEncodingException ex) { + // fall through + } + } + + return new String(_bytes); + } +} diff --git a/Source/com/drew/metadata/Tag.java b/Source/com/drew/metadata/Tag.java index f8603ef..3a05523 100644 --- a/Source/com/drew/metadata/Tag.java +++ b/Source/com/drew/metadata/Tag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import com.drew.lang.annotations.Nullable; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("unused") public class Tag { private final int _tagType; @@ -53,16 +54,14 @@ public class Tag /** * Gets the tag type in hex notation as a String with padded leading - * zeroes if necessary (i.e. <code>0x100E</code>). + * zeroes if necessary (i.e. <code>0x100e</code>). * * @return the tag type as a string in hexadecimal notation */ @NotNull public String getTagTypeHex() { - String hex = Integer.toHexString(_tagType); - while (hex.length() < 4) hex = "0" + hex; - return "0x" + hex; + return String.format("0x%04x", _tagType); } /** @@ -85,7 +84,6 @@ public class Tag * * @return whether this tag has a name */ - @NotNull public boolean hasTagName() { return _directory.hasTagName(_tagType); @@ -116,7 +114,7 @@ public class Tag } /** - * A basic representation of the tag's type and value. EG: <code>[FNumber] F2.8</code>. + * A basic representation of the tag's type and value. EG: <code>[Exif IFD0] FNumber - f/2.8</code>. * * @return the tag's type and value */ diff --git a/Source/com/drew/metadata/TagDescriptor.java b/Source/com/drew/metadata/TagDescriptor.java index 50e7e46..aef6bad 100644 --- a/Source/com/drew/metadata/TagDescriptor.java +++ b/Source/com/drew/metadata/TagDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,10 @@ import com.drew.lang.annotations.Nullable; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; +import java.math.RoundingMode; +import java.nio.charset.Charset; +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -70,11 +74,18 @@ public class TagDescriptor<T extends Directory> if (object.getClass().isArray()) { final int length = Array.getLength(object); if (length > 16) { - final String componentTypeName = object.getClass().getComponentType().getName(); - return String.format("[%d %s%s]", length, componentTypeName, length == 1 ? "" : "s"); + return String.format("[%d values]", length); } } + if (object instanceof Date) + { + // Produce a date string having a format that includes the offset in form "+00:00" + return new SimpleDateFormat("EEE MMM dd HH:mm:ss Z yyyy") + .format((Date) object) + .replaceAll("([0-9]{2} [^ ]+)$", ":$1"); + } + // no special handling required, so use default conversion to a string return _directory.getString(tagType); } @@ -258,7 +269,7 @@ public class TagDescriptor<T extends Directory> } @Nullable - protected String getAsciiStringFromBytes(int tag) + protected String getStringFromBytes(int tag, Charset cs) { byte[] values = _directory.getByteArray(tag); @@ -266,9 +277,190 @@ public class TagDescriptor<T extends Directory> return null; try { - return new String(values, "ASCII").trim(); + return new String(values, cs.name()).trim(); } catch (UnsupportedEncodingException e) { return null; } } + + @Nullable + protected String getRationalOrDoubleString(int tagType) + { + Rational rational = _directory.getRational(tagType); + if (rational != null) + return rational.toSimpleString(true); + + Double d = _directory.getDoubleObject(tagType); + if (d != null) + { + DecimalFormat format = new DecimalFormat("0.###"); + return format.format(d); + } + + return null; + } + + @Nullable + protected static String getFStopDescription(double fStop) + { + DecimalFormat format = new DecimalFormat("0.0"); + format.setRoundingMode(RoundingMode.HALF_UP); + return "f/" + format.format(fStop); + } + + @Nullable + protected static String getFocalLengthDescription(double mm) + { + DecimalFormat format = new DecimalFormat("0.#"); + format.setRoundingMode(RoundingMode.HALF_UP); + return format.format(mm) + " mm"; + } + + @Nullable + protected String getLensSpecificationDescription(int tag) + { + Rational[] values = _directory.getRationalArray(tag); + + if (values == null || values.length != 4 || (values[0].isZero() && values[2].isZero())) + return null; + + StringBuilder sb = new StringBuilder(); + + if (values[0].equals(values[1])) + sb.append(values[0].toSimpleString(true)).append("mm"); + else + sb.append(values[0].toSimpleString(true)).append('-').append(values[1].toSimpleString(true)).append("mm"); + + if (!values[2].isZero()) { + sb.append(' '); + + DecimalFormat format = new DecimalFormat("0.0"); + format.setRoundingMode(RoundingMode.HALF_UP); + + if (values[2].equals(values[3])) + sb.append(getFStopDescription(values[2].doubleValue())); + else + sb.append("f/").append(format.format(values[2].doubleValue())).append('-').append(format.format(values[3].doubleValue())); + } + + return sb.toString(); + } + + @Nullable + protected String getOrientationDescription(int tag) + { + return getIndexedDescription(tag, 1, + "Top, left side (Horizontal / normal)", + "Top, right side (Mirror horizontal)", + "Bottom, right side (Rotate 180)", + "Bottom, left side (Mirror vertical)", + "Left side, top (Mirror horizontal and rotate 270 CW)", + "Right side, top (Rotate 90 CW)", + "Right side, bottom (Mirror horizontal and rotate 90 CW)", + "Left side, bottom (Rotate 270 CW)"); + } + + @Nullable + protected String getShutterSpeedDescription(int tag) + { + // I believe this method to now be stable, but am leaving some alternative snippets of + // code in here, to assist anyone who's looking into this (given that I don't have a public CVS). + +// float apexValue = _directory.getFloat(ExifSubIFDDirectory.TAG_SHUTTER_SPEED); +// int apexPower = (int)Math.pow(2.0, apexValue); +// return "1/" + apexPower + " sec"; + // TODO test this method + // thanks to Mark Edwards for spotting and patching a bug in the calculation of this + // description (spotted bug using a Canon EOS 300D) + // thanks also to Gli Blr for spotting this bug + Float apexValue = _directory.getFloatObject(tag); + if (apexValue == null) + return null; + if (apexValue <= 1) { + float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2)))); + long apexPower10 = Math.round((double)apexPower * 10.0); + float fApexPower = (float)apexPower10 / 10.0f; + DecimalFormat format = new DecimalFormat("0.##"); + format.setRoundingMode(RoundingMode.HALF_UP); + return format.format(fApexPower) + " sec"; + } else { + int apexPower = (int)((Math.exp(apexValue * Math.log(2)))); + return "1/" + apexPower + " sec"; + } + +/* + // This alternative implementation offered by Bill Richards + // TODO determine which is the correct / more-correct implementation + double apexValue = _directory.getDouble(ExifSubIFDDirectory.TAG_SHUTTER_SPEED); + double apexPower = Math.pow(2.0, apexValue); + + StringBuffer sb = new StringBuffer(); + if (apexPower > 1) + apexPower = Math.floor(apexPower); + + if (apexPower < 1) { + sb.append((int)Math.round(1/apexPower)); + } else { + sb.append("1/"); + sb.append((int)apexPower); + } + sb.append(" sec"); + return sb.toString(); +*/ + } + + // EXIF LightSource + @Nullable + protected String getLightSourceDescription(short wbtype) + { + switch (wbtype) + { + case 0: + return "Unknown"; + case 1: + return "Daylight"; + case 2: + return "Fluorescent"; + case 3: + return "Tungsten (Incandescent)"; + case 4: + return "Flash"; + case 9: + return "Fine Weather"; + case 10: + return "Cloudy"; + case 11: + return "Shade"; + case 12: + return "Daylight Fluorescent"; // (D 5700 - 7100K) + case 13: + return "Day White Fluorescent"; // (N 4600 - 5500K) + case 14: + return "Cool White Fluorescent"; // (W 3800 - 4500K) + case 15: + return "White Fluorescent"; // (WW 3250 - 3800K) + case 16: + return "Warm White Fluorescent"; // (L 2600 - 3250K) + case 17: + return "Standard Light A"; + case 18: + return "Standard Light B"; + case 19: + return "Standard Light C"; + case 20: + return "D55"; + case 21: + return "D65"; + case 22: + return "D75"; + case 23: + return "D50"; + case 24: + return "ISO Studio Tungsten"; + case 255: + return "Other"; + } + + return getDescription(wbtype); + } } diff --git a/Source/com/drew/metadata/adobe/AdobeJpegDescriptor.java b/Source/com/drew/metadata/adobe/AdobeJpegDescriptor.java index 7d2e61b..26d74fb 100644 --- a/Source/com/drew/metadata/adobe/AdobeJpegDescriptor.java +++ b/Source/com/drew/metadata/adobe/AdobeJpegDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,12 @@ package com.drew.metadata.adobe; import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; +import static com.drew.metadata.adobe.AdobeJpegDirectory.*; + /** * Provides human-readable string versions of the tags stored in an AdobeJpegDirectory. */ +@SuppressWarnings("WeakerAccess") public class AdobeJpegDescriptor extends TagDescriptor<AdobeJpegDirectory> { public AdobeJpegDescriptor(AdobeJpegDirectory directory) @@ -38,9 +41,9 @@ public class AdobeJpegDescriptor extends TagDescriptor<AdobeJpegDirectory> public String getDescription(int tagType) { switch (tagType) { - case AdobeJpegDirectory.TAG_COLOR_TRANSFORM: + case TAG_COLOR_TRANSFORM: return getColorTransformDescription(); - case AdobeJpegDirectory.TAG_DCT_ENCODE_VERSION: + case TAG_DCT_ENCODE_VERSION: return getDctEncodeVersionDescription(); default: return super.getDescription(tagType); @@ -50,7 +53,7 @@ public class AdobeJpegDescriptor extends TagDescriptor<AdobeJpegDirectory> @Nullable private String getDctEncodeVersionDescription() { - Integer value = _directory.getInteger(AdobeJpegDirectory.TAG_COLOR_TRANSFORM); + Integer value = _directory.getInteger(TAG_DCT_ENCODE_VERSION); return value == null ? null : value == 0x64 @@ -61,14 +64,9 @@ public class AdobeJpegDescriptor extends TagDescriptor<AdobeJpegDirectory> @Nullable private String getColorTransformDescription() { - Integer value = _directory.getInteger(AdobeJpegDirectory.TAG_COLOR_TRANSFORM); - if (value==null) - return null; - switch (value) { - case 0: return "Unknown (RGB or CMYK)"; - case 1: return "YCbCr"; - case 2: return "YCCK"; - default: return String.format("Unknown transform (%d)", value); - } + return getIndexedDescription(TAG_COLOR_TRANSFORM, + "Unknown (RGB or CMYK)", + "YCbCr", + "YCCK"); } } diff --git a/Source/com/drew/metadata/adobe/AdobeJpegDirectory.java b/Source/com/drew/metadata/adobe/AdobeJpegDirectory.java index 8c5492c..f775249 100644 --- a/Source/com/drew/metadata/adobe/AdobeJpegDirectory.java +++ b/Source/com/drew/metadata/adobe/AdobeJpegDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import java.util.HashMap; /** * Contains image encoding information for DCT filters, as stored by Adobe. */ +@SuppressWarnings("WeakerAccess") public class AdobeJpegDirectory extends Directory { public static final int TAG_DCT_ENCODE_VERSION = 0; diff --git a/Source/com/drew/metadata/adobe/AdobeJpegReader.java b/Source/com/drew/metadata/adobe/AdobeJpegReader.java index be40745..d5d9101 100644 --- a/Source/com/drew/metadata/adobe/AdobeJpegReader.java +++ b/Source/com/drew/metadata/adobe/AdobeJpegReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,38 +30,42 @@ import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import java.io.IOException; -import java.util.Arrays; +import java.util.Collections; /** * Decodes Adobe formatted data stored in JPEG files, normally in the APPE (App14) segment. * - * @author Philip, Drew Noakes https://drewnoakes.com + * @author Philip + * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class AdobeJpegReader implements JpegSegmentMetadataReader { + public static final String PREAMBLE = "Adobe"; + @NotNull public Iterable<JpegSegmentType> getSegmentTypes() { - return Arrays.asList(JpegSegmentType.APPE); - } - - public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType) - { - return segmentBytes.length == 12 && "Adobe".equalsIgnoreCase(new String(segmentBytes, 0, 5)); + return Collections.singletonList(JpegSegmentType.APPE); } - public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) { - extract(new SequentialByteArrayReader(segmentBytes), metadata); + for (byte[] bytes : segments) { + if (bytes.length == 12 && PREAMBLE.equalsIgnoreCase(new String(bytes, 0, PREAMBLE.length()))) + extract(new SequentialByteArrayReader(bytes), metadata); + } } public void extract(@NotNull SequentialReader reader, @NotNull Metadata metadata) { - final Directory directory = metadata.getOrCreateDirectory(AdobeJpegDirectory.class); + Directory directory = new AdobeJpegDirectory(); + metadata.addDirectory(directory); + try { reader.setMotorolaByteOrder(false); - if (!reader.getString(5).equals("Adobe")) { + if (!reader.getString(PREAMBLE.length()).equals(PREAMBLE)) { directory.addError("Invalid Adobe JPEG data header."); return; } diff --git a/Source/com/drew/metadata/adobe/package-info.java b/Source/com/drew/metadata/adobe/package-info.java new file mode 100644 index 0000000..ea34838 --- /dev/null +++ b/Source/com/drew/metadata/adobe/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of Adobe metadata. + */ +package com.drew.metadata.adobe; diff --git a/Source/com/drew/metadata/adobe/package.html b/Source/com/drew/metadata/adobe/package.html deleted file mode 100644 index 175c98b..0000000 --- a/Source/com/drew/metadata/adobe/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of Adobe metadata. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/metadata/bmp/BmpHeaderDescriptor.java b/Source/com/drew/metadata/bmp/BmpHeaderDescriptor.java index 4ea0ee1..b01ce7d 100644 --- a/Source/com/drew/metadata/bmp/BmpHeaderDescriptor.java +++ b/Source/com/drew/metadata/bmp/BmpHeaderDescriptor.java @@ -1,12 +1,35 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.bmp; import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; +import static com.drew.metadata.bmp.BmpHeaderDirectory.*; + /** * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class BmpHeaderDescriptor extends TagDescriptor<BmpHeaderDirectory> { public BmpHeaderDescriptor(@NotNull BmpHeaderDirectory directory) @@ -18,7 +41,7 @@ public class BmpHeaderDescriptor extends TagDescriptor<BmpHeaderDirectory> public String getDescription(int tagType) { switch (tagType) { - case BmpHeaderDirectory.TAG_COMPRESSION: + case TAG_COMPRESSION: return getCompressionDescription(); default: return super.getDescription(tagType); @@ -36,10 +59,10 @@ public class BmpHeaderDescriptor extends TagDescriptor<BmpHeaderDirectory> // 5 = PNG // 6 = Bit field try { - Integer value = _directory.getInteger(BmpHeaderDirectory.TAG_COMPRESSION); + Integer value = _directory.getInteger(TAG_COMPRESSION); if (value == null) return null; - Integer headerSize = _directory.getInteger(BmpHeaderDirectory.TAG_HEADER_SIZE); + Integer headerSize = _directory.getInteger(TAG_HEADER_SIZE); if (headerSize == null) return null; @@ -52,7 +75,7 @@ public class BmpHeaderDescriptor extends TagDescriptor<BmpHeaderDirectory> case 5: return "PNG"; case 6: return "Bit field"; default: - return super.getDescription(BmpHeaderDirectory.TAG_COMPRESSION); + return super.getDescription(TAG_COMPRESSION); } } catch (Exception e) { return null; diff --git a/Source/com/drew/metadata/bmp/BmpHeaderDirectory.java b/Source/com/drew/metadata/bmp/BmpHeaderDirectory.java index 7124d51..e85c5c1 100644 --- a/Source/com/drew/metadata/bmp/BmpHeaderDirectory.java +++ b/Source/com/drew/metadata/bmp/BmpHeaderDirectory.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.bmp; import com.drew.lang.annotations.NotNull; @@ -8,6 +28,7 @@ import java.util.HashMap; /** * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class BmpHeaderDirectory extends Directory { public static final int TAG_HEADER_SIZE = -1; diff --git a/Source/com/drew/metadata/bmp/BmpReader.java b/Source/com/drew/metadata/bmp/BmpReader.java index a3f4f20..798b5fa 100644 --- a/Source/com/drew/metadata/bmp/BmpReader.java +++ b/Source/com/drew/metadata/bmp/BmpReader.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.bmp; import com.drew.lang.SequentialReader; @@ -13,7 +33,8 @@ public class BmpReader { public void extract(@NotNull final SequentialReader reader, final @NotNull Metadata metadata) { - final BmpHeaderDirectory directory = metadata.getOrCreateDirectory(BmpHeaderDirectory.class); + BmpHeaderDirectory directory = new BmpHeaderDirectory(); + metadata.addDirectory(directory); // FILE HEADER // diff --git a/Source/com/drew/metadata/bmp/package-info.java b/Source/com/drew/metadata/bmp/package-info.java new file mode 100644 index 0000000..ce10653 --- /dev/null +++ b/Source/com/drew/metadata/bmp/package-info.java @@ -0,0 +1,6 @@ +/** + * Contains classes for the extraction and modelling of BMP file metadata. + * + * @since 2.7.0 + */ +package com.drew.metadata.bmp; diff --git a/Source/com/drew/metadata/bmp/package.html b/Source/com/drew/metadata/bmp/package.html deleted file mode 100644 index 15dbdc5..0000000 --- a/Source/com/drew/metadata/bmp/package.html +++ /dev/null @@ -1,34 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of BMP file metadata. - -<!-- Put @see and @since tags down here. --> -@since 2.7.0 - -</body> -</html> diff --git a/Source/com/drew/metadata/exif/ExifDescriptorBase.java b/Source/com/drew/metadata/exif/ExifDescriptorBase.java new file mode 100644 index 0000000..551d0f1 --- /dev/null +++ b/Source/com/drew/metadata/exif/ExifDescriptorBase.java @@ -0,0 +1,1234 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.imaging.PhotographicConversions; +import com.drew.lang.Rational; +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.lang.ByteArrayReader; +import com.drew.metadata.Directory; +import com.drew.metadata.TagDescriptor; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.Map; + +import static com.drew.metadata.exif.ExifDirectoryBase.*; + +/** + * Base class for several Exif format descriptor classes. + * + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public abstract class ExifDescriptorBase<T extends Directory> extends TagDescriptor<T> +{ + /** + * Dictates whether rational values will be represented in decimal format in instances + * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3). + */ + private final boolean _allowDecimalRepresentationOfRationals = true; + + // Note for the potential addition of brightness presentation in eV: + // Brightness of taken subject. To calculate Exposure(Ev) from BrightnessValue(Bv), + // you must add SensitivityValue(Sv). + // Ev=BV+Sv Sv=log2(ISOSpeedRating/3.125) + // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32. + + public ExifDescriptorBase(@NotNull T directory) + { + super(directory); + } + + @Nullable + @Override + public String getDescription(int tagType) + { + // TODO order case blocks and corresponding methods in the same order as the TAG_* values are defined + + switch (tagType) { + case TAG_INTEROP_INDEX: + return getInteropIndexDescription(); + case TAG_INTEROP_VERSION: + return getInteropVersionDescription(); + case TAG_ORIENTATION: + return getOrientationDescription(); + case TAG_RESOLUTION_UNIT: + return getResolutionDescription(); + case TAG_YCBCR_POSITIONING: + return getYCbCrPositioningDescription(); + case TAG_X_RESOLUTION: + return getXResolutionDescription(); + case TAG_Y_RESOLUTION: + return getYResolutionDescription(); + case TAG_IMAGE_WIDTH: + return getImageWidthDescription(); + case TAG_IMAGE_HEIGHT: + return getImageHeightDescription(); + case TAG_BITS_PER_SAMPLE: + return getBitsPerSampleDescription(); + case TAG_PHOTOMETRIC_INTERPRETATION: + return getPhotometricInterpretationDescription(); + case TAG_ROWS_PER_STRIP: + return getRowsPerStripDescription(); + case TAG_STRIP_BYTE_COUNTS: + return getStripByteCountsDescription(); + case TAG_SAMPLES_PER_PIXEL: + return getSamplesPerPixelDescription(); + case TAG_PLANAR_CONFIGURATION: + return getPlanarConfigurationDescription(); + case TAG_YCBCR_SUBSAMPLING: + return getYCbCrSubsamplingDescription(); + case TAG_REFERENCE_BLACK_WHITE: + return getReferenceBlackWhiteDescription(); + case TAG_WIN_AUTHOR: + return getWindowsAuthorDescription(); + case TAG_WIN_COMMENT: + return getWindowsCommentDescription(); + case TAG_WIN_KEYWORDS: + return getWindowsKeywordsDescription(); + case TAG_WIN_SUBJECT: + return getWindowsSubjectDescription(); + case TAG_WIN_TITLE: + return getWindowsTitleDescription(); + case TAG_NEW_SUBFILE_TYPE: + return getNewSubfileTypeDescription(); + case TAG_SUBFILE_TYPE: + return getSubfileTypeDescription(); + case TAG_THRESHOLDING: + return getThresholdingDescription(); + case TAG_FILL_ORDER: + return getFillOrderDescription(); + case TAG_CFA_PATTERN_2: + return getCfaPattern2Description(); + case TAG_EXPOSURE_TIME: + return getExposureTimeDescription(); + case TAG_SHUTTER_SPEED: + return getShutterSpeedDescription(); + case TAG_FNUMBER: + return getFNumberDescription(); + case TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL: + return getCompressedAverageBitsPerPixelDescription(); + case TAG_SUBJECT_DISTANCE: + return getSubjectDistanceDescription(); + case TAG_METERING_MODE: + return getMeteringModeDescription(); + case TAG_WHITE_BALANCE: + return getWhiteBalanceDescription(); + case TAG_FLASH: + return getFlashDescription(); + case TAG_FOCAL_LENGTH: + return getFocalLengthDescription(); + case TAG_COLOR_SPACE: + return getColorSpaceDescription(); + case TAG_EXIF_IMAGE_WIDTH: + return getExifImageWidthDescription(); + case TAG_EXIF_IMAGE_HEIGHT: + return getExifImageHeightDescription(); + case TAG_FOCAL_PLANE_RESOLUTION_UNIT: + return getFocalPlaneResolutionUnitDescription(); + case TAG_FOCAL_PLANE_X_RESOLUTION: + return getFocalPlaneXResolutionDescription(); + case TAG_FOCAL_PLANE_Y_RESOLUTION: + return getFocalPlaneYResolutionDescription(); + case TAG_EXPOSURE_PROGRAM: + return getExposureProgramDescription(); + case TAG_APERTURE: + return getApertureValueDescription(); + case TAG_MAX_APERTURE: + return getMaxApertureValueDescription(); + case TAG_SENSING_METHOD: + return getSensingMethodDescription(); + case TAG_EXPOSURE_BIAS: + return getExposureBiasDescription(); + case TAG_FILE_SOURCE: + return getFileSourceDescription(); + case TAG_SCENE_TYPE: + return getSceneTypeDescription(); + case TAG_CFA_PATTERN: + return getCfaPatternDescription(); + case TAG_COMPONENTS_CONFIGURATION: + return getComponentConfigurationDescription(); + case TAG_EXIF_VERSION: + return getExifVersionDescription(); + case TAG_FLASHPIX_VERSION: + return getFlashPixVersionDescription(); + case TAG_ISO_EQUIVALENT: + return getIsoEquivalentDescription(); + case TAG_USER_COMMENT: + return getUserCommentDescription(); + case TAG_CUSTOM_RENDERED: + return getCustomRenderedDescription(); + case TAG_EXPOSURE_MODE: + return getExposureModeDescription(); + case TAG_WHITE_BALANCE_MODE: + return getWhiteBalanceModeDescription(); + case TAG_DIGITAL_ZOOM_RATIO: + return getDigitalZoomRatioDescription(); + case TAG_35MM_FILM_EQUIV_FOCAL_LENGTH: + return get35mmFilmEquivFocalLengthDescription(); + case TAG_SCENE_CAPTURE_TYPE: + return getSceneCaptureTypeDescription(); + case TAG_GAIN_CONTROL: + return getGainControlDescription(); + case TAG_CONTRAST: + return getContrastDescription(); + case TAG_SATURATION: + return getSaturationDescription(); + case TAG_SHARPNESS: + return getSharpnessDescription(); + case TAG_SUBJECT_DISTANCE_RANGE: + return getSubjectDistanceRangeDescription(); + case TAG_SENSITIVITY_TYPE: + return getSensitivityTypeRangeDescription(); + case TAG_COMPRESSION: + return getCompressionDescription(); + case TAG_JPEG_PROC: + return getJpegProcDescription(); + case TAG_LENS_SPECIFICATION: + return getLensSpecificationDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getInteropVersionDescription() + { + return getVersionBytesDescription(TAG_INTEROP_VERSION, 2); + } + + @Nullable + public String getInteropIndexDescription() + { + String value = _directory.getString(TAG_INTEROP_INDEX); + + if (value == null) + return null; + + return "R98".equalsIgnoreCase(value.trim()) + ? "Recommended Exif Interoperability Rules (ExifR98)" + : "Unknown (" + value + ")"; + } + + @Nullable + public String getReferenceBlackWhiteDescription() + { + // For some reason, sometimes this is read as a long[] and + // getIntArray isn't able to deal with it + int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE); + if (ints==null || ints.length < 6) + { + Object o = _directory.getObject(TAG_REFERENCE_BLACK_WHITE); + if (o != null && (o instanceof long[])) + { + long[] longs = (long[])o; + if (longs.length < 6) + return null; + + ints = new int[longs.length]; + for (int i = 0; i < longs.length; i++) + ints[i] = (int)longs[i]; + } + else + return null; + } + + int blackR = ints[0]; + int whiteR = ints[1]; + int blackG = ints[2]; + int whiteG = ints[3]; + int blackB = ints[4]; + int whiteB = ints[5]; + return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB); + } + + @Nullable + public String getYResolutionDescription() + { + Rational value = _directory.getRational(TAG_Y_RESOLUTION); + if (value==null) + return null; + final String unit = getResolutionDescription(); + return String.format("%s dots per %s", + value.toSimpleString(_allowDecimalRepresentationOfRationals), + unit == null ? "unit" : unit.toLowerCase()); + } + + @Nullable + public String getXResolutionDescription() + { + Rational value = _directory.getRational(TAG_X_RESOLUTION); + if (value == null) + return null; + final String unit = getResolutionDescription(); + return String.format("%s dots per %s", + value.toSimpleString(_allowDecimalRepresentationOfRationals), + unit == null ? "unit" : unit.toLowerCase()); + } + + @Nullable + public String getYCbCrPositioningDescription() + { + return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point"); + } + + @Nullable + public String getOrientationDescription() + { + return super.getOrientationDescription(TAG_ORIENTATION); + } + + @Nullable + public String getResolutionDescription() + { + // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch) + return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm"); + } + + /** The Windows specific tags uses plain Unicode. */ + @Nullable + private String getUnicodeDescription(int tag) + { + byte[] bytes = _directory.getByteArray(tag); + if (bytes == null) + return null; + try { + // Decode the unicode string and trim the unicode zero "\0" from the end. + return new String(bytes, "UTF-16LE").trim(); + } catch (UnsupportedEncodingException ex) { + return null; + } + } + + @Nullable + public String getWindowsAuthorDescription() + { + return getUnicodeDescription(TAG_WIN_AUTHOR); + } + + @Nullable + public String getWindowsCommentDescription() + { + return getUnicodeDescription(TAG_WIN_COMMENT); + } + + @Nullable + public String getWindowsKeywordsDescription() + { + return getUnicodeDescription(TAG_WIN_KEYWORDS); + } + + @Nullable + public String getWindowsTitleDescription() + { + return getUnicodeDescription(TAG_WIN_TITLE); + } + + @Nullable + public String getWindowsSubjectDescription() + { + return getUnicodeDescription(TAG_WIN_SUBJECT); + } + + @Nullable + public String getYCbCrSubsamplingDescription() + { + int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING); + if (positions == null || positions.length < 2) + return null; + if (positions[0] == 2 && positions[1] == 1) { + return "YCbCr4:2:2"; + } else if (positions[0] == 2 && positions[1] == 2) { + return "YCbCr4:2:0"; + } else { + return "(Unknown)"; + } + } + + @Nullable + public String getPlanarConfigurationDescription() + { + // When image format is no compression YCbCr, this value shows byte aligns of YCbCr + // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling + // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr + // plane format. + return getIndexedDescription(TAG_PLANAR_CONFIGURATION, + 1, + "Chunky (contiguous for each subsampling pixel)", + "Separate (Y-plane/Cb-plane/Cr-plane format)" + ); + } + + @Nullable + public String getSamplesPerPixelDescription() + { + String value = _directory.getString(TAG_SAMPLES_PER_PIXEL); + return value == null ? null : value + " samples/pixel"; + } + + @Nullable + public String getRowsPerStripDescription() + { + final String value = _directory.getString(TAG_ROWS_PER_STRIP); + return value == null ? null : value + " rows/strip"; + } + + @Nullable + public String getStripByteCountsDescription() + { + final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS); + return value == null ? null : value + " bytes"; + } + + @Nullable + public String getPhotometricInterpretationDescription() + { + // Shows the color space of the image data components + Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION); + if (value == null) + return null; + switch (value) { + case 0: return "WhiteIsZero"; + case 1: return "BlackIsZero"; + case 2: return "RGB"; + case 3: return "RGB Palette"; + case 4: return "Transparency Mask"; + case 5: return "CMYK"; + case 6: return "YCbCr"; + case 8: return "CIELab"; + case 9: return "ICCLab"; + case 10: return "ITULab"; + case 32803: return "Color Filter Array"; + case 32844: return "Pixar LogL"; + case 32845: return "Pixar LogLuv"; + case 32892: return "Linear Raw"; + default: + return "Unknown colour space"; + } + } + + @Nullable + public String getBitsPerSampleDescription() + { + String value = _directory.getString(TAG_BITS_PER_SAMPLE); + return value == null ? null : value + " bits/component/pixel"; + } + + @Nullable + public String getImageWidthDescription() + { + String value = _directory.getString(TAG_IMAGE_WIDTH); + return value == null ? null : value + " pixels"; + } + + @Nullable + public String getImageHeightDescription() + { + String value = _directory.getString(TAG_IMAGE_HEIGHT); + return value == null ? null : value + " pixels"; + } + + @Nullable + public String getNewSubfileTypeDescription() + { + return getIndexedDescription(TAG_NEW_SUBFILE_TYPE, 0, + "Full-resolution image", + "Reduced-resolution image", + "Single page of multi-page image", + "Single page of multi-page reduced-resolution image", + "Transparency mask", + "Transparency mask of reduced-resolution image", + "Transparency mask of multi-page image", + "Transparency mask of reduced-resolution multi-page image" + ); + } + + @Nullable + public String getSubfileTypeDescription() + { + return getIndexedDescription(TAG_SUBFILE_TYPE, 1, + "Full-resolution image", + "Reduced-resolution image", + "Single page of multi-page image" + ); + } + + @Nullable + public String getThresholdingDescription() + { + return getIndexedDescription(TAG_THRESHOLDING, 1, + "No dithering or halftoning", + "Ordered dither or halftone", + "Randomized dither" + ); + } + + @Nullable + public String getFillOrderDescription() + { + return getIndexedDescription(TAG_FILL_ORDER, 1, + "Normal", + "Reversed" + ); + } + + @Nullable + public String getSubjectDistanceRangeDescription() + { + return getIndexedDescription(TAG_SUBJECT_DISTANCE_RANGE, + "Unknown", + "Macro", + "Close view", + "Distant view" + ); + } + + @Nullable + public String getSensitivityTypeRangeDescription() + { + return getIndexedDescription(TAG_SENSITIVITY_TYPE, + "Unknown", + "Standard Output Sensitivity", + "Recommended Exposure Index", + "ISO Speed", + "Standard Output Sensitivity and Recommended Exposure Index", + "Standard Output Sensitivity and ISO Speed", + "Recommended Exposure Index and ISO Speed", + "Standard Output Sensitivity, Recommended Exposure Index and ISO Speed" + ); + } + + @Nullable + public String getLensSpecificationDescription() + { + return getLensSpecificationDescription(TAG_LENS_SPECIFICATION); + } + + @Nullable + public String getSharpnessDescription() + { + return getIndexedDescription(TAG_SHARPNESS, + "None", + "Low", + "Hard" + ); + } + + @Nullable + public String getSaturationDescription() + { + return getIndexedDescription(TAG_SATURATION, + "None", + "Low saturation", + "High saturation" + ); + } + + @Nullable + public String getContrastDescription() + { + return getIndexedDescription(TAG_CONTRAST, + "None", + "Soft", + "Hard" + ); + } + + @Nullable + public String getGainControlDescription() + { + return getIndexedDescription(TAG_GAIN_CONTROL, + "None", + "Low gain up", + "Low gain down", + "High gain up", + "High gain down" + ); + } + + @Nullable + public String getSceneCaptureTypeDescription() + { + return getIndexedDescription(TAG_SCENE_CAPTURE_TYPE, + "Standard", + "Landscape", + "Portrait", + "Night scene" + ); + } + + @Nullable + public String get35mmFilmEquivFocalLengthDescription() + { + Integer value = _directory.getInteger(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH); + return value == null + ? null + : value == 0 + ? "Unknown" + : getFocalLengthDescription(value); + } + + @Nullable + public String getDigitalZoomRatioDescription() + { + Rational value = _directory.getRational(TAG_DIGITAL_ZOOM_RATIO); + return value == null + ? null + : value.getNumerator() == 0 + ? "Digital zoom not used" + : new DecimalFormat("0.#").format(value.doubleValue()); + } + + @Nullable + public String getWhiteBalanceModeDescription() + { + return getIndexedDescription(TAG_WHITE_BALANCE_MODE, + "Auto white balance", + "Manual white balance" + ); + } + + @Nullable + public String getExposureModeDescription() + { + return getIndexedDescription(TAG_EXPOSURE_MODE, + "Auto exposure", + "Manual exposure", + "Auto bracket" + ); + } + + @Nullable + public String getCustomRenderedDescription() + { + return getIndexedDescription(TAG_CUSTOM_RENDERED, + "Normal process", + "Custom process" + ); + } + + @Nullable + public String getUserCommentDescription() + { + byte[] commentBytes = _directory.getByteArray(TAG_USER_COMMENT); + if (commentBytes == null) + return null; + if (commentBytes.length == 0) + return ""; + + final Map<String, String> encodingMap = new HashMap<String, String>(); + encodingMap.put("ASCII", System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1". + encodingMap.put("UNICODE", "UTF-16LE"); + encodingMap.put("JIS", "Shift-JIS"); // We assume this charset for now. Another suggestion is "JIS". + + try { + if (commentBytes.length >= 10) { + String firstTenBytesString = new String(commentBytes, 0, 10); + + // try each encoding name + for (Map.Entry<String, String> pair : encodingMap.entrySet()) { + String encodingName = pair.getKey(); + String charset = pair.getValue(); + if (firstTenBytesString.startsWith(encodingName)) { + // skip any null or blank characters commonly present after the encoding name, up to a limit of 10 from the start + for (int j = encodingName.length(); j < 10; j++) { + byte b = commentBytes[j]; + if (b != '\0' && b != ' ') + return new String(commentBytes, j, commentBytes.length - j, charset).trim(); + } + return new String(commentBytes, 10, commentBytes.length - 10, charset).trim(); + } + } + } + // special handling fell through, return a plain string representation + return new String(commentBytes, System.getProperty("file.encoding")).trim(); + } catch (UnsupportedEncodingException ex) { + return null; + } + } + + @Nullable + public String getIsoEquivalentDescription() + { + // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values + Integer isoEquiv = _directory.getInteger(TAG_ISO_EQUIVALENT); + // There used to be a check here that multiplied ISO values < 50 by 200. + // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40. + return isoEquiv != null + ? Integer.toString(isoEquiv) + : null; + } + + @Nullable + public String getExifVersionDescription() + { + return getVersionBytesDescription(TAG_EXIF_VERSION, 2); + } + + @Nullable + public String getFlashPixVersionDescription() + { + return getVersionBytesDescription(TAG_FLASHPIX_VERSION, 2); + } + + @Nullable + public String getSceneTypeDescription() + { + return getIndexedDescription(TAG_SCENE_TYPE, + 1, + "Directly photographed image" + ); + } + + /// <summary> + /// String description of CFA Pattern + /// </summary> + /// <remarks> + /// Converted from Exiftool version 10.33 created by Phil Harvey + /// http://www.sno.phy.queensu.ca/~phil/exiftool/ + /// lib\Image\ExifTool\Exif.pm + /// + /// Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used. + /// It does not apply to all sensing methods. + /// </remarks> + @Nullable + public String getCfaPatternDescription() + { + return formatCFAPattern(decodeCfaPattern(TAG_CFA_PATTERN)); + } + + /// <summary> + /// String description of CFA Pattern + /// </summary> + /// <remarks> + /// Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used. + /// It does not apply to all sensing methods. + /// + /// ExifDirectoryBase.TAG_CFA_PATTERN_2 holds only the pixel pattern. ExifDirectoryBase.TAG_CFA_REPEAT_PATTERN_DIM is expected to exist and pass + /// some conditional tests. + /// </remarks> + @Nullable + public String getCfaPattern2Description() + { + byte[] values = _directory.getByteArray(TAG_CFA_PATTERN_2); + if (values == null) + return null; + + int[] repeatPattern = _directory.getIntArray(TAG_CFA_REPEAT_PATTERN_DIM); + if (repeatPattern == null) + return String.format("Repeat Pattern not found for CFAPattern (%s)", super.getDescription(TAG_CFA_PATTERN_2)); + + if (repeatPattern.length == 2 && values.length == (repeatPattern[0] * repeatPattern[1])) + { + int[] intpattern = new int[2 + values.length]; + intpattern[0] = repeatPattern[0]; + intpattern[1] = repeatPattern[1]; + + for (int i = 0; i < values.length; i++) + intpattern[i + 2] = values[i] & 0xFF; // convert the values[i] byte to unsigned + + return formatCFAPattern(intpattern); + } + + return String.format("Unknown Pattern (%s)", super.getDescription(TAG_CFA_PATTERN_2)); + } + + @Nullable + private static String formatCFAPattern(@Nullable int[] pattern) + { + if (pattern == null) + return null; + if (pattern.length < 2) + return "<truncated data>"; + if (pattern[0] == 0 && pattern[1] == 0) + return "<zero pattern size>"; + + int end = 2 + pattern[0] * pattern[1]; + if (end > pattern.length) + return "<invalid pattern size>"; + + String[] cfaColors = { "Red", "Green", "Blue", "Cyan", "Magenta", "Yellow", "White" }; + + StringBuilder ret = new StringBuilder(); + ret.append("["); + for (int pos = 2; pos < end; pos++) + { + if (pattern[pos] <= cfaColors.length - 1) + ret.append(cfaColors[pattern[pos]]); + else + ret.append("Unknown"); // indicated pattern position is outside the array bounds + + if ((pos - 2) % pattern[1] == 0) + ret.append(","); + else if(pos != end - 1) + ret.append("]["); + } + ret.append("]"); + + return ret.toString(); + } + + /// <summary> + /// Decode raw CFAPattern value + /// </summary> + /// <remarks> + /// Converted from Exiftool version 10.33 created by Phil Harvey + /// http://www.sno.phy.queensu.ca/~phil/exiftool/ + /// lib\Image\ExifTool\Exif.pm + /// + /// The value consists of: + /// - Two short, being the grid width and height of the repeated pattern. + /// - Next, for every pixel in that pattern, an identification code. + /// </remarks> + @Nullable + private int[] decodeCfaPattern(int tagType) + { + int[] ret; + + byte[] values = _directory.getByteArray(tagType); + if (values == null) + return null; + + if (values.length < 4) + { + ret = new int[values.length]; + for (int i = 0; i < values.length; i++) + ret[i] = values[i]; + return ret; + } + + ret = new int[values.length - 2]; + + try { + ByteArrayReader reader = new ByteArrayReader(values); + + // first two values should be read as 16-bits (2 bytes) + short item0 = reader.getInt16(0); + short item1 = reader.getInt16(2); + + Boolean copyArray = false; + int end = 2 + item0 * item1; + if (end > values.length) // sanity check in case of byte order problems; calculated 'end' should be <= length of the values + { + // try swapping byte order (I have seen this order different than in EXIF) + reader.setMotorolaByteOrder(!reader.isMotorolaByteOrder()); + item0 = reader.getInt16(0); + item1 = reader.getInt16(2); + + if (values.length >= (2 + item0 * item1)) + copyArray = true; + } + else + copyArray = true; + + if(copyArray) + { + ret[0] = item0; + ret[1] = item1; + + for (int i = 4; i < values.length; i++) + ret[i - 2] = reader.getInt8(i); + } + } catch (IOException ex) { + _directory.addError("IO exception processing data: " + ex.getMessage()); + } + + return ret; + } + + @Nullable + public String getFileSourceDescription() + { + return getIndexedDescription(TAG_FILE_SOURCE, + 1, + "Film Scanner", + "Reflection Print Scanner", + "Digital Still Camera (DSC)" + ); + } + + @Nullable + public String getExposureBiasDescription() + { + Rational value = _directory.getRational(TAG_EXPOSURE_BIAS); + if (value == null) + return null; + return value.toSimpleString(true) + " EV"; + } + + @Nullable + public String getMaxApertureValueDescription() + { + Double aperture = _directory.getDoubleObject(TAG_MAX_APERTURE); + if (aperture == null) + return null; + double fStop = PhotographicConversions.apertureToFStop(aperture); + return getFStopDescription(fStop); + } + + @Nullable + public String getApertureValueDescription() + { + Double aperture = _directory.getDoubleObject(TAG_APERTURE); + if (aperture == null) + return null; + double fStop = PhotographicConversions.apertureToFStop(aperture); + return getFStopDescription(fStop); + } + + @Nullable + public String getExposureProgramDescription() + { + return getIndexedDescription(TAG_EXPOSURE_PROGRAM, + 1, + "Manual control", + "Program normal", + "Aperture priority", + "Shutter priority", + "Program creative (slow program)", + "Program action (high-speed program)", + "Portrait mode", + "Landscape mode" + ); + } + + + @Nullable + public String getFocalPlaneXResolutionDescription() + { + Rational rational = _directory.getRational(TAG_FOCAL_PLANE_X_RESOLUTION); + if (rational == null) + return null; + final String unit = getFocalPlaneResolutionUnitDescription(); + return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) + + (unit == null ? "" : " " + unit.toLowerCase()); + } + + @Nullable + public String getFocalPlaneYResolutionDescription() + { + Rational rational = _directory.getRational(TAG_FOCAL_PLANE_Y_RESOLUTION); + if (rational == null) + return null; + final String unit = getFocalPlaneResolutionUnitDescription(); + return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) + + (unit == null ? "" : " " + unit.toLowerCase()); + } + + @Nullable + public String getFocalPlaneResolutionUnitDescription() + { + // Unit of FocalPlaneXResolution/FocalPlaneYResolution. + // '1' means no-unit, '2' inch, '3' centimeter. + return getIndexedDescription(TAG_FOCAL_PLANE_RESOLUTION_UNIT, + 1, + "(No unit)", + "Inches", + "cm" + ); + } + + @Nullable + public String getExifImageWidthDescription() + { + final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_WIDTH); + return value == null ? null : value + " pixels"; + } + + @Nullable + public String getExifImageHeightDescription() + { + final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_HEIGHT); + return value == null ? null : value + " pixels"; + } + + @Nullable + public String getColorSpaceDescription() + { + final Integer value = _directory.getInteger(TAG_COLOR_SPACE); + if (value == null) + return null; + if (value == 1) + return "sRGB"; + if (value == 65535) + return "Undefined"; + return "Unknown (" + value + ")"; + } + + @Nullable + public String getFocalLengthDescription() + { + Rational value = _directory.getRational(TAG_FOCAL_LENGTH); + return value == null ? null : getFocalLengthDescription(value.doubleValue()); + } + + @Nullable + public String getFlashDescription() + { + /* + * This is a bit mask. + * 0 = flash fired + * 1 = return detected + * 2 = return able to be detected + * 3 = unknown + * 4 = auto used + * 5 = unknown + * 6 = red eye reduction used + */ + + final Integer value = _directory.getInteger(TAG_FLASH); + + if (value == null) + return null; + + StringBuilder sb = new StringBuilder(); + + if ((value & 0x1) != 0) + sb.append("Flash fired"); + else + sb.append("Flash did not fire"); + + // check if we're able to detect a return, before we mention it + if ((value & 0x4) != 0) { + if ((value & 0x2) != 0) + sb.append(", return detected"); + else + sb.append(", return not detected"); + } + + if ((value & 0x10) != 0) + sb.append(", auto"); + + if ((value & 0x40) != 0) + sb.append(", red-eye reduction"); + + return sb.toString(); + } + + @Nullable + public String getWhiteBalanceDescription() + { + // See http://web.archive.org/web/20131018091152/http://exif.org/Exif2-2.PDF page 35 + final Integer value = _directory.getInteger(TAG_WHITE_BALANCE); + if (value == null) + return null; + switch (value) { + case 0: return "Unknown"; + case 1: return "Daylight"; + case 2: return "Florescent"; + case 3: return "Tungsten"; + case 4: return "Flash"; + case 9: return "Fine Weather"; + case 10: return "Cloudy"; + case 11: return "Shade"; + case 12: return "Daylight Fluorescent"; + case 13: return "Day White Fluorescent"; + case 14: return "Cool White Fluorescent"; + case 15: return "White Fluorescent"; + case 16: return "Warm White Fluorescent"; + case 17: return "Standard light"; + case 18: return "Standard light (B)"; + case 19: return "Standard light (C)"; + case 20: return "D55"; + case 21: return "D65"; + case 22: return "D75"; + case 23: return "D50"; + case 24: return "Studio Tungsten"; + case 255: return "(Other)"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getMeteringModeDescription() + { + // '0' means unknown, '1' average, '2' center weighted average, '3' spot + // '4' multi-spot, '5' multi-segment, '6' partial, '255' other + Integer value = _directory.getInteger(TAG_METERING_MODE); + if (value == null) + return null; + switch (value) { + case 0: return "Unknown"; + case 1: return "Average"; + case 2: return "Center weighted average"; + case 3: return "Spot"; + case 4: return "Multi-spot"; + case 5: return "Multi-segment"; + case 6: return "Partial"; + case 255: return "(Other)"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getCompressionDescription() + { + Integer value = _directory.getInteger(TAG_COMPRESSION); + if (value == null) + return null; + switch (value) { + case 1: return "Uncompressed"; + case 2: return "CCITT 1D"; + case 3: return "T4/Group 3 Fax"; + case 4: return "T6/Group 4 Fax"; + case 5: return "LZW"; + case 6: return "JPEG (old-style)"; + case 7: return "JPEG"; + case 8: return "Adobe Deflate"; + case 9: return "JBIG B&W"; + case 10: return "JBIG Color"; + case 99: return "JPEG"; + case 262: return "Kodak 262"; + case 32766: return "Next"; + case 32767: return "Sony ARW Compressed"; + case 32769: return "Packed RAW"; + case 32770: return "Samsung SRW Compressed"; + case 32771: return "CCIRLEW"; + case 32772: return "Samsung SRW Compressed 2"; + case 32773: return "PackBits"; + case 32809: return "Thunderscan"; + case 32867: return "Kodak KDC Compressed"; + case 32895: return "IT8CTPAD"; + case 32896: return "IT8LW"; + case 32897: return "IT8MP"; + case 32898: return "IT8BL"; + case 32908: return "PixarFilm"; + case 32909: return "PixarLog"; + case 32946: return "Deflate"; + case 32947: return "DCS"; + case 34661: return "JBIG"; + case 34676: return "SGILog"; + case 34677: return "SGILog24"; + case 34712: return "JPEG 2000"; + case 34713: return "Nikon NEF Compressed"; + case 34715: return "JBIG2 TIFF FX"; + case 34718: return "Microsoft Document Imaging (MDI) Binary Level Codec"; + case 34719: return "Microsoft Document Imaging (MDI) Progressive Transform Codec"; + case 34720: return "Microsoft Document Imaging (MDI) Vector"; + case 34892: return "Lossy JPEG"; + case 65000: return "Kodak DCR Compressed"; + case 65535: return "Pentax PEF Compressed"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getSubjectDistanceDescription() + { + Rational value = _directory.getRational(TAG_SUBJECT_DISTANCE); + if (value == null) + return null; + DecimalFormat formatter = new DecimalFormat("0.0##"); + return formatter.format(value.doubleValue()) + " metres"; + } + + @Nullable + public String getCompressedAverageBitsPerPixelDescription() + { + Rational value = _directory.getRational(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL); + if (value == null) + return null; + String ratio = value.toSimpleString(_allowDecimalRepresentationOfRationals); + return value.isInteger() && value.intValue() == 1 + ? ratio + " bit/pixel" + : ratio + " bits/pixel"; + } + + @Nullable + public String getExposureTimeDescription() + { + String value = _directory.getString(TAG_EXPOSURE_TIME); + return value == null ? null : value + " sec"; + } + + @Nullable + public String getShutterSpeedDescription() + { + return super.getShutterSpeedDescription(TAG_SHUTTER_SPEED); + } + + @Nullable + public String getFNumberDescription() + { + Rational value = _directory.getRational(TAG_FNUMBER); + if (value == null) + return null; + return getFStopDescription(value.doubleValue()); + } + + @Nullable + public String getSensingMethodDescription() + { + // '1' Not defined, '2' One-chip color area sensor, '3' Two-chip color area sensor + // '4' Three-chip color area sensor, '5' Color sequential area sensor + // '7' Trilinear sensor '8' Color sequential linear sensor, 'Other' reserved + return getIndexedDescription(TAG_SENSING_METHOD, + 1, + "(Not defined)", + "One-chip color area sensor", + "Two-chip color area sensor", + "Three-chip color area sensor", + "Color sequential area sensor", + null, + "Trilinear sensor", + "Color sequential linear sensor" + ); + } + + @Nullable + public String getComponentConfigurationDescription() + { + int[] components = _directory.getIntArray(TAG_COMPONENTS_CONFIGURATION); + if (components == null) + return null; + String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"}; + StringBuilder componentConfig = new StringBuilder(); + for (int i = 0; i < Math.min(4, components.length); i++) { + int j = components[i]; + if (j > 0 && j < componentStrings.length) { + componentConfig.append(componentStrings[j]); + } + } + return componentConfig.toString(); + } + + @Nullable + public String getJpegProcDescription() + { + Integer value = _directory.getInteger(TAG_JPEG_PROC); + if (value == null) + return null; + switch (value) { + case 1: return "Baseline"; + case 14: return "Lossless"; + default: + return "Unknown (" + value + ")"; + } + } +} diff --git a/Source/com/drew/metadata/exif/ExifDirectoryBase.java b/Source/com/drew/metadata/exif/ExifDirectoryBase.java new file mode 100644 index 0000000..56c288d --- /dev/null +++ b/Source/com/drew/metadata/exif/ExifDirectoryBase.java @@ -0,0 +1,764 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * Base class for several Exif format tag directories. + * + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public abstract class ExifDirectoryBase extends Directory +{ + public static final int TAG_INTEROP_INDEX = 0x0001; + public static final int TAG_INTEROP_VERSION = 0x0002; + + /** + * The new subfile type tag. + * 0 = Full-resolution Image + * 1 = Reduced-resolution image + * 2 = Single page of multi-page image + * 3 = Single page of multi-page reduced-resolution image + * 4 = Transparency mask + * 5 = Transparency mask of reduced-resolution image + * 6 = Transparency mask of multi-page image + * 7 = Transparency mask of reduced-resolution multi-page image + */ + public static final int TAG_NEW_SUBFILE_TYPE = 0x00FE; + /** + * The old subfile type tag. + * 1 = Full-resolution image (Main image) + * 2 = Reduced-resolution image (Thumbnail) + * 3 = Single page of multi-page image + */ + public static final int TAG_SUBFILE_TYPE = 0x00FF; + + public static final int TAG_IMAGE_WIDTH = 0x0100; + public static final int TAG_IMAGE_HEIGHT = 0x0101; + + /** + * When image format is no compression, this value shows the number of bits + * per component for each pixel. Usually this value is '8,8,8'. + */ + public static final int TAG_BITS_PER_SAMPLE = 0x0102; + public static final int TAG_COMPRESSION = 0x0103; + + /** + * Shows the color space of the image data components. + * 0 = WhiteIsZero + * 1 = BlackIsZero + * 2 = RGB + * 3 = RGB Palette + * 4 = Transparency Mask + * 5 = CMYK + * 6 = YCbCr + * 8 = CIELab + * 9 = ICCLab + * 10 = ITULab + * 32803 = Color Filter Array + * 32844 = Pixar LogL + * 32845 = Pixar LogLuv + * 34892 = Linear Raw + */ + public static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106; + + /** + * 1 = No dithering or halftoning + * 2 = Ordered dither or halftone + * 3 = Randomized dither + */ + public static final int TAG_THRESHOLDING = 0x0107; + + /** + * 1 = Normal + * 2 = Reversed + */ + public static final int TAG_FILL_ORDER = 0x010A; + public static final int TAG_DOCUMENT_NAME = 0x010D; + + public static final int TAG_IMAGE_DESCRIPTION = 0x010E; + + public static final int TAG_MAKE = 0x010F; + public static final int TAG_MODEL = 0x0110; + /** The position in the file of raster data. */ + public static final int TAG_STRIP_OFFSETS = 0x0111; + public static final int TAG_ORIENTATION = 0x0112; + /** Each pixel is composed of this many samples. */ + public static final int TAG_SAMPLES_PER_PIXEL = 0x0115; + /** The raster is codified by a single block of data holding this many rows. */ + public static final int TAG_ROWS_PER_STRIP = 0x0116; + /** The size of the raster data in bytes. */ + public static final int TAG_STRIP_BYTE_COUNTS = 0x0117; + public static final int TAG_MIN_SAMPLE_VALUE = 0x0118; + public static final int TAG_MAX_SAMPLE_VALUE = 0x0119; + public static final int TAG_X_RESOLUTION = 0x011A; + public static final int TAG_Y_RESOLUTION = 0x011B; + /** + * When image format is no compression YCbCr, this value shows byte aligns of + * YCbCr data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for + * each subsampling pixel. If value is '2', Y/Cb/Cr value is separated and + * stored to Y plane/Cb plane/Cr plane format. + */ + public static final int TAG_PLANAR_CONFIGURATION = 0x011C; + public static final int TAG_PAGE_NAME = 0x011D; + + public static final int TAG_RESOLUTION_UNIT = 0x0128; + public static final int TAG_PAGE_NUMBER = 0x0129; + + public static final int TAG_TRANSFER_FUNCTION = 0x012D; + public static final int TAG_SOFTWARE = 0x0131; + public static final int TAG_DATETIME = 0x0132; + public static final int TAG_ARTIST = 0x013B; + public static final int TAG_HOST_COMPUTER = 0x013C; + public static final int TAG_PREDICTOR = 0x013D; + public static final int TAG_WHITE_POINT = 0x013E; + public static final int TAG_PRIMARY_CHROMATICITIES = 0x013F; + + public static final int TAG_TILE_WIDTH = 0x0142; + public static final int TAG_TILE_LENGTH = 0x0143; + public static final int TAG_TILE_OFFSETS = 0x0144; + public static final int TAG_TILE_BYTE_COUNTS = 0x0145; + + /** + * Tag is a pointer to one or more sub-IFDs. + + Seems to be used exclusively by raw formats, referencing one or two IFDs. + */ + public static final int TAG_SUB_IFD_OFFSET = 0x014a; + + public static final int TAG_TRANSFER_RANGE = 0x0156; + public static final int TAG_JPEG_TABLES = 0x015B; + public static final int TAG_JPEG_PROC = 0x0200; + + // 0x0201 can have all kinds of descriptions for thumbnail starting index + // 0x0202 can have all kinds of descriptions for thumbnail length + public static final int TAG_JPEG_RESTART_INTERVAL = 0x0203; + public static final int TAG_JPEG_LOSSLESS_PREDICTORS = 0x0205; + public static final int TAG_JPEG_POINT_TRANSFORMS = 0x0206; + public static final int TAG_JPEG_Q_TABLES = 0x0207; + public static final int TAG_JPEG_DC_TABLES = 0x0208; + public static final int TAG_JPEG_AC_TABLES = 0x0209; + + public static final int TAG_YCBCR_COEFFICIENTS = 0x0211; + public static final int TAG_YCBCR_SUBSAMPLING = 0x0212; + public static final int TAG_YCBCR_POSITIONING = 0x0213; + public static final int TAG_REFERENCE_BLACK_WHITE = 0x0214; + public static final int TAG_STRIP_ROW_COUNTS = 0x022f; + public static final int TAG_APPLICATION_NOTES = 0x02bc; + + public static final int TAG_RELATED_IMAGE_FILE_FORMAT = 0x1000; + public static final int TAG_RELATED_IMAGE_WIDTH = 0x1001; + public static final int TAG_RELATED_IMAGE_HEIGHT = 0x1002; + + public static final int TAG_RATING = 0x4746; + + public static final int TAG_CFA_REPEAT_PATTERN_DIM = 0x828D; + /** There are two definitions for CFA pattern, I don't know the difference... */ + public static final int TAG_CFA_PATTERN_2 = 0x828E; + public static final int TAG_BATTERY_LEVEL = 0x828F; + public static final int TAG_COPYRIGHT = 0x8298; + /** + * Exposure time (reciprocal of shutter speed). Unit is second. + */ + public static final int TAG_EXPOSURE_TIME = 0x829A; + /** + * The actual F-number(F-stop) of lens when the image was taken. + */ + public static final int TAG_FNUMBER = 0x829D; + public static final int TAG_IPTC_NAA = 0x83BB; + public static final int TAG_INTER_COLOR_PROFILE = 0x8773; + /** + * Exposure program that the camera used when image was taken. '1' means + * manual control, '2' program normal, '3' aperture priority, '4' shutter + * priority, '5' program creative (slow program), '6' program action + * (high-speed program), '7' portrait mode, '8' landscape mode. + */ + public static final int TAG_EXPOSURE_PROGRAM = 0x8822; + public static final int TAG_SPECTRAL_SENSITIVITY = 0x8824; + public static final int TAG_ISO_EQUIVALENT = 0x8827; + /** + * Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524. + * <p> + * OECF is the relationship between the camera optical input and the image values. + * <p> + * The values are: + * <ul> + * <li>Two shorts, indicating respectively number of columns, and number of rows.</li> + * <li>For each column, the column name in a null-terminated ASCII string.</li> + * <li>For each cell, an SRATIONAL value.</li> + * </ul> + */ + public static final int TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION = 0x8828; + public static final int TAG_INTERLACE = 0x8829; + public static final int TAG_TIME_ZONE_OFFSET_TIFF_EP = 0x882A; + public static final int TAG_SELF_TIMER_MODE_TIFF_EP = 0x882B; + /** + * Applies to ISO tag. + * + * 0 = Unknown + * 1 = Standard Output Sensitivity + * 2 = Recommended Exposure Index + * 3 = ISO Speed + * 4 = Standard Output Sensitivity and Recommended Exposure Index + * 5 = Standard Output Sensitivity and ISO Speed + * 6 = Recommended Exposure Index and ISO Speed + * 7 = Standard Output Sensitivity, Recommended Exposure Index and ISO Speed + */ + public static final int TAG_SENSITIVITY_TYPE = 0x8830; + public static final int TAG_STANDARD_OUTPUT_SENSITIVITY = 0x8831; + public static final int TAG_RECOMMENDED_EXPOSURE_INDEX = 0x8832; + /** Non-standard, but in use. */ + public static final int TAG_TIME_ZONE_OFFSET = 0x882A; + public static final int TAG_SELF_TIMER_MODE = 0x882B; + + public static final int TAG_EXIF_VERSION = 0x9000; + public static final int TAG_DATETIME_ORIGINAL = 0x9003; + public static final int TAG_DATETIME_DIGITIZED = 0x9004; + + public static final int TAG_COMPONENTS_CONFIGURATION = 0x9101; + /** + * Average (rough estimate) compression level in JPEG bits per pixel. + * */ + public static final int TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL = 0x9102; + + /** + * Shutter speed by APEX value. To convert this value to ordinary 'Shutter Speed'; + * calculate this value's power of 2, then reciprocal. For example, if the + * ShutterSpeedValue is '4', shutter speed is 1/(24)=1/16 second. + */ + public static final int TAG_SHUTTER_SPEED = 0x9201; + /** + * The actual aperture value of lens when the image was taken. Unit is APEX. + * To convert this value to ordinary F-number (F-stop), calculate this value's + * power of root 2 (=1.4142). For example, if the ApertureValue is '5', + * F-number is 1.4142^5 = F5.6. + */ + public static final int TAG_APERTURE = 0x9202; + public static final int TAG_BRIGHTNESS_VALUE = 0x9203; + public static final int TAG_EXPOSURE_BIAS = 0x9204; + /** + * Maximum aperture value of lens. You can convert to F-number by calculating + * power of root 2 (same process of ApertureValue:0x9202). + * The actual aperture value of lens when the image was taken. To convert this + * value to ordinary f-number(f-stop), calculate the value's power of root 2 + * (=1.4142). For example, if the ApertureValue is '5', f-number is 1.41425^5 = F5.6. + */ + public static final int TAG_MAX_APERTURE = 0x9205; + /** + * Indicates the distance the autofocus camera is focused to. Tends to be less accurate as distance increases. + */ + public static final int TAG_SUBJECT_DISTANCE = 0x9206; + /** + * Exposure metering method. '0' means unknown, '1' average, '2' center + * weighted average, '3' spot, '4' multi-spot, '5' multi-segment, '6' partial, + * '255' other. + */ + public static final int TAG_METERING_MODE = 0x9207; + + /** + * @deprecated use {@link com.drew.metadata.exif.ExifDirectoryBase#TAG_WHITE_BALANCE} instead. + */ + @Deprecated + public static final int TAG_LIGHT_SOURCE = 0x9208; + /** + * White balance (aka light source). '0' means unknown, '1' daylight, + * '2' fluorescent, '3' tungsten, '10' flash, '17' standard light A, + * '18' standard light B, '19' standard light C, '20' D55, '21' D65, + * '22' D75, '255' other. + */ + public static final int TAG_WHITE_BALANCE = 0x9208; + /** + * 0x0 = 0000000 = No Flash + * 0x1 = 0000001 = Fired + * 0x5 = 0000101 = Fired, Return not detected + * 0x7 = 0000111 = Fired, Return detected + * 0x9 = 0001001 = On + * 0xd = 0001101 = On, Return not detected + * 0xf = 0001111 = On, Return detected + * 0x10 = 0010000 = Off + * 0x18 = 0011000 = Auto, Did not fire + * 0x19 = 0011001 = Auto, Fired + * 0x1d = 0011101 = Auto, Fired, Return not detected + * 0x1f = 0011111 = Auto, Fired, Return detected + * 0x20 = 0100000 = No flash function + * 0x41 = 1000001 = Fired, Red-eye reduction + * 0x45 = 1000101 = Fired, Red-eye reduction, Return not detected + * 0x47 = 1000111 = Fired, Red-eye reduction, Return detected + * 0x49 = 1001001 = On, Red-eye reduction + * 0x4d = 1001101 = On, Red-eye reduction, Return not detected + * 0x4f = 1001111 = On, Red-eye reduction, Return detected + * 0x59 = 1011001 = Auto, Fired, Red-eye reduction + * 0x5d = 1011101 = Auto, Fired, Red-eye reduction, Return not detected + * 0x5f = 1011111 = Auto, Fired, Red-eye reduction, Return detected + * 6543210 (positions) + * + * This is a bitmask. + * 0 = flash fired + * 1 = return detected + * 2 = return able to be detected + * 3 = unknown + * 4 = auto used + * 5 = unknown + * 6 = red eye reduction used + */ + public static final int TAG_FLASH = 0x9209; + /** + * Focal length of lens used to take image. Unit is millimeter. + * Nice digital cameras actually save the focal length as a function of how far they are zoomed in. + */ + public static final int TAG_FOCAL_LENGTH = 0x920A; + + public static final int TAG_FLASH_ENERGY_TIFF_EP = 0x920B; + public static final int TAG_SPATIAL_FREQ_RESPONSE_TIFF_EP = 0x920C; + public static final int TAG_NOISE = 0x920D; + public static final int TAG_FOCAL_PLANE_X_RESOLUTION_TIFF_EP = 0x920E; + public static final int TAG_FOCAL_PLANE_Y_RESOLUTION_TIFF_EP = 0x920F; + public static final int TAG_IMAGE_NUMBER = 0x9211; + public static final int TAG_SECURITY_CLASSIFICATION = 0x9212; + public static final int TAG_IMAGE_HISTORY = 0x9213; + public static final int TAG_SUBJECT_LOCATION_TIFF_EP = 0x9214; + public static final int TAG_EXPOSURE_INDEX_TIFF_EP = 0x9215; + public static final int TAG_STANDARD_ID_TIFF_EP = 0x9216; + + /** + * This tag holds the Exif Makernote. Makernotes are free to be in any format, though they are often IFDs. + * To determine the format, we consider the starting bytes of the makernote itself and sometimes the + * camera model and make. + * <p> + * The component count for this tag includes all of the bytes needed for the makernote. + */ + public static final int TAG_MAKERNOTE = 0x927C; + + public static final int TAG_USER_COMMENT = 0x9286; + + public static final int TAG_SUBSECOND_TIME = 0x9290; + public static final int TAG_SUBSECOND_TIME_ORIGINAL = 0x9291; + public static final int TAG_SUBSECOND_TIME_DIGITIZED = 0x9292; + + /** The image title, as used by Windows XP. */ + public static final int TAG_WIN_TITLE = 0x9C9B; + /** The image comment, as used by Windows XP. */ + public static final int TAG_WIN_COMMENT = 0x9C9C; + /** The image author, as used by Windows XP (called Artist in the Windows shell). */ + public static final int TAG_WIN_AUTHOR = 0x9C9D; + /** The image keywords, as used by Windows XP. */ + public static final int TAG_WIN_KEYWORDS = 0x9C9E; + /** The image subject, as used by Windows XP. */ + public static final int TAG_WIN_SUBJECT = 0x9C9F; + + public static final int TAG_FLASHPIX_VERSION = 0xA000; + /** + * Defines Color Space. DCF image must use sRGB color space so value is + * always '1'. If the picture uses the other color space, value is + * '65535':Uncalibrated. + */ + public static final int TAG_COLOR_SPACE = 0xA001; + public static final int TAG_EXIF_IMAGE_WIDTH = 0xA002; + public static final int TAG_EXIF_IMAGE_HEIGHT = 0xA003; + public static final int TAG_RELATED_SOUND_FILE = 0xA004; + + public static final int TAG_FLASH_ENERGY = 0xA20B; + public static final int TAG_SPATIAL_FREQ_RESPONSE = 0xA20C; + public static final int TAG_FOCAL_PLANE_X_RESOLUTION = 0xA20E; + public static final int TAG_FOCAL_PLANE_Y_RESOLUTION = 0xA20F; + /** + * Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit, + * '2' inch, '3' centimeter. + * + * Note: Some of Fujifilm's digicam(e.g.FX2700,FX2900,Finepix4700Z/40i etc) + * uses value '3' so it must be 'centimeter', but it seems that they use a + * '8.3mm?'(1/3in.?) to their ResolutionUnit. Fuji's BUG? Finepix4900Z has + * been changed to use value '2' but it doesn't match to actual value also. + */ + public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT = 0xA210; + public static final int TAG_SUBJECT_LOCATION = 0xA214; + public static final int TAG_EXPOSURE_INDEX = 0xA215; + public static final int TAG_SENSING_METHOD = 0xA217; + + public static final int TAG_FILE_SOURCE = 0xA300; + public static final int TAG_SCENE_TYPE = 0xA301; + public static final int TAG_CFA_PATTERN = 0xA302; + + /** + * This tag indicates the use of special processing on image data, such as rendering + * geared to output. When special processing is performed, the reader is expected to + * disable or minimize any further processing. + * Tag = 41985 (A401.H) + * Type = SHORT + * Count = 1 + * Default = 0 + * 0 = Normal process + * 1 = Custom process + * Other = reserved + */ + public static final int TAG_CUSTOM_RENDERED = 0xA401; + /** + * This tag indicates the exposure mode set when the image was shot. In auto-bracketing + * mode, the camera shoots a series of frames of the same scene at different exposure settings. + * Tag = 41986 (A402.H) + * Type = SHORT + * Count = 1 + * Default = none + * 0 = Auto exposure + * 1 = Manual exposure + * 2 = Auto bracket + * Other = reserved + */ + public static final int TAG_EXPOSURE_MODE = 0xA402; + /** + * This tag indicates the white balance mode set when the image was shot. + * Tag = 41987 (A403.H) + * Type = SHORT + * Count = 1 + * Default = none + * 0 = Auto white balance + * 1 = Manual white balance + * Other = reserved + */ + public static final int TAG_WHITE_BALANCE_MODE = 0xA403; + /** + * This tag indicates the digital zoom ratio when the image was shot. If the + * numerator of the recorded value is 0, this indicates that digital zoom was + * not used. + * Tag = 41988 (A404.H) + * Type = RATIONAL + * Count = 1 + * Default = none + */ + public static final int TAG_DIGITAL_ZOOM_RATIO = 0xA404; + /** + * This tag indicates the equivalent focal length assuming a 35mm film camera, + * in mm. A value of 0 means the focal length is unknown. Note that this tag + * differs from the FocalLength tag. + * Tag = 41989 (A405.H) + * Type = SHORT + * Count = 1 + * Default = none + */ + public static final int TAG_35MM_FILM_EQUIV_FOCAL_LENGTH = 0xA405; + /** + * This tag indicates the type of scene that was shot. It can also be used to + * record the mode in which the image was shot. Note that this differs from + * the scene type (SceneType) tag. + * Tag = 41990 (A406.H) + * Type = SHORT + * Count = 1 + * Default = 0 + * 0 = Standard + * 1 = Landscape + * 2 = Portrait + * 3 = Night scene + * Other = reserved + */ + public static final int TAG_SCENE_CAPTURE_TYPE = 0xA406; + /** + * This tag indicates the degree of overall image gain adjustment. + * Tag = 41991 (A407.H) + * Type = SHORT + * Count = 1 + * Default = none + * 0 = None + * 1 = Low gain up + * 2 = High gain up + * 3 = Low gain down + * 4 = High gain down + * Other = reserved + */ + public static final int TAG_GAIN_CONTROL = 0xA407; + /** + * This tag indicates the direction of contrast processing applied by the camera + * when the image was shot. + * Tag = 41992 (A408.H) + * Type = SHORT + * Count = 1 + * Default = 0 + * 0 = Normal + * 1 = Soft + * 2 = Hard + * Other = reserved + */ + public static final int TAG_CONTRAST = 0xA408; + /** + * This tag indicates the direction of saturation processing applied by the camera + * when the image was shot. + * Tag = 41993 (A409.H) + * Type = SHORT + * Count = 1 + * Default = 0 + * 0 = Normal + * 1 = Low saturation + * 2 = High saturation + * Other = reserved + */ + public static final int TAG_SATURATION = 0xA409; + /** + * This tag indicates the direction of sharpness processing applied by the camera + * when the image was shot. + * Tag = 41994 (A40A.H) + * Type = SHORT + * Count = 1 + * Default = 0 + * 0 = Normal + * 1 = Soft + * 2 = Hard + * Other = reserved + */ + public static final int TAG_SHARPNESS = 0xA40A; + /** + * This tag indicates information on the picture-taking conditions of a particular + * camera model. The tag is used only to indicate the picture-taking conditions in + * the reader. + * Tag = 41995 (A40B.H) + * Type = UNDEFINED + * Count = Any + * Default = none + * + * The information is recorded in the format shown below. The data is recorded + * in Unicode using SHORT type for the number of display rows and columns and + * UNDEFINED type for the camera settings. The Unicode (UCS-2) string including + * Signature is NULL terminated. The specifics of the Unicode string are as given + * in ISO/IEC 10464-1. + * + * Length Type Meaning + * ------+-----------+------------------ + * 2 SHORT Display columns + * 2 SHORT Display rows + * Any UNDEFINED Camera setting-1 + * Any UNDEFINED Camera setting-2 + * : : : + * Any UNDEFINED Camera setting-n + */ + public static final int TAG_DEVICE_SETTING_DESCRIPTION = 0xA40B; + /** + * This tag indicates the distance to the subject. + * Tag = 41996 (A40C.H) + * Type = SHORT + * Count = 1 + * Default = none + * 0 = unknown + * 1 = Macro + * 2 = Close view + * 3 = Distant view + * Other = reserved + */ + public static final int TAG_SUBJECT_DISTANCE_RANGE = 0xA40C; + + /** + * This tag indicates an identifier assigned uniquely to each image. It is + * recorded as an ASCII string equivalent to hexadecimal notation and 128-bit + * fixed length. + * Tag = 42016 (A420.H) + * Type = ASCII + * Count = 33 + * Default = none + */ + public static final int TAG_IMAGE_UNIQUE_ID = 0xA420; + /** String. */ + public static final int TAG_CAMERA_OWNER_NAME = 0xA430; + /** String. */ + public static final int TAG_BODY_SERIAL_NUMBER = 0xA431; + /** An array of four Rational64u numbers giving focal and aperture ranges. */ + public static final int TAG_LENS_SPECIFICATION = 0xA432; + /** String. */ + public static final int TAG_LENS_MAKE = 0xA433; + /** String. */ + public static final int TAG_LENS_MODEL = 0xA434; + /** String. */ + public static final int TAG_LENS_SERIAL_NUMBER = 0xA435; + /** Rational64u. */ + public static final int TAG_GAMMA = 0xA500; + + public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0xC4A5; + + public static final int TAG_PANASONIC_TITLE = 0xC6D2; + public static final int TAG_PANASONIC_TITLE_2 = 0xC6D3; + + public static final int TAG_PADDING = 0xEA1C; + + public static final int TAG_LENS = 0xFDEA; + + protected static void addExifTagNames(HashMap<Integer, String> map) + { + map.put(TAG_INTEROP_INDEX, "Interoperability Index"); + map.put(TAG_INTEROP_VERSION, "Interoperability Version"); + map.put(TAG_NEW_SUBFILE_TYPE, "New Subfile Type"); + map.put(TAG_SUBFILE_TYPE, "Subfile Type"); + map.put(TAG_IMAGE_WIDTH, "Image Width"); + map.put(TAG_IMAGE_HEIGHT, "Image Height"); + map.put(TAG_BITS_PER_SAMPLE, "Bits Per Sample"); + map.put(TAG_COMPRESSION, "Compression"); + map.put(TAG_PHOTOMETRIC_INTERPRETATION, "Photometric Interpretation"); + map.put(TAG_THRESHOLDING, "Thresholding"); + map.put(TAG_FILL_ORDER, "Fill Order"); + map.put(TAG_DOCUMENT_NAME, "Document Name"); + map.put(TAG_IMAGE_DESCRIPTION, "Image Description"); + map.put(TAG_MAKE, "Make"); + map.put(TAG_MODEL, "Model"); + map.put(TAG_STRIP_OFFSETS, "Strip Offsets"); + map.put(TAG_ORIENTATION, "Orientation"); + map.put(TAG_SAMPLES_PER_PIXEL, "Samples Per Pixel"); + map.put(TAG_ROWS_PER_STRIP, "Rows Per Strip"); + map.put(TAG_STRIP_BYTE_COUNTS, "Strip Byte Counts"); + map.put(TAG_MIN_SAMPLE_VALUE, "Minimum Sample Value"); + map.put(TAG_MAX_SAMPLE_VALUE, "Maximum Sample Value"); + map.put(TAG_X_RESOLUTION, "X Resolution"); + map.put(TAG_Y_RESOLUTION, "Y Resolution"); + map.put(TAG_PLANAR_CONFIGURATION, "Planar Configuration"); + map.put(TAG_PAGE_NAME, "Page Name"); + map.put(TAG_RESOLUTION_UNIT, "Resolution Unit"); + map.put(TAG_PAGE_NUMBER, "Page Number"); + map.put(TAG_TRANSFER_FUNCTION, "Transfer Function"); + map.put(TAG_SOFTWARE, "Software"); + map.put(TAG_DATETIME, "Date/Time"); + map.put(TAG_ARTIST, "Artist"); + map.put(TAG_PREDICTOR, "Predictor"); + map.put(TAG_HOST_COMPUTER, "Host Computer"); + map.put(TAG_WHITE_POINT, "White Point"); + map.put(TAG_PRIMARY_CHROMATICITIES, "Primary Chromaticities"); + map.put(TAG_TILE_WIDTH, "Tile Width"); + map.put(TAG_TILE_LENGTH, "Tile Length"); + map.put(TAG_TILE_OFFSETS, "Tile Offsets"); + map.put(TAG_TILE_BYTE_COUNTS, "Tile Byte Counts"); + map.put(TAG_SUB_IFD_OFFSET, "Sub IFD Pointer(s)"); + map.put(TAG_TRANSFER_RANGE, "Transfer Range"); + map.put(TAG_JPEG_TABLES, "JPEG Tables"); + map.put(TAG_JPEG_PROC, "JPEG Proc"); + + map.put(TAG_JPEG_RESTART_INTERVAL, "JPEG Restart Interval"); + map.put(TAG_JPEG_LOSSLESS_PREDICTORS, "JPEG Lossless Predictors"); + map.put(TAG_JPEG_POINT_TRANSFORMS, "JPEG Point Transforms"); + map.put(TAG_JPEG_Q_TABLES, "JPEGQ Tables"); + map.put(TAG_JPEG_DC_TABLES, "JPEGDC Tables"); + map.put(TAG_JPEG_AC_TABLES, "JPEGAC Tables"); + + map.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients"); + map.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling"); + map.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning"); + map.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White"); + map.put(TAG_STRIP_ROW_COUNTS, "Strip Row Counts"); + map.put(TAG_APPLICATION_NOTES, "Application Notes"); + map.put(TAG_RELATED_IMAGE_FILE_FORMAT, "Related Image File Format"); + map.put(TAG_RELATED_IMAGE_WIDTH, "Related Image Width"); + map.put(TAG_RELATED_IMAGE_HEIGHT, "Related Image Height"); + map.put(TAG_RATING, "Rating"); + map.put(TAG_CFA_REPEAT_PATTERN_DIM, "CFA Repeat Pattern Dim"); + map.put(TAG_CFA_PATTERN_2, "CFA Pattern"); + map.put(TAG_BATTERY_LEVEL, "Battery Level"); + map.put(TAG_COPYRIGHT, "Copyright"); + map.put(TAG_EXPOSURE_TIME, "Exposure Time"); + map.put(TAG_FNUMBER, "F-Number"); + map.put(TAG_IPTC_NAA, "IPTC/NAA"); + map.put(TAG_INTER_COLOR_PROFILE, "Inter Color Profile"); + map.put(TAG_EXPOSURE_PROGRAM, "Exposure Program"); + map.put(TAG_SPECTRAL_SENSITIVITY, "Spectral Sensitivity"); + map.put(TAG_ISO_EQUIVALENT, "ISO Speed Ratings"); + map.put(TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION, "Opto-electric Conversion Function (OECF)"); + map.put(TAG_INTERLACE, "Interlace"); + map.put(TAG_TIME_ZONE_OFFSET_TIFF_EP, "Time Zone Offset"); + map.put(TAG_SELF_TIMER_MODE_TIFF_EP, "Self Timer Mode"); + map.put(TAG_SENSITIVITY_TYPE, "Sensitivity Type"); + map.put(TAG_STANDARD_OUTPUT_SENSITIVITY, "Standard Output Sensitivity"); + map.put(TAG_RECOMMENDED_EXPOSURE_INDEX, "Recommended Exposure Index"); + map.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset"); + map.put(TAG_SELF_TIMER_MODE, "Self Timer Mode"); + map.put(TAG_EXIF_VERSION, "Exif Version"); + map.put(TAG_DATETIME_ORIGINAL, "Date/Time Original"); + map.put(TAG_DATETIME_DIGITIZED, "Date/Time Digitized"); + map.put(TAG_COMPONENTS_CONFIGURATION, "Components Configuration"); + map.put(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, "Compressed Bits Per Pixel"); + map.put(TAG_SHUTTER_SPEED, "Shutter Speed Value"); + map.put(TAG_APERTURE, "Aperture Value"); + map.put(TAG_BRIGHTNESS_VALUE, "Brightness Value"); + map.put(TAG_EXPOSURE_BIAS, "Exposure Bias Value"); + map.put(TAG_MAX_APERTURE, "Max Aperture Value"); + map.put(TAG_SUBJECT_DISTANCE, "Subject Distance"); + map.put(TAG_METERING_MODE, "Metering Mode"); + map.put(TAG_WHITE_BALANCE, "White Balance"); + map.put(TAG_FLASH, "Flash"); + map.put(TAG_FOCAL_LENGTH, "Focal Length"); + map.put(TAG_FLASH_ENERGY_TIFF_EP, "Flash Energy"); + map.put(TAG_SPATIAL_FREQ_RESPONSE_TIFF_EP, "Spatial Frequency Response"); + map.put(TAG_NOISE, "Noise"); + map.put(TAG_FOCAL_PLANE_X_RESOLUTION_TIFF_EP, "Focal Plane X Resolution"); + map.put(TAG_FOCAL_PLANE_Y_RESOLUTION_TIFF_EP, "Focal Plane Y Resolution"); + map.put(TAG_IMAGE_NUMBER, "Image Number"); + map.put(TAG_SECURITY_CLASSIFICATION, "Security Classification"); + map.put(TAG_IMAGE_HISTORY, "Image History"); + map.put(TAG_SUBJECT_LOCATION_TIFF_EP, "Subject Location"); + map.put(TAG_EXPOSURE_INDEX_TIFF_EP, "Exposure Index"); + map.put(TAG_STANDARD_ID_TIFF_EP, "TIFF/EP Standard ID"); + map.put(TAG_MAKERNOTE, "Makernote"); + map.put(TAG_USER_COMMENT, "User Comment"); + map.put(TAG_SUBSECOND_TIME, "Sub-Sec Time"); + map.put(TAG_SUBSECOND_TIME_ORIGINAL, "Sub-Sec Time Original"); + map.put(TAG_SUBSECOND_TIME_DIGITIZED, "Sub-Sec Time Digitized"); + map.put(TAG_WIN_TITLE, "Windows XP Title"); + map.put(TAG_WIN_COMMENT, "Windows XP Comment"); + map.put(TAG_WIN_AUTHOR, "Windows XP Author"); + map.put(TAG_WIN_KEYWORDS, "Windows XP Keywords"); + map.put(TAG_WIN_SUBJECT, "Windows XP Subject"); + map.put(TAG_FLASHPIX_VERSION, "FlashPix Version"); + map.put(TAG_COLOR_SPACE, "Color Space"); + map.put(TAG_EXIF_IMAGE_WIDTH, "Exif Image Width"); + map.put(TAG_EXIF_IMAGE_HEIGHT, "Exif Image Height"); + map.put(TAG_RELATED_SOUND_FILE, "Related Sound File"); + map.put(TAG_FLASH_ENERGY, "Flash Energy"); + map.put(TAG_SPATIAL_FREQ_RESPONSE, "Spatial Frequency Response"); + map.put(TAG_FOCAL_PLANE_X_RESOLUTION, "Focal Plane X Resolution"); + map.put(TAG_FOCAL_PLANE_Y_RESOLUTION, "Focal Plane Y Resolution"); + map.put(TAG_FOCAL_PLANE_RESOLUTION_UNIT, "Focal Plane Resolution Unit"); + map.put(TAG_SUBJECT_LOCATION, "Subject Location"); + map.put(TAG_EXPOSURE_INDEX, "Exposure Index"); + map.put(TAG_SENSING_METHOD, "Sensing Method"); + map.put(TAG_FILE_SOURCE, "File Source"); + map.put(TAG_SCENE_TYPE, "Scene Type"); + map.put(TAG_CFA_PATTERN, "CFA Pattern"); + map.put(TAG_CUSTOM_RENDERED, "Custom Rendered"); + map.put(TAG_EXPOSURE_MODE, "Exposure Mode"); + map.put(TAG_WHITE_BALANCE_MODE, "White Balance Mode"); + map.put(TAG_DIGITAL_ZOOM_RATIO, "Digital Zoom Ratio"); + map.put(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH, "Focal Length 35"); + map.put(TAG_SCENE_CAPTURE_TYPE, "Scene Capture Type"); + map.put(TAG_GAIN_CONTROL, "Gain Control"); + map.put(TAG_CONTRAST, "Contrast"); + map.put(TAG_SATURATION, "Saturation"); + map.put(TAG_SHARPNESS, "Sharpness"); + map.put(TAG_DEVICE_SETTING_DESCRIPTION, "Device Setting Description"); + map.put(TAG_SUBJECT_DISTANCE_RANGE, "Subject Distance Range"); + map.put(TAG_IMAGE_UNIQUE_ID, "Unique Image ID"); + map.put(TAG_CAMERA_OWNER_NAME, "Camera Owner Name"); + map.put(TAG_BODY_SERIAL_NUMBER, "Body Serial Number"); + map.put(TAG_LENS_SPECIFICATION, "Lens Specification"); + map.put(TAG_LENS_MAKE, "Lens Make"); + map.put(TAG_LENS_MODEL, "Lens Model"); + map.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number"); + map.put(TAG_GAMMA, "Gamma"); + map.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info"); + map.put(TAG_PANASONIC_TITLE, "Panasonic Title"); + map.put(TAG_PANASONIC_TITLE_2, "Panasonic Title (2)"); + map.put(TAG_PADDING, "Padding"); + map.put(TAG_LENS, "Lens"); + } +} diff --git a/Source/com/drew/metadata/exif/ExifIFD0Descriptor.java b/Source/com/drew/metadata/exif/ExifIFD0Descriptor.java index bf2f3a8..6d8084b 100644 --- a/Source/com/drew/metadata/exif/ExifIFD0Descriptor.java +++ b/Source/com/drew/metadata/exif/ExifIFD0Descriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,190 +21,18 @@ package com.drew.metadata.exif; -import com.drew.lang.Rational; import com.drew.lang.annotations.NotNull; -import com.drew.lang.annotations.Nullable; -import com.drew.metadata.TagDescriptor; - -import java.io.UnsupportedEncodingException; - -import static com.drew.metadata.exif.ExifIFD0Directory.*; /** * Provides human-readable string representations of tag values stored in a {@link ExifIFD0Directory}. * * @author Drew Noakes https://drewnoakes.com */ -public class ExifIFD0Descriptor extends TagDescriptor<ExifIFD0Directory> +@SuppressWarnings("WeakerAccess") +public class ExifIFD0Descriptor extends ExifDescriptorBase<ExifIFD0Directory> { - /** - * Dictates whether rational values will be represented in decimal format in instances - * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3). - */ - private final boolean _allowDecimalRepresentationOfRationals = true; - public ExifIFD0Descriptor(@NotNull ExifIFD0Directory directory) { super(directory); } - - // Note for the potential addition of brightness presentation in eV: - // Brightness of taken subject. To calculate Exposure(Ev) from BrightnessValue(Bv), - // you must add SensitivityValue(Sv). - // Ev=BV+Sv Sv=log2(ISOSpeedRating/3.125) - // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32. - - /** - * Returns a descriptive value of the specified tag for this image. - * Where possible, known values will be substituted here in place of the raw - * tokens actually kept in the Exif segment. If no substitution is - * available, the value provided by getString(int) will be returned. - * @param tagType the tag to find a description for - * @return a description of the image's value for the specified tag, or - * <code>null</code> if the tag hasn't been defined. - */ - @Override - @Nullable - public String getDescription(int tagType) - { - switch (tagType) { - case TAG_RESOLUTION_UNIT: - return getResolutionDescription(); - case TAG_YCBCR_POSITIONING: - return getYCbCrPositioningDescription(); - case TAG_X_RESOLUTION: - return getXResolutionDescription(); - case TAG_Y_RESOLUTION: - return getYResolutionDescription(); - case TAG_REFERENCE_BLACK_WHITE: - return getReferenceBlackWhiteDescription(); - case TAG_ORIENTATION: - return getOrientationDescription(); - - case TAG_WIN_AUTHOR: - return getWindowsAuthorDescription(); - case TAG_WIN_COMMENT: - return getWindowsCommentDescription(); - case TAG_WIN_KEYWORDS: - return getWindowsKeywordsDescription(); - case TAG_WIN_SUBJECT: - return getWindowsSubjectDescription(); - case TAG_WIN_TITLE: - return getWindowsTitleDescription(); - - default: - return super.getDescription(tagType); - } - } - - @Nullable - public String getReferenceBlackWhiteDescription() - { - int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE); - if (ints==null || ints.length < 6) - return null; - int blackR = ints[0]; - int whiteR = ints[1]; - int blackG = ints[2]; - int whiteG = ints[3]; - int blackB = ints[4]; - int whiteB = ints[5]; - return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB); - } - - @Nullable - public String getYResolutionDescription() - { - Rational value = _directory.getRational(TAG_Y_RESOLUTION); - if (value==null) - return null; - final String unit = getResolutionDescription(); - return String.format("%s dots per %s", - value.toSimpleString(_allowDecimalRepresentationOfRationals), - unit == null ? "unit" : unit.toLowerCase()); - } - - @Nullable - public String getXResolutionDescription() - { - Rational value = _directory.getRational(TAG_X_RESOLUTION); - if (value==null) - return null; - final String unit = getResolutionDescription(); - return String.format("%s dots per %s", - value.toSimpleString(_allowDecimalRepresentationOfRationals), - unit == null ? "unit" : unit.toLowerCase()); - } - - @Nullable - public String getYCbCrPositioningDescription() - { - return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point"); - } - - @Nullable - public String getOrientationDescription() - { - return getIndexedDescription(TAG_ORIENTATION, 1, - "Top, left side (Horizontal / normal)", - "Top, right side (Mirror horizontal)", - "Bottom, right side (Rotate 180)", - "Bottom, left side (Mirror vertical)", - "Left side, top (Mirror horizontal and rotate 270 CW)", - "Right side, top (Rotate 90 CW)", - "Right side, bottom (Mirror horizontal and rotate 90 CW)", - "Left side, bottom (Rotate 270 CW)"); - } - - @Nullable - public String getResolutionDescription() - { - // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch) - return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm"); - } - - /** The Windows specific tags uses plain Unicode. */ - @Nullable - private String getUnicodeDescription(int tag) - { - byte[] bytes = _directory.getByteArray(tag); - if (bytes == null) - return null; - try { - // Decode the unicode string and trim the unicode zero "\0" from the end. - return new String(bytes, "UTF-16LE").trim(); - } catch (UnsupportedEncodingException ex) { - return null; - } - } - - @Nullable - public String getWindowsAuthorDescription() - { - return getUnicodeDescription(TAG_WIN_AUTHOR); - } - - @Nullable - public String getWindowsCommentDescription() - { - return getUnicodeDescription(TAG_WIN_COMMENT); - } - - @Nullable - public String getWindowsKeywordsDescription() - { - return getUnicodeDescription(TAG_WIN_KEYWORDS); - } - - @Nullable - public String getWindowsTitleDescription() - { - return getUnicodeDescription(TAG_WIN_TITLE); - } - - @Nullable - public String getWindowsSubjectDescription() - { - return getUnicodeDescription(TAG_WIN_SUBJECT); - } } diff --git a/Source/com/drew/metadata/exif/ExifIFD0Directory.java b/Source/com/drew/metadata/exif/ExifIFD0Directory.java index e9cfb5c..1b393ba 100644 --- a/Source/com/drew/metadata/exif/ExifIFD0Directory.java +++ b/Source/com/drew/metadata/exif/ExifIFD0Directory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ package com.drew.metadata.exif; import com.drew.lang.annotations.NotNull; -import com.drew.metadata.Directory; import java.util.HashMap; @@ -31,83 +30,26 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ -public class ExifIFD0Directory extends Directory +@SuppressWarnings("WeakerAccess") +public class ExifIFD0Directory extends ExifDirectoryBase { - public static final int TAG_IMAGE_DESCRIPTION = 0x010E; - public static final int TAG_MAKE = 0x010F; - public static final int TAG_MODEL = 0x0110; - public static final int TAG_ORIENTATION = 0x0112; - public static final int TAG_X_RESOLUTION = 0x011A; - public static final int TAG_Y_RESOLUTION = 0x011B; - public static final int TAG_RESOLUTION_UNIT = 0x0128; - public static final int TAG_SOFTWARE = 0x0131; - public static final int TAG_DATETIME = 0x0132; - public static final int TAG_ARTIST = 0x013B; - public static final int TAG_WHITE_POINT = 0x013E; - public static final int TAG_PRIMARY_CHROMATICITIES = 0x013F; - - public static final int TAG_YCBCR_COEFFICIENTS = 0x0211; - public static final int TAG_YCBCR_POSITIONING = 0x0213; - public static final int TAG_REFERENCE_BLACK_WHITE = 0x0214; - - /** This tag is a pointer to the Exif SubIFD. */ public static final int TAG_EXIF_SUB_IFD_OFFSET = 0x8769; /** This tag is a pointer to the Exif GPS IFD. */ public static final int TAG_GPS_INFO_OFFSET = 0x8825; - public static final int TAG_COPYRIGHT = 0x8298; - - /** Non-standard, but in use. */ - public static final int TAG_TIME_ZONE_OFFSET = 0x882a; - - /** The image title, as used by Windows XP. */ - public static final int TAG_WIN_TITLE = 0x9C9B; - /** The image comment, as used by Windows XP. */ - public static final int TAG_WIN_COMMENT = 0x9C9C; - /** The image author, as used by Windows XP (called Artist in the Windows shell). */ - public static final int TAG_WIN_AUTHOR = 0x9C9D; - /** The image keywords, as used by Windows XP. */ - public static final int TAG_WIN_KEYWORDS = 0x9C9E; - /** The image subject, as used by Windows XP. */ - public static final int TAG_WIN_SUBJECT = 0x9C9F; + public ExifIFD0Directory() + { + this.setDescriptor(new ExifIFD0Descriptor(this)); + } @NotNull protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); static { - _tagNameMap.put(TAG_IMAGE_DESCRIPTION, "Image Description"); - _tagNameMap.put(TAG_MAKE, "Make"); - _tagNameMap.put(TAG_MODEL, "Model"); - _tagNameMap.put(TAG_ORIENTATION, "Orientation"); - _tagNameMap.put(TAG_X_RESOLUTION, "X Resolution"); - _tagNameMap.put(TAG_Y_RESOLUTION, "Y Resolution"); - _tagNameMap.put(TAG_RESOLUTION_UNIT, "Resolution Unit"); - _tagNameMap.put(TAG_SOFTWARE, "Software"); - _tagNameMap.put(TAG_DATETIME, "Date/Time"); - _tagNameMap.put(TAG_ARTIST, "Artist"); - _tagNameMap.put(TAG_WHITE_POINT, "White Point"); - _tagNameMap.put(TAG_PRIMARY_CHROMATICITIES, "Primary Chromaticities"); - _tagNameMap.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients"); - _tagNameMap.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning"); - _tagNameMap.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White"); - - _tagNameMap.put(TAG_COPYRIGHT, "Copyright"); - - _tagNameMap.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset"); - - _tagNameMap.put(TAG_WIN_AUTHOR, "Windows XP Author"); - _tagNameMap.put(TAG_WIN_COMMENT, "Windows XP Comment"); - _tagNameMap.put(TAG_WIN_KEYWORDS, "Windows XP Keywords"); - _tagNameMap.put(TAG_WIN_SUBJECT, "Windows XP Subject"); - _tagNameMap.put(TAG_WIN_TITLE, "Windows XP Title"); - } - - public ExifIFD0Directory() - { - this.setDescriptor(new ExifIFD0Descriptor(this)); + addExifTagNames(_tagNameMap); } @Override diff --git a/Source/com/drew/metadata/exif/ExifImageDescriptor.java b/Source/com/drew/metadata/exif/ExifImageDescriptor.java new file mode 100644 index 0000000..2e11159 --- /dev/null +++ b/Source/com/drew/metadata/exif/ExifImageDescriptor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; + +/** + * Provides human-readable string representations of tag values stored in a {@link ExifImageDirectory}. + * + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class ExifImageDescriptor extends ExifDescriptorBase<ExifImageDirectory> +{ + public ExifImageDescriptor(@NotNull ExifImageDirectory directory) + { + super(directory); + } +} diff --git a/Source/com/drew/metadata/exif/ExifImageDirectory.java b/Source/com/drew/metadata/exif/ExifImageDirectory.java new file mode 100644 index 0000000..8da34c1 --- /dev/null +++ b/Source/com/drew/metadata/exif/ExifImageDirectory.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; + +import java.util.HashMap; + +/** + * Describes One of several Exif directories. + * + * Holds information about image IFD's in a chain after the first. The first page is stored in IFD0. + * Currently, this only applied to multi-page TIFF images + * + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class ExifImageDirectory extends ExifDirectoryBase +{ + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + addExifTagNames(_tagNameMap); + } + + public ExifImageDirectory() + { + this.setDescriptor(new ExifImageDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Exif Image"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/ExifInteropDescriptor.java b/Source/com/drew/metadata/exif/ExifInteropDescriptor.java index 185ab11..2029f42 100644 --- a/Source/com/drew/metadata/exif/ExifInteropDescriptor.java +++ b/Source/com/drew/metadata/exif/ExifInteropDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,53 +21,17 @@ package com.drew.metadata.exif; import com.drew.lang.annotations.NotNull; -import com.drew.lang.annotations.Nullable; -import com.drew.metadata.TagDescriptor; - -import static com.drew.metadata.exif.ExifInteropDirectory.*; /** * Provides human-readable string representations of tag values stored in a {@link ExifInteropDirectory}. * * @author Drew Noakes https://drewnoakes.com */ -public class ExifInteropDescriptor extends TagDescriptor<ExifInteropDirectory> +@SuppressWarnings("WeakerAccess") +public class ExifInteropDescriptor extends ExifDescriptorBase<ExifInteropDirectory> { public ExifInteropDescriptor(@NotNull ExifInteropDirectory directory) { super(directory); } - - @Override - @Nullable - public String getDescription(int tagType) - { - switch (tagType) { - case TAG_INTEROP_INDEX: - return getInteropIndexDescription(); - case TAG_INTEROP_VERSION: - return getInteropVersionDescription(); - default: - return super.getDescription(tagType); - } - } - - @Nullable - public String getInteropVersionDescription() - { - return getVersionBytesDescription(TAG_INTEROP_VERSION, 2); - } - - @Nullable - public String getInteropIndexDescription() - { - String value = _directory.getString(TAG_INTEROP_INDEX); - - if (value == null) - return null; - - return "R98".equalsIgnoreCase(value.trim()) - ? "Recommended Exif Interoperability Rules (ExifR98)" - : "Unknown (" + value + ")"; - } } diff --git a/Source/com/drew/metadata/exif/ExifInteropDirectory.java b/Source/com/drew/metadata/exif/ExifInteropDirectory.java index 7e6e68d..ee0f543 100644 --- a/Source/com/drew/metadata/exif/ExifInteropDirectory.java +++ b/Source/com/drew/metadata/exif/ExifInteropDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ package com.drew.metadata.exif; import com.drew.lang.annotations.NotNull; -import com.drew.metadata.Directory; import java.util.HashMap; @@ -30,24 +29,15 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ -public class ExifInteropDirectory extends Directory +@SuppressWarnings("WeakerAccess") +public class ExifInteropDirectory extends ExifDirectoryBase { - public static final int TAG_INTEROP_INDEX = 0x0001; - public static final int TAG_INTEROP_VERSION = 0x0002; - public static final int TAG_RELATED_IMAGE_FILE_FORMAT = 0x1000; - public static final int TAG_RELATED_IMAGE_WIDTH = 0x1001; - public static final int TAG_RELATED_IMAGE_LENGTH = 0x1002; - @NotNull protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); static { - _tagNameMap.put(TAG_INTEROP_INDEX, "Interoperability Index"); - _tagNameMap.put(TAG_INTEROP_VERSION, "Interoperability Version"); - _tagNameMap.put(TAG_RELATED_IMAGE_FILE_FORMAT, "Related Image File Format"); - _tagNameMap.put(TAG_RELATED_IMAGE_WIDTH, "Related Image Width"); - _tagNameMap.put(TAG_RELATED_IMAGE_LENGTH, "Related Image Length"); + addExifTagNames(_tagNameMap); } public ExifInteropDirectory() diff --git a/Source/com/drew/metadata/exif/ExifReader.java b/Source/com/drew/metadata/exif/ExifReader.java index ecbc95a..ffc77c6 100644 --- a/Source/com/drew/metadata/exif/ExifReader.java +++ b/Source/com/drew/metadata/exif/ExifReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,11 +25,14 @@ import com.drew.imaging.jpeg.JpegSegmentType; import com.drew.imaging.tiff.TiffProcessingException; import com.drew.imaging.tiff.TiffReader; import com.drew.lang.ByteArrayReader; +import com.drew.lang.RandomAccessReader; import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import java.io.IOException; -import java.util.Arrays; +import java.util.Collections; /** * Decodes Exif binary data, populating a {@link Metadata} object with tag values in {@link ExifSubIFDDirectory}, @@ -38,77 +41,60 @@ import java.util.Arrays; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class ExifReader implements JpegSegmentMetadataReader { - /** - * The offset at which the TIFF data actually starts. This may be necessary when, for example, processing - * JPEG Exif data from APP0 which has a 6-byte preamble before starting the TIFF data. - */ - private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0"; + /** Exif data stored in JPEG files' APP1 segment are preceded by this six character preamble. */ + public static final String JPEG_SEGMENT_PREAMBLE = "Exif\0\0"; - private boolean _storeThumbnailBytes = true; - - public boolean isStoreThumbnailBytes() + @NotNull + public Iterable<JpegSegmentType> getSegmentTypes() { - return _storeThumbnailBytes; + return Collections.singletonList(JpegSegmentType.APP1); } - public void setStoreThumbnailBytes(boolean storeThumbnailBytes) + public void readJpegSegments(@NotNull final Iterable<byte[]> segments, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType) { - _storeThumbnailBytes = storeThumbnailBytes; + assert(segmentType == JpegSegmentType.APP1); + + for (byte[] segmentBytes : segments) { + // Filter any segments containing unexpected preambles + if (segmentBytes.length < JPEG_SEGMENT_PREAMBLE.length() || !new String(segmentBytes, 0, JPEG_SEGMENT_PREAMBLE.length()).equals(JPEG_SEGMENT_PREAMBLE)) + continue; + extract(new ByteArrayReader(segmentBytes), metadata, JPEG_SEGMENT_PREAMBLE.length()); + } } - @NotNull - public Iterable<JpegSegmentType> getSegmentTypes() + /** Reads TIFF formatted Exif data from start of the specified {@link RandomAccessReader}. */ + public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata) { - return Arrays.asList(JpegSegmentType.APP1); + extract(reader, metadata, 0); } - public boolean canProcess(@NotNull final byte[] segmentBytes, @NotNull final JpegSegmentType segmentType) + /** Reads TIFF formatted Exif data a specified offset within a {@link RandomAccessReader}. */ + public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, int readerOffset) { - return segmentBytes.length >= JPEG_EXIF_SEGMENT_PREAMBLE.length() && new String(segmentBytes, 0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equalsIgnoreCase(JPEG_EXIF_SEGMENT_PREAMBLE); + extract(reader, metadata, readerOffset, null); } - public void extract(@NotNull final byte[] segmentBytes, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType) + /** Reads TIFF formatted Exif data at a specified offset within a {@link RandomAccessReader}. */ + public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, int readerOffset, @Nullable Directory parentDirectory) { - if (segmentBytes == null) - throw new NullPointerException("segmentBytes cannot be null"); - if (metadata == null) - throw new NullPointerException("metadata cannot be null"); - if (segmentType == null) - throw new NullPointerException("segmentType cannot be null"); + ExifTiffHandler exifTiffHandler = new ExifTiffHandler(metadata, parentDirectory); try { - ByteArrayReader reader = new ByteArrayReader(segmentBytes); - - // - // Check for the header preamble - // - try { - if (!reader.getString(0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equals(JPEG_EXIF_SEGMENT_PREAMBLE)) { - // TODO what do to with this error state? - System.err.println("Invalid JPEG Exif segment preamble"); - return; - } - } catch (IOException e) { - // TODO what do to with this error state? - e.printStackTrace(System.err); - return; - } - - // // Read the TIFF-formatted Exif data - // new TiffReader().processTiff( reader, - new ExifTiffHandler(metadata, _storeThumbnailBytes), - JPEG_EXIF_SEGMENT_PREAMBLE.length() + exifTiffHandler, + readerOffset ); - } catch (TiffProcessingException e) { + exifTiffHandler.error("Exception processing TIFF data: " + e.getMessage()); // TODO what do to with this error state? e.printStackTrace(System.err); } catch (IOException e) { + exifTiffHandler.error("Exception processing TIFF data: " + e.getMessage()); // TODO what do to with this error state? e.printStackTrace(System.err); } diff --git a/Source/com/drew/metadata/exif/ExifSubIFDDescriptor.java b/Source/com/drew/metadata/exif/ExifSubIFDDescriptor.java index 46ca073..e00b171 100644 --- a/Source/com/drew/metadata/exif/ExifSubIFDDescriptor.java +++ b/Source/com/drew/metadata/exif/ExifSubIFDDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,816 +20,18 @@ */ package com.drew.metadata.exif; -import com.drew.imaging.PhotographicConversions; -import com.drew.lang.Rational; import com.drew.lang.annotations.NotNull; -import com.drew.lang.annotations.Nullable; -import com.drew.metadata.TagDescriptor; - -import java.io.UnsupportedEncodingException; -import java.text.DecimalFormat; -import java.util.HashMap; -import java.util.Map; - -import static com.drew.metadata.exif.ExifSubIFDDirectory.*; /** * Provides human-readable string representations of tag values stored in a {@link ExifSubIFDDirectory}. * * @author Drew Noakes https://drewnoakes.com */ -public class ExifSubIFDDescriptor extends TagDescriptor<ExifSubIFDDirectory> +@SuppressWarnings("WeakerAccess") +public class ExifSubIFDDescriptor extends ExifDescriptorBase<ExifSubIFDDirectory> { - /** - * Dictates whether rational values will be represented in decimal format in instances - * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3). - */ - private final boolean _allowDecimalRepresentationOfRationals = true; - - @NotNull - private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#"); - public ExifSubIFDDescriptor(@NotNull ExifSubIFDDirectory directory) { super(directory); } - - // Note for the potential addition of brightness presentation in eV: - // Brightness of taken subject. To calculate Exposure(Ev) from BrightnessValue(Bv), - // you must add SensitivityValue(Sv). - // Ev=BV+Sv Sv=log2(ISOSpeedRating/3.125) - // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32. - - /** - * Returns a descriptive value of the specified tag for this image. - * Where possible, known values will be substituted here in place of the raw - * tokens actually kept in the Exif segment. If no substitution is - * available, the value provided by getString(int) will be returned. - * - * @param tagType the tag to find a description for - * @return a description of the image's value for the specified tag, or - * <code>null</code> if the tag hasn't been defined. - */ - @Override - @Nullable - public String getDescription(int tagType) - { - switch (tagType) { - case TAG_NEW_SUBFILE_TYPE: - return getNewSubfileTypeDescription(); - case TAG_SUBFILE_TYPE: - return getSubfileTypeDescription(); - case TAG_THRESHOLDING: - return getThresholdingDescription(); - case TAG_FILL_ORDER: - return getFillOrderDescription(); - case TAG_EXPOSURE_TIME: - return getExposureTimeDescription(); - case TAG_SHUTTER_SPEED: - return getShutterSpeedDescription(); - case TAG_FNUMBER: - return getFNumberDescription(); - case TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL: - return getCompressedAverageBitsPerPixelDescription(); - case TAG_SUBJECT_DISTANCE: - return getSubjectDistanceDescription(); - case TAG_METERING_MODE: - return getMeteringModeDescription(); - case TAG_WHITE_BALANCE: - return getWhiteBalanceDescription(); - case TAG_FLASH: - return getFlashDescription(); - case TAG_FOCAL_LENGTH: - return getFocalLengthDescription(); - case TAG_COLOR_SPACE: - return getColorSpaceDescription(); - case TAG_EXIF_IMAGE_WIDTH: - return getExifImageWidthDescription(); - case TAG_EXIF_IMAGE_HEIGHT: - return getExifImageHeightDescription(); - case TAG_FOCAL_PLANE_RESOLUTION_UNIT: - return getFocalPlaneResolutionUnitDescription(); - case TAG_FOCAL_PLANE_X_RESOLUTION: - return getFocalPlaneXResolutionDescription(); - case TAG_FOCAL_PLANE_Y_RESOLUTION: - return getFocalPlaneYResolutionDescription(); - case TAG_BITS_PER_SAMPLE: - return getBitsPerSampleDescription(); - case TAG_PHOTOMETRIC_INTERPRETATION: - return getPhotometricInterpretationDescription(); - case TAG_ROWS_PER_STRIP: - return getRowsPerStripDescription(); - case TAG_STRIP_BYTE_COUNTS: - return getStripByteCountsDescription(); - case TAG_SAMPLES_PER_PIXEL: - return getSamplesPerPixelDescription(); - case TAG_PLANAR_CONFIGURATION: - return getPlanarConfigurationDescription(); - case TAG_YCBCR_SUBSAMPLING: - return getYCbCrSubsamplingDescription(); - case TAG_EXPOSURE_PROGRAM: - return getExposureProgramDescription(); - case TAG_APERTURE: - return getApertureValueDescription(); - case TAG_MAX_APERTURE: - return getMaxApertureValueDescription(); - case TAG_SENSING_METHOD: - return getSensingMethodDescription(); - case TAG_EXPOSURE_BIAS: - return getExposureBiasDescription(); - case TAG_FILE_SOURCE: - return getFileSourceDescription(); - case TAG_SCENE_TYPE: - return getSceneTypeDescription(); - case TAG_COMPONENTS_CONFIGURATION: - return getComponentConfigurationDescription(); - case TAG_EXIF_VERSION: - return getExifVersionDescription(); - case TAG_FLASHPIX_VERSION: - return getFlashPixVersionDescription(); - case TAG_ISO_EQUIVALENT: - return getIsoEquivalentDescription(); - case TAG_USER_COMMENT: - return getUserCommentDescription(); - case TAG_CUSTOM_RENDERED: - return getCustomRenderedDescription(); - case TAG_EXPOSURE_MODE: - return getExposureModeDescription(); - case TAG_WHITE_BALANCE_MODE: - return getWhiteBalanceModeDescription(); - case TAG_DIGITAL_ZOOM_RATIO: - return getDigitalZoomRatioDescription(); - case TAG_35MM_FILM_EQUIV_FOCAL_LENGTH: - return get35mmFilmEquivFocalLengthDescription(); - case TAG_SCENE_CAPTURE_TYPE: - return getSceneCaptureTypeDescription(); - case TAG_GAIN_CONTROL: - return getGainControlDescription(); - case TAG_CONTRAST: - return getContrastDescription(); - case TAG_SATURATION: - return getSaturationDescription(); - case TAG_SHARPNESS: - return getSharpnessDescription(); - case TAG_SUBJECT_DISTANCE_RANGE: - return getSubjectDistanceRangeDescription(); - default: - return super.getDescription(tagType); - } - } - - @Nullable - public String getNewSubfileTypeDescription() - { - return getIndexedDescription(TAG_NEW_SUBFILE_TYPE, 1, - "Full-resolution image", - "Reduced-resolution image", - "Single page of multi-page reduced-resolution image", - "Transparency mask", - "Transparency mask of reduced-resolution image", - "Transparency mask of multi-page image", - "Transparency mask of reduced-resolution multi-page image" - ); - } - - @Nullable - public String getSubfileTypeDescription() - { - return getIndexedDescription(TAG_SUBFILE_TYPE, 1, - "Full-resolution image", - "Reduced-resolution image", - "Single page of multi-page image" - ); - } - - @Nullable - public String getThresholdingDescription() - { - return getIndexedDescription(TAG_THRESHOLDING, 1, - "No dithering or halftoning", - "Ordered dither or halftone", - "Randomized dither" - ); - } - - @Nullable - public String getFillOrderDescription() - { - return getIndexedDescription(TAG_FILL_ORDER, 1, - "Normal", - "Reversed" - ); - } - - @Nullable - public String getSubjectDistanceRangeDescription() - { - return getIndexedDescription(TAG_SUBJECT_DISTANCE_RANGE, - "Unknown", - "Macro", - "Close view", - "Distant view" - ); - } - - @Nullable - public String getSharpnessDescription() - { - return getIndexedDescription(TAG_SHARPNESS, - "None", - "Low", - "Hard" - ); - } - - @Nullable - public String getSaturationDescription() - { - return getIndexedDescription(TAG_SATURATION, - "None", - "Low saturation", - "High saturation" - ); - } - - @Nullable - public String getContrastDescription() - { - return getIndexedDescription(TAG_CONTRAST, - "None", - "Soft", - "Hard" - ); - } - - @Nullable - public String getGainControlDescription() - { - return getIndexedDescription(TAG_GAIN_CONTROL, - "None", - "Low gain up", - "Low gain down", - "High gain up", - "High gain down" - ); - } - - @Nullable - public String getSceneCaptureTypeDescription() - { - return getIndexedDescription(TAG_SCENE_CAPTURE_TYPE, - "Standard", - "Landscape", - "Portrait", - "Night scene" - ); - } - - @Nullable - public String get35mmFilmEquivFocalLengthDescription() - { - Integer value = _directory.getInteger(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH); - return value == null - ? null - : value == 0 - ? "Unknown" - : SimpleDecimalFormatter.format(value) + "mm"; - } - - @Nullable - public String getDigitalZoomRatioDescription() - { - Rational value = _directory.getRational(TAG_DIGITAL_ZOOM_RATIO); - return value == null - ? null - : value.getNumerator() == 0 - ? "Digital zoom not used." - : SimpleDecimalFormatter.format(value.doubleValue()); - } - - @Nullable - public String getWhiteBalanceModeDescription() - { - return getIndexedDescription(TAG_WHITE_BALANCE_MODE, - "Auto white balance", - "Manual white balance" - ); - } - - @Nullable - public String getExposureModeDescription() - { - return getIndexedDescription(TAG_EXPOSURE_MODE, - "Auto exposure", - "Manual exposure", - "Auto bracket" - ); - } - - @Nullable - public String getCustomRenderedDescription() - { - return getIndexedDescription(TAG_CUSTOM_RENDERED, - "Normal process", - "Custom process" - ); - } - - @Nullable - public String getUserCommentDescription() - { - byte[] commentBytes = _directory.getByteArray(TAG_USER_COMMENT); - if (commentBytes == null) - return null; - if (commentBytes.length == 0) - return ""; - - final Map<String, String> encodingMap = new HashMap<String, String>(); - encodingMap.put("ASCII", System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1". - encodingMap.put("UNICODE", "UTF-16LE"); - encodingMap.put("JIS", "Shift-JIS"); // We assume this charset for now. Another suggestion is "JIS". - - try { - if (commentBytes.length >= 10) { - String firstTenBytesString = new String(commentBytes, 0, 10); - - // try each encoding name - for (Map.Entry<String, String> pair : encodingMap.entrySet()) { - String encodingName = pair.getKey(); - String charset = pair.getValue(); - if (firstTenBytesString.startsWith(encodingName)) { - // skip any null or blank characters commonly present after the encoding name, up to a limit of 10 from the start - for (int j = encodingName.length(); j < 10; j++) { - byte b = commentBytes[j]; - if (b != '\0' && b != ' ') - return new String(commentBytes, j, commentBytes.length - j, charset).trim(); - } - return new String(commentBytes, 10, commentBytes.length - 10, charset).trim(); - } - } - } - // special handling fell through, return a plain string representation - return new String(commentBytes, System.getProperty("file.encoding")).trim(); - } catch (UnsupportedEncodingException ex) { - return null; - } - } - - @Nullable - public String getIsoEquivalentDescription() - { - // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values - Integer isoEquiv = _directory.getInteger(TAG_ISO_EQUIVALENT); - // There used to be a check here that multiplied ISO values < 50 by 200. - // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40. - return isoEquiv != null - ? Integer.toString(isoEquiv) - : null; - } - - @Nullable - public String getExifVersionDescription() - { - return getVersionBytesDescription(TAG_EXIF_VERSION, 2); - } - - @Nullable - public String getFlashPixVersionDescription() - { - return getVersionBytesDescription(TAG_FLASHPIX_VERSION, 2); - } - - @Nullable - public String getSceneTypeDescription() - { - return getIndexedDescription(TAG_SCENE_TYPE, - 1, - "Directly photographed image" - ); - } - - @Nullable - public String getFileSourceDescription() - { - return getIndexedDescription(TAG_FILE_SOURCE, - 1, - "Film Scanner", - "Reflection Print Scanner", - "Digital Still Camera (DSC)" - ); - } - - @Nullable - public String getExposureBiasDescription() - { - Rational value = _directory.getRational(TAG_EXPOSURE_BIAS); - if (value == null) - return null; - return value.toSimpleString(true) + " EV"; - } - - @Nullable - public String getMaxApertureValueDescription() - { - Double aperture = _directory.getDoubleObject(TAG_MAX_APERTURE); - if (aperture == null) - return null; - double fStop = PhotographicConversions.apertureToFStop(aperture); - return "F" + SimpleDecimalFormatter.format(fStop); - } - - @Nullable - public String getApertureValueDescription() - { - Double aperture = _directory.getDoubleObject(TAG_APERTURE); - if (aperture == null) - return null; - double fStop = PhotographicConversions.apertureToFStop(aperture); - return "F" + SimpleDecimalFormatter.format(fStop); - } - - @Nullable - public String getExposureProgramDescription() - { - return getIndexedDescription(TAG_EXPOSURE_PROGRAM, - 1, - "Manual control", - "Program normal", - "Aperture priority", - "Shutter priority", - "Program creative (slow program)", - "Program action (high-speed program)", - "Portrait mode", - "Landscape mode" - ); - } - - @Nullable - public String getYCbCrSubsamplingDescription() - { - int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING); - if (positions == null) - return null; - if (positions[0] == 2 && positions[1] == 1) { - return "YCbCr4:2:2"; - } else if (positions[0] == 2 && positions[1] == 2) { - return "YCbCr4:2:0"; - } else { - return "(Unknown)"; - } - } - - @Nullable - public String getPlanarConfigurationDescription() - { - // When image format is no compression YCbCr, this value shows byte aligns of YCbCr - // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling - // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr - // plane format. - return getIndexedDescription(TAG_PLANAR_CONFIGURATION, - 1, - "Chunky (contiguous for each subsampling pixel)", - "Separate (Y-plane/Cb-plane/Cr-plane format)" - ); - } - - @Nullable - public String getSamplesPerPixelDescription() - { - String value = _directory.getString(TAG_SAMPLES_PER_PIXEL); - return value == null ? null : value + " samples/pixel"; - } - - @Nullable - public String getRowsPerStripDescription() - { - final String value = _directory.getString(TAG_ROWS_PER_STRIP); - return value == null ? null : value + " rows/strip"; - } - - @Nullable - public String getStripByteCountsDescription() - { - final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS); - return value == null ? null : value + " bytes"; - } - - @Nullable - public String getPhotometricInterpretationDescription() - { - // Shows the color space of the image data components - Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION); - if (value == null) - return null; - switch (value) { - case 0: return "WhiteIsZero"; - case 1: return "BlackIsZero"; - case 2: return "RGB"; - case 3: return "RGB Palette"; - case 4: return "Transparency Mask"; - case 5: return "CMYK"; - case 6: return "YCbCr"; - case 8: return "CIELab"; - case 9: return "ICCLab"; - case 10: return "ITULab"; - case 32803: return "Color Filter Array"; - case 32844: return "Pixar LogL"; - case 32845: return "Pixar LogLuv"; - case 32892: return "Linear Raw"; - default: - return "Unknown colour space"; - } - } - - @Nullable - public String getBitsPerSampleDescription() - { - String value = _directory.getString(TAG_BITS_PER_SAMPLE); - return value == null ? null : value + " bits/component/pixel"; - } - - @Nullable - public String getFocalPlaneXResolutionDescription() - { - Rational rational = _directory.getRational(TAG_FOCAL_PLANE_X_RESOLUTION); - if (rational == null) - return null; - final String unit = getFocalPlaneResolutionUnitDescription(); - return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) - + (unit == null ? "" : " " + unit.toLowerCase()); - } - - @Nullable - public String getFocalPlaneYResolutionDescription() - { - Rational rational = _directory.getRational(TAG_FOCAL_PLANE_Y_RESOLUTION); - if (rational == null) - return null; - final String unit = getFocalPlaneResolutionUnitDescription(); - return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) - + (unit == null ? "" : " " + unit.toLowerCase()); - } - - @Nullable - public String getFocalPlaneResolutionUnitDescription() - { - // Unit of FocalPlaneXResolution/FocalPlaneYResolution. - // '1' means no-unit, '2' inch, '3' centimeter. - return getIndexedDescription(TAG_FOCAL_PLANE_RESOLUTION_UNIT, - 1, - "(No unit)", - "Inches", - "cm" - ); - } - - @Nullable - public String getExifImageWidthDescription() - { - final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_WIDTH); - return value == null ? null : value + " pixels"; - } - - @Nullable - public String getExifImageHeightDescription() - { - final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_HEIGHT); - return value == null ? null : value + " pixels"; - } - - @Nullable - public String getColorSpaceDescription() - { - final Integer value = _directory.getInteger(TAG_COLOR_SPACE); - if (value == null) - return null; - if (value == 1) - return "sRGB"; - if (value == 65535) - return "Undefined"; - return "Unknown (" + value + ")"; - } - - @Nullable - public String getFocalLengthDescription() - { - Rational value = _directory.getRational(TAG_FOCAL_LENGTH); - if (value == null) - return null; - java.text.DecimalFormat formatter = new DecimalFormat("0.0##"); - return formatter.format(value.doubleValue()) + " mm"; - } - - @Nullable - public String getFlashDescription() - { - /* - * This is a bit mask. - * 0 = flash fired - * 1 = return detected - * 2 = return able to be detected - * 3 = unknown - * 4 = auto used - * 5 = unknown - * 6 = red eye reduction used - */ - - final Integer value = _directory.getInteger(TAG_FLASH); - - if (value == null) - return null; - - StringBuilder sb = new StringBuilder(); - - if ((value & 0x1) != 0) - sb.append("Flash fired"); - else - sb.append("Flash did not fire"); - - // check if we're able to detect a return, before we mention it - if ((value & 0x4) != 0) { - if ((value & 0x2) != 0) - sb.append(", return detected"); - else - sb.append(", return not detected"); - } - - if ((value & 0x10) != 0) - sb.append(", auto"); - - if ((value & 0x40) != 0) - sb.append(", red-eye reduction"); - - return sb.toString(); - } - - @Nullable - public String getWhiteBalanceDescription() - { - // '0' means unknown, '1' daylight, '2' fluorescent, '3' tungsten, '10' flash, - // '17' standard light A, '18' standard light B, '19' standard light C, '20' D55, - // '21' D65, '22' D75, '255' other. - final Integer value = _directory.getInteger(TAG_WHITE_BALANCE); - if (value == null) - return null; - switch (value) { - case 0: return "Unknown"; - case 1: return "Daylight"; - case 2: return "Florescent"; - case 3: return "Tungsten"; - case 10: return "Flash"; - case 17: return "Standard light"; - case 18: return "Standard light (B)"; - case 19: return "Standard light (C)"; - case 20: return "D55"; - case 21: return "D65"; - case 22: return "D75"; - case 255: return "(Other)"; - default: - return "Unknown (" + value + ")"; - } - } - - @Nullable - public String getMeteringModeDescription() - { - // '0' means unknown, '1' average, '2' center weighted average, '3' spot - // '4' multi-spot, '5' multi-segment, '6' partial, '255' other - Integer value = _directory.getInteger(TAG_METERING_MODE); - if (value == null) - return null; - switch (value) { - case 0: return "Unknown"; - case 1: return "Average"; - case 2: return "Center weighted average"; - case 3: return "Spot"; - case 4: return "Multi-spot"; - case 5: return "Multi-segment"; - case 6: return "Partial"; - case 255: return "(Other)"; - default: - return ""; - } - } - - @Nullable - public String getSubjectDistanceDescription() - { - Rational value = _directory.getRational(TAG_SUBJECT_DISTANCE); - if (value == null) - return null; - java.text.DecimalFormat formatter = new DecimalFormat("0.0##"); - return formatter.format(value.doubleValue()) + " metres"; - } - - @Nullable - public String getCompressedAverageBitsPerPixelDescription() - { - Rational value = _directory.getRational(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL); - if (value == null) - return null; - String ratio = value.toSimpleString(_allowDecimalRepresentationOfRationals); - return value.isInteger() && value.intValue() == 1 - ? ratio + " bit/pixel" - : ratio + " bits/pixel"; - } - - @Nullable - public String getExposureTimeDescription() - { - String value = _directory.getString(TAG_EXPOSURE_TIME); - return value == null ? null : value + " sec"; - } - - @Nullable - public String getShutterSpeedDescription() - { - // I believe this method to now be stable, but am leaving some alternative snippets of - // code in here, to assist anyone who's looking into this (given that I don't have a public CVS). - -// float apexValue = _directory.getFloat(ExifSubIFDDirectory.TAG_SHUTTER_SPEED); -// int apexPower = (int)Math.pow(2.0, apexValue); -// return "1/" + apexPower + " sec"; - // TODO test this method - // thanks to Mark Edwards for spotting and patching a bug in the calculation of this - // description (spotted bug using a Canon EOS 300D) - // thanks also to Gli Blr for spotting this bug - Float apexValue = _directory.getFloatObject(TAG_SHUTTER_SPEED); - if (apexValue == null) - return null; - if (apexValue <= 1) { - float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2)))); - long apexPower10 = Math.round((double)apexPower * 10.0); - float fApexPower = (float)apexPower10 / 10.0f; - return fApexPower + " sec"; - } else { - int apexPower = (int)((Math.exp(apexValue * Math.log(2)))); - return "1/" + apexPower + " sec"; - } - -/* - // This alternative implementation offered by Bill Richards - // TODO determine which is the correct / more-correct implementation - double apexValue = _directory.getDouble(ExifSubIFDDirectory.TAG_SHUTTER_SPEED); - double apexPower = Math.pow(2.0, apexValue); - - StringBuffer sb = new StringBuffer(); - if (apexPower > 1) - apexPower = Math.floor(apexPower); - - if (apexPower < 1) { - sb.append((int)Math.round(1/apexPower)); - } else { - sb.append("1/"); - sb.append((int)apexPower); - } - sb.append(" sec"); - return sb.toString(); -*/ - } - - @Nullable - public String getFNumberDescription() - { - Rational value = _directory.getRational(TAG_FNUMBER); - if (value == null) - return null; - return "F" + SimpleDecimalFormatter.format(value.doubleValue()); - } - - @Nullable - public String getSensingMethodDescription() - { - // '1' Not defined, '2' One-chip color area sensor, '3' Two-chip color area sensor - // '4' Three-chip color area sensor, '5' Color sequential area sensor - // '7' Trilinear sensor '8' Color sequential linear sensor, 'Other' reserved - return getIndexedDescription(TAG_SENSING_METHOD, - 1, - "(Not defined)", - "One-chip color area sensor", - "Two-chip color area sensor", - "Three-chip color area sensor", - "Color sequential area sensor", - null, - "Trilinear sensor", - "Color sequential linear sensor" - ); - } - - @Nullable - public String getComponentConfigurationDescription() - { - int[] components = _directory.getIntArray(TAG_COMPONENTS_CONFIGURATION); - if (components == null) - return null; - String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"}; - StringBuilder componentConfig = new StringBuilder(); - for (int i = 0; i < Math.min(4, components.length); i++) { - int j = components[i]; - if (j > 0 && j < componentStrings.length) { - componentConfig.append(componentStrings[j]); - } - } - return componentConfig.toString(); - } } diff --git a/Source/com/drew/metadata/exif/ExifSubIFDDirectory.java b/Source/com/drew/metadata/exif/ExifSubIFDDirectory.java index 92b0944..a1c7319 100644 --- a/Source/com/drew/metadata/exif/ExifSubIFDDirectory.java +++ b/Source/com/drew/metadata/exif/ExifSubIFDDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,632 +21,34 @@ package com.drew.metadata.exif; import com.drew.lang.annotations.NotNull; -import com.drew.metadata.Directory; +import com.drew.lang.annotations.Nullable; +import java.util.Date; import java.util.HashMap; +import java.util.TimeZone; /** * Describes Exif tags from the SubIFD directory. * * @author Drew Noakes https://drewnoakes.com */ -public class ExifSubIFDDirectory extends Directory +@SuppressWarnings("WeakerAccess") +public class ExifSubIFDDirectory extends ExifDirectoryBase { - /** - * The actual aperture value of lens when the image was taken. Unit is APEX. - * To convert this value to ordinary F-number (F-stop), calculate this value's - * power of root 2 (=1.4142). For example, if the ApertureValue is '5', - * F-number is 1.4142^5 = F5.6. - */ - public static final int TAG_APERTURE = 0x9202; - /** - * When image format is no compression, this value shows the number of bits - * per component for each pixel. Usually this value is '8,8,8'. - */ - public static final int TAG_BITS_PER_SAMPLE = 0x0102; - - /** - * Shows the color space of the image data components. - * 0 = WhiteIsZero - * 1 = BlackIsZero - * 2 = RGB - * 3 = RGB Palette - * 4 = Transparency Mask - * 5 = CMYK - * 6 = YCbCr - * 8 = CIELab - * 9 = ICCLab - * 10 = ITULab - * 32803 = Color Filter Array - * 32844 = Pixar LogL - * 32845 = Pixar LogLuv - * 34892 = Linear Raw - */ - public static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106; - - /** - * 1 = No dithering or halftoning - * 2 = Ordered dither or halftone - * 3 = Randomized dither - */ - public static final int TAG_THRESHOLDING = 0x0107; - - /** - * 1 = Normal - * 2 = Reversed - */ - public static final int TAG_FILL_ORDER = 0x010A; - public static final int TAG_DOCUMENT_NAME = 0x010D; - - /** The position in the file of raster data. */ - public static final int TAG_STRIP_OFFSETS = 0x0111; - /** Each pixel is composed of this many samples. */ - public static final int TAG_SAMPLES_PER_PIXEL = 0x0115; - /** The raster is codified by a single block of data holding this many rows. */ - public static final int TAG_ROWS_PER_STRIP = 0x116; - /** The size of the raster data in bytes. */ - public static final int TAG_STRIP_BYTE_COUNTS = 0x0117; - public static final int TAG_MIN_SAMPLE_VALUE = 0x0118; - public static final int TAG_MAX_SAMPLE_VALUE = 0x0119; - /** - * When image format is no compression YCbCr, this value shows byte aligns of - * YCbCr data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for - * each subsampling pixel. If value is '2', Y/Cb/Cr value is separated and - * stored to Y plane/Cb plane/Cr plane format. - */ - public static final int TAG_PLANAR_CONFIGURATION = 0x011C; - public static final int TAG_YCBCR_SUBSAMPLING = 0x0212; - - /** - * The new subfile type tag. - * 0 = Full-resolution Image - * 1 = Reduced-resolution image - * 2 = Single page of multi-page image - * 3 = Single page of multi-page reduced-resolution image - * 4 = Transparency mask - * 5 = Transparency mask of reduced-resolution image - * 6 = Transparency mask of multi-page image - * 7 = Transparency mask of reduced-resolution multi-page image - */ - public static final int TAG_NEW_SUBFILE_TYPE = 0x00FE; - /** - * The old subfile type tag. - * 1 = Full-resolution image (Main image) - * 2 = Reduced-resolution image (Thumbnail) - * 3 = Single page of multi-page image - */ - public static final int TAG_SUBFILE_TYPE = 0x00FF; - public static final int TAG_TRANSFER_FUNCTION = 0x012D; - public static final int TAG_PREDICTOR = 0x013D; - public static final int TAG_TILE_WIDTH = 0x0142; - public static final int TAG_TILE_LENGTH = 0x0143; - public static final int TAG_TILE_OFFSETS = 0x0144; - public static final int TAG_TILE_BYTE_COUNTS = 0x0145; - public static final int TAG_JPEG_TABLES = 0x015B; - public static final int TAG_CFA_REPEAT_PATTERN_DIM = 0x828D; - /** There are two definitions for CFA pattern, I don't know the difference... */ - public static final int TAG_CFA_PATTERN_2 = 0x828E; - public static final int TAG_BATTERY_LEVEL = 0x828F; - public static final int TAG_IPTC_NAA = 0x83BB; - public static final int TAG_INTER_COLOR_PROFILE = 0x8773; - public static final int TAG_SPECTRAL_SENSITIVITY = 0x8824; - /** - * Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524. - * <p> - * OECF is the relationship between the camera optical input and the image values. - * <p> - * The values are: - * <ul> - * <li>Two shorts, indicating respectively number of columns, and number of rows.</li> - * <li>For each column, the column name in a null-terminated ASCII string.</li> - * <li>For each cell, an SRATIONAL value.</li> - * </ul> - */ - public static final int TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION = 0x8828; - public static final int TAG_INTERLACE = 0x8829; - public static final int TAG_TIME_ZONE_OFFSET = 0x882A; - public static final int TAG_SELF_TIMER_MODE = 0x882B; - public static final int TAG_FLASH_ENERGY = 0x920B; - public static final int TAG_SPATIAL_FREQ_RESPONSE = 0x920C; - public static final int TAG_NOISE = 0x920D; - public static final int TAG_IMAGE_NUMBER = 0x9211; - public static final int TAG_SECURITY_CLASSIFICATION = 0x9212; - public static final int TAG_IMAGE_HISTORY = 0x9213; - public static final int TAG_SUBJECT_LOCATION = 0x9214; - /** There are two definitions for exposure index, I don't know the difference... */ - public static final int TAG_EXPOSURE_INDEX_2 = 0x9215; - public static final int TAG_TIFF_EP_STANDARD_ID = 0x9216; - public static final int TAG_FLASH_ENERGY_2 = 0xA20B; - public static final int TAG_SPATIAL_FREQ_RESPONSE_2 = 0xA20C; - public static final int TAG_SUBJECT_LOCATION_2 = 0xA214; - public static final int TAG_PAGE_NAME = 0x011D; - /** - * Exposure time (reciprocal of shutter speed). Unit is second. - */ - public static final int TAG_EXPOSURE_TIME = 0x829A; - /** - * The actual F-number(F-stop) of lens when the image was taken. - */ - public static final int TAG_FNUMBER = 0x829D; - /** - * Exposure program that the camera used when image was taken. '1' means - * manual control, '2' program normal, '3' aperture priority, '4' shutter - * priority, '5' program creative (slow program), '6' program action - * (high-speed program), '7' portrait mode, '8' landscape mode. - */ - public static final int TAG_EXPOSURE_PROGRAM = 0x8822; - public static final int TAG_ISO_EQUIVALENT = 0x8827; - public static final int TAG_EXIF_VERSION = 0x9000; - public static final int TAG_DATETIME_ORIGINAL = 0x9003; - public static final int TAG_DATETIME_DIGITIZED = 0x9004; - public static final int TAG_COMPONENTS_CONFIGURATION = 0x9101; - /** - * Average (rough estimate) compression level in JPEG bits per pixel. - * */ - public static final int TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL = 0x9102; - /** - * Shutter speed by APEX value. To convert this value to ordinary 'Shutter Speed'; - * calculate this value's power of 2, then reciprocal. For example, if the - * ShutterSpeedValue is '4', shutter speed is 1/(24)=1/16 second. - */ - public static final int TAG_SHUTTER_SPEED = 0x9201; - public static final int TAG_BRIGHTNESS_VALUE = 0x9203; - public static final int TAG_EXPOSURE_BIAS = 0x9204; - /** - * Maximum aperture value of lens. You can convert to F-number by calculating - * power of root 2 (same process of ApertureValue:0x9202). - * The actual aperture value of lens when the image was taken. To convert this - * value to ordinary f-number(f-stop), calculate the value's power of root 2 - * (=1.4142). For example, if the ApertureValue is '5', f-number is 1.41425^5 = F5.6. - */ - public static final int TAG_MAX_APERTURE = 0x9205; - /** - * Indicates the distance the autofocus camera is focused to. Tends to be less accurate as distance increases. - */ - public static final int TAG_SUBJECT_DISTANCE = 0x9206; - /** - * Exposure metering method. '0' means unknown, '1' average, '2' center - * weighted average, '3' spot, '4' multi-spot, '5' multi-segment, '6' partial, - * '255' other. - */ - public static final int TAG_METERING_MODE = 0x9207; - - public static final int TAG_LIGHT_SOURCE = 0x9208; - /** - * White balance (aka light source). '0' means unknown, '1' daylight, - * '2' fluorescent, '3' tungsten, '10' flash, '17' standard light A, - * '18' standard light B, '19' standard light C, '20' D55, '21' D65, - * '22' D75, '255' other. - */ - public static final int TAG_WHITE_BALANCE = 0x9208; - /** - * 0x0 = 0000000 = No Flash - * 0x1 = 0000001 = Fired - * 0x5 = 0000101 = Fired, Return not detected - * 0x7 = 0000111 = Fired, Return detected - * 0x9 = 0001001 = On - * 0xd = 0001101 = On, Return not detected - * 0xf = 0001111 = On, Return detected - * 0x10 = 0010000 = Off - * 0x18 = 0011000 = Auto, Did not fire - * 0x19 = 0011001 = Auto, Fired - * 0x1d = 0011101 = Auto, Fired, Return not detected - * 0x1f = 0011111 = Auto, Fired, Return detected - * 0x20 = 0100000 = No flash function - * 0x41 = 1000001 = Fired, Red-eye reduction - * 0x45 = 1000101 = Fired, Red-eye reduction, Return not detected - * 0x47 = 1000111 = Fired, Red-eye reduction, Return detected - * 0x49 = 1001001 = On, Red-eye reduction - * 0x4d = 1001101 = On, Red-eye reduction, Return not detected - * 0x4f = 1001111 = On, Red-eye reduction, Return detected - * 0x59 = 1011001 = Auto, Fired, Red-eye reduction - * 0x5d = 1011101 = Auto, Fired, Red-eye reduction, Return not detected - * 0x5f = 1011111 = Auto, Fired, Red-eye reduction, Return detected - * 6543210 (positions) - * - * This is a bitmask. - * 0 = flash fired - * 1 = return detected - * 2 = return able to be detected - * 3 = unknown - * 4 = auto used - * 5 = unknown - * 6 = red eye reduction used - */ - public static final int TAG_FLASH = 0x9209; - /** - * Focal length of lens used to take image. Unit is millimeter. - * Nice digital cameras actually save the focal length as a function of how far they are zoomed in. - */ - public static final int TAG_FOCAL_LENGTH = 0x920A; - - /** - * This tag holds the Exif Makernote. Makernotes are free to be in any format, though they are often IFDs. - * To determine the format, we consider the starting bytes of the makernote itself and sometimes the - * camera model and make. - * <p> - * The component count for this tag includes all of the bytes needed for the makernote. - */ - public static final int TAG_MAKERNOTE = 0x927C; - - public static final int TAG_USER_COMMENT = 0x9286; - - public static final int TAG_SUBSECOND_TIME = 0x9290; - public static final int TAG_SUBSECOND_TIME_ORIGINAL = 0x9291; - public static final int TAG_SUBSECOND_TIME_DIGITIZED = 0x9292; - - public static final int TAG_FLASHPIX_VERSION = 0xA000; - /** - * Defines Color Space. DCF image must use sRGB color space so value is - * always '1'. If the picture uses the other color space, value is - * '65535':Uncalibrated. - */ - public static final int TAG_COLOR_SPACE = 0xA001; - public static final int TAG_EXIF_IMAGE_WIDTH = 0xA002; - public static final int TAG_EXIF_IMAGE_HEIGHT = 0xA003; - public static final int TAG_RELATED_SOUND_FILE = 0xA004; - /** This tag is a pointer to the Exif Interop IFD. */ public static final int TAG_INTEROP_OFFSET = 0xA005; - public static final int TAG_FOCAL_PLANE_X_RESOLUTION = 0xA20E; - public static final int TAG_FOCAL_PLANE_Y_RESOLUTION = 0xA20F; - /** - * Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit, - * '2' inch, '3' centimeter. - * - * Note: Some of Fujifilm's digicam(e.g.FX2700,FX2900,Finepix4700Z/40i etc) - * uses value '3' so it must be 'centimeter', but it seems that they use a - * '8.3mm?'(1/3in.?) to their ResolutionUnit. Fuji's BUG? Finepix4900Z has - * been changed to use value '2' but it doesn't match to actual value also. - */ - public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT = 0xA210; - public static final int TAG_EXPOSURE_INDEX = 0xA215; - public static final int TAG_SENSING_METHOD = 0xA217; - public static final int TAG_FILE_SOURCE = 0xA300; - public static final int TAG_SCENE_TYPE = 0xA301; - public static final int TAG_CFA_PATTERN = 0xA302; - - // these tags new with Exif 2.2 (?) [A401 - A4 - /** - * This tag indicates the use of special processing on image data, such as rendering - * geared to output. When special processing is performed, the reader is expected to - * disable or minimize any further processing. - * Tag = 41985 (A401.H) - * Type = SHORT - * Count = 1 - * Default = 0 - * 0 = Normal process - * 1 = Custom process - * Other = reserved - */ - public static final int TAG_CUSTOM_RENDERED = 0xA401; - - /** - * This tag indicates the exposure mode set when the image was shot. In auto-bracketing - * mode, the camera shoots a series of frames of the same scene at different exposure settings. - * Tag = 41986 (A402.H) - * Type = SHORT - * Count = 1 - * Default = none - * 0 = Auto exposure - * 1 = Manual exposure - * 2 = Auto bracket - * Other = reserved - */ - public static final int TAG_EXPOSURE_MODE = 0xA402; - - /** - * This tag indicates the white balance mode set when the image was shot. - * Tag = 41987 (A403.H) - * Type = SHORT - * Count = 1 - * Default = none - * 0 = Auto white balance - * 1 = Manual white balance - * Other = reserved - */ - public static final int TAG_WHITE_BALANCE_MODE = 0xA403; - - /** - * This tag indicates the digital zoom ratio when the image was shot. If the - * numerator of the recorded value is 0, this indicates that digital zoom was - * not used. - * Tag = 41988 (A404.H) - * Type = RATIONAL - * Count = 1 - * Default = none - */ - public static final int TAG_DIGITAL_ZOOM_RATIO = 0xA404; - - /** - * This tag indicates the equivalent focal length assuming a 35mm film camera, - * in mm. A value of 0 means the focal length is unknown. Note that this tag - * differs from the FocalLength tag. - * Tag = 41989 (A405.H) - * Type = SHORT - * Count = 1 - * Default = none - */ - public static final int TAG_35MM_FILM_EQUIV_FOCAL_LENGTH = 0xA405; - - /** - * This tag indicates the type of scene that was shot. It can also be used to - * record the mode in which the image was shot. Note that this differs from - * the scene type (SceneType) tag. - * Tag = 41990 (A406.H) - * Type = SHORT - * Count = 1 - * Default = 0 - * 0 = Standard - * 1 = Landscape - * 2 = Portrait - * 3 = Night scene - * Other = reserved - */ - public static final int TAG_SCENE_CAPTURE_TYPE = 0xA406; - - /** - * This tag indicates the degree of overall image gain adjustment. - * Tag = 41991 (A407.H) - * Type = SHORT - * Count = 1 - * Default = none - * 0 = None - * 1 = Low gain up - * 2 = High gain up - * 3 = Low gain down - * 4 = High gain down - * Other = reserved - */ - public static final int TAG_GAIN_CONTROL = 0xA407; - - /** - * This tag indicates the direction of contrast processing applied by the camera - * when the image was shot. - * Tag = 41992 (A408.H) - * Type = SHORT - * Count = 1 - * Default = 0 - * 0 = Normal - * 1 = Soft - * 2 = Hard - * Other = reserved - */ - public static final int TAG_CONTRAST = 0xA408; - - /** - * This tag indicates the direction of saturation processing applied by the camera - * when the image was shot. - * Tag = 41993 (A409.H) - * Type = SHORT - * Count = 1 - * Default = 0 - * 0 = Normal - * 1 = Low saturation - * 2 = High saturation - * Other = reserved - */ - public static final int TAG_SATURATION = 0xA409; - - /** - * This tag indicates the direction of sharpness processing applied by the camera - * when the image was shot. - * Tag = 41994 (A40A.H) - * Type = SHORT - * Count = 1 - * Default = 0 - * 0 = Normal - * 1 = Soft - * 2 = Hard - * Other = reserved - */ - public static final int TAG_SHARPNESS = 0xA40A; - - // TODO support this tag (I haven't seen a camera's actual implementation of this yet) - - /** - * This tag indicates information on the picture-taking conditions of a particular - * camera model. The tag is used only to indicate the picture-taking conditions in - * the reader. - * Tag = 41995 (A40B.H) - * Type = UNDEFINED - * Count = Any - * Default = none - * - * The information is recorded in the format shown below. The data is recorded - * in Unicode using SHORT type for the number of display rows and columns and - * UNDEFINED type for the camera settings. The Unicode (UCS-2) string including - * Signature is NULL terminated. The specifics of the Unicode string are as given - * in ISO/IEC 10464-1. - * - * Length Type Meaning - * ------+-----------+------------------ - * 2 SHORT Display columns - * 2 SHORT Display rows - * Any UNDEFINED Camera setting-1 - * Any UNDEFINED Camera setting-2 - * : : : - * Any UNDEFINED Camera setting-n - */ - public static final int TAG_DEVICE_SETTING_DESCRIPTION = 0xA40B; - - /** - * This tag indicates the distance to the subject. - * Tag = 41996 (A40C.H) - * Type = SHORT - * Count = 1 - * Default = none - * 0 = unknown - * 1 = Macro - * 2 = Close view - * 3 = Distant view - * Other = reserved - */ - public static final int TAG_SUBJECT_DISTANCE_RANGE = 0xA40C; - - /** - * This tag indicates an identifier assigned uniquely to each image. It is - * recorded as an ASCII string equivalent to hexadecimal notation and 128-bit - * fixed length. - * Tag = 42016 (A420.H) - * Type = ASCII - * Count = 33 - * Default = none - */ - public static final int TAG_IMAGE_UNIQUE_ID = 0xA420; - - /** String. */ - public static final int TAG_CAMERA_OWNER_NAME = 0xA430; - /** String. */ - public static final int TAG_BODY_SERIAL_NUMBER = 0xA431; - /** An array of four Rational64u numbers giving focal and aperture ranges. */ - public static final int TAG_LENS_SPECIFICATION = 0xA432; - /** String. */ - public static final int TAG_LENS_MAKE = 0xA433; - /** String. */ - public static final int TAG_LENS_MODEL = 0xA434; - /** String. */ - public static final int TAG_LENS_SERIAL_NUMBER = 0xA435; - /** Rational64u. */ - public static final int TAG_GAMMA = 0xA500; - - public static final int TAG_LENS = 0xFDEA; + public ExifSubIFDDirectory() + { + this.setDescriptor(new ExifSubIFDDescriptor(this)); + } @NotNull protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); static { - _tagNameMap.put(TAG_FILL_ORDER, "Fill Order"); - _tagNameMap.put(TAG_DOCUMENT_NAME, "Document Name"); - // TODO why don't these tags have fields associated with them? - _tagNameMap.put(0x1000, "Related Image File Format"); - _tagNameMap.put(0x1001, "Related Image Width"); - _tagNameMap.put(0x1002, "Related Image Length"); - _tagNameMap.put(0x0156, "Transfer Range"); - _tagNameMap.put(0x0200, "JPEG Proc"); - _tagNameMap.put(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, "Compressed Bits Per Pixel"); - _tagNameMap.put(TAG_MAKERNOTE, "Makernote"); - _tagNameMap.put(TAG_INTEROP_OFFSET, "Interoperability Offset"); - - _tagNameMap.put(TAG_NEW_SUBFILE_TYPE, "New Subfile Type"); - _tagNameMap.put(TAG_SUBFILE_TYPE, "Subfile Type"); - _tagNameMap.put(TAG_BITS_PER_SAMPLE, "Bits Per Sample"); - _tagNameMap.put(TAG_PHOTOMETRIC_INTERPRETATION, "Photometric Interpretation"); - _tagNameMap.put(TAG_THRESHOLDING, "Thresholding"); - _tagNameMap.put(TAG_STRIP_OFFSETS, "Strip Offsets"); - _tagNameMap.put(TAG_SAMPLES_PER_PIXEL, "Samples Per Pixel"); - _tagNameMap.put(TAG_ROWS_PER_STRIP, "Rows Per Strip"); - _tagNameMap.put(TAG_STRIP_BYTE_COUNTS, "Strip Byte Counts"); - _tagNameMap.put(TAG_PAGE_NAME, "Page Name"); - _tagNameMap.put(TAG_PLANAR_CONFIGURATION, "Planar Configuration"); - _tagNameMap.put(TAG_TRANSFER_FUNCTION, "Transfer Function"); - _tagNameMap.put(TAG_PREDICTOR, "Predictor"); - _tagNameMap.put(TAG_TILE_WIDTH, "Tile Width"); - _tagNameMap.put(TAG_TILE_LENGTH, "Tile Length"); - _tagNameMap.put(TAG_TILE_OFFSETS, "Tile Offsets"); - _tagNameMap.put(TAG_TILE_BYTE_COUNTS, "Tile Byte Counts"); - _tagNameMap.put(TAG_JPEG_TABLES, "JPEG Tables"); - _tagNameMap.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling"); - _tagNameMap.put(TAG_CFA_REPEAT_PATTERN_DIM, "CFA Repeat Pattern Dim"); - _tagNameMap.put(TAG_CFA_PATTERN_2, "CFA Pattern"); - _tagNameMap.put(TAG_BATTERY_LEVEL, "Battery Level"); - _tagNameMap.put(TAG_EXPOSURE_TIME, "Exposure Time"); - _tagNameMap.put(TAG_FNUMBER, "F-Number"); - _tagNameMap.put(TAG_IPTC_NAA, "IPTC/NAA"); - _tagNameMap.put(TAG_INTER_COLOR_PROFILE, "Inter Color Profile"); - _tagNameMap.put(TAG_EXPOSURE_PROGRAM, "Exposure Program"); - _tagNameMap.put(TAG_SPECTRAL_SENSITIVITY, "Spectral Sensitivity"); - _tagNameMap.put(TAG_ISO_EQUIVALENT, "ISO Speed Ratings"); - _tagNameMap.put(TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION, "Opto-electric Conversion Function (OECF)"); - _tagNameMap.put(TAG_INTERLACE, "Interlace"); - _tagNameMap.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset"); - _tagNameMap.put(TAG_SELF_TIMER_MODE, "Self Timer Mode"); - _tagNameMap.put(TAG_EXIF_VERSION, "Exif Version"); - _tagNameMap.put(TAG_DATETIME_ORIGINAL, "Date/Time Original"); - _tagNameMap.put(TAG_DATETIME_DIGITIZED, "Date/Time Digitized"); - _tagNameMap.put(TAG_COMPONENTS_CONFIGURATION, "Components Configuration"); - _tagNameMap.put(TAG_SHUTTER_SPEED, "Shutter Speed Value"); - _tagNameMap.put(TAG_APERTURE, "Aperture Value"); - _tagNameMap.put(TAG_BRIGHTNESS_VALUE, "Brightness Value"); - _tagNameMap.put(TAG_EXPOSURE_BIAS, "Exposure Bias Value"); - _tagNameMap.put(TAG_MAX_APERTURE, "Max Aperture Value"); - _tagNameMap.put(TAG_SUBJECT_DISTANCE, "Subject Distance"); - _tagNameMap.put(TAG_METERING_MODE, "Metering Mode"); - _tagNameMap.put(TAG_LIGHT_SOURCE, "Light Source"); - _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance"); - _tagNameMap.put(TAG_FLASH, "Flash"); - _tagNameMap.put(TAG_FOCAL_LENGTH, "Focal Length"); - _tagNameMap.put(TAG_FLASH_ENERGY, "Flash Energy"); - _tagNameMap.put(TAG_SPATIAL_FREQ_RESPONSE, "Spatial Frequency Response"); - _tagNameMap.put(TAG_NOISE, "Noise"); - _tagNameMap.put(TAG_IMAGE_NUMBER, "Image Number"); - _tagNameMap.put(TAG_SECURITY_CLASSIFICATION, "Security Classification"); - _tagNameMap.put(TAG_IMAGE_HISTORY, "Image History"); - _tagNameMap.put(TAG_SUBJECT_LOCATION, "Subject Location"); - _tagNameMap.put(TAG_EXPOSURE_INDEX, "Exposure Index"); - _tagNameMap.put(TAG_TIFF_EP_STANDARD_ID, "TIFF/EP Standard ID"); - _tagNameMap.put(TAG_USER_COMMENT, "User Comment"); - _tagNameMap.put(TAG_SUBSECOND_TIME, "Sub-Sec Time"); - _tagNameMap.put(TAG_SUBSECOND_TIME_ORIGINAL, "Sub-Sec Time Original"); - _tagNameMap.put(TAG_SUBSECOND_TIME_DIGITIZED, "Sub-Sec Time Digitized"); - _tagNameMap.put(TAG_FLASHPIX_VERSION, "FlashPix Version"); - _tagNameMap.put(TAG_COLOR_SPACE, "Color Space"); - _tagNameMap.put(TAG_EXIF_IMAGE_WIDTH, "Exif Image Width"); - _tagNameMap.put(TAG_EXIF_IMAGE_HEIGHT, "Exif Image Height"); - _tagNameMap.put(TAG_RELATED_SOUND_FILE, "Related Sound File"); - // 0x920B in TIFF/EP - _tagNameMap.put(TAG_FLASH_ENERGY_2, "Flash Energy"); - // 0x920C in TIFF/EP - _tagNameMap.put(TAG_SPATIAL_FREQ_RESPONSE_2, "Spatial Frequency Response"); - // 0x920E in TIFF/EP - _tagNameMap.put(TAG_FOCAL_PLANE_X_RESOLUTION, "Focal Plane X Resolution"); - // 0x920F in TIFF/EP - _tagNameMap.put(TAG_FOCAL_PLANE_Y_RESOLUTION, "Focal Plane Y Resolution"); - // 0x9210 in TIFF/EP - _tagNameMap.put(TAG_FOCAL_PLANE_RESOLUTION_UNIT, "Focal Plane Resolution Unit"); - // 0x9214 in TIFF/EP - _tagNameMap.put(TAG_SUBJECT_LOCATION_2, "Subject Location"); - // 0x9215 in TIFF/EP - _tagNameMap.put(TAG_EXPOSURE_INDEX_2, "Exposure Index"); - // 0x9217 in TIFF/EP - _tagNameMap.put(TAG_SENSING_METHOD, "Sensing Method"); - _tagNameMap.put(TAG_FILE_SOURCE, "File Source"); - _tagNameMap.put(TAG_SCENE_TYPE, "Scene Type"); - _tagNameMap.put(TAG_CFA_PATTERN, "CFA Pattern"); - - _tagNameMap.put(TAG_CUSTOM_RENDERED, "Custom Rendered"); - _tagNameMap.put(TAG_EXPOSURE_MODE, "Exposure Mode"); - _tagNameMap.put(TAG_WHITE_BALANCE_MODE, "White Balance Mode"); - _tagNameMap.put(TAG_DIGITAL_ZOOM_RATIO, "Digital Zoom Ratio"); - _tagNameMap.put(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH, "Focal Length 35"); - _tagNameMap.put(TAG_SCENE_CAPTURE_TYPE, "Scene Capture Type"); - _tagNameMap.put(TAG_GAIN_CONTROL, "Gain Control"); - _tagNameMap.put(TAG_CONTRAST, "Contrast"); - _tagNameMap.put(TAG_SATURATION, "Saturation"); - _tagNameMap.put(TAG_SHARPNESS, "Sharpness"); - _tagNameMap.put(TAG_DEVICE_SETTING_DESCRIPTION, "Device Setting Description"); - _tagNameMap.put(TAG_SUBJECT_DISTANCE_RANGE, "Subject Distance Range"); - _tagNameMap.put(TAG_IMAGE_UNIQUE_ID, "Unique Image ID"); - - _tagNameMap.put(TAG_CAMERA_OWNER_NAME, "Camera Owner Name"); - _tagNameMap.put(TAG_BODY_SERIAL_NUMBER, "Body Serial Number"); - _tagNameMap.put(TAG_LENS_SPECIFICATION, "Lens Specification"); - _tagNameMap.put(TAG_LENS_MAKE, "Lens Make"); - _tagNameMap.put(TAG_LENS_MODEL, "Lens Model"); - _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number"); - _tagNameMap.put(TAG_GAMMA, "Gamma"); - - _tagNameMap.put(TAG_MIN_SAMPLE_VALUE, "Minimum sample value"); - _tagNameMap.put(TAG_MAX_SAMPLE_VALUE, "Maximum sample value"); - - _tagNameMap.put(TAG_LENS, "Lens"); - } - - public ExifSubIFDDirectory() - { - this.setDescriptor(new ExifSubIFDDescriptor(this)); + addExifTagNames(_tagNameMap); } @Override @@ -662,4 +64,60 @@ public class ExifSubIFDDirectory extends Directory { return _tagNameMap; } + + /** + * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds + * representing the date and time when this image was captured. Attempts will be made to parse the + * values as though it is in the GMT {@link TimeZone}. + * + * @return A Date object representing when this image was captured, if possible, otherwise null + */ + @Nullable + public Date getDateOriginal() + { + return getDateOriginal(null); + } + + /** + * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds + * representing the date and time when this image was captured. Attempts will be made to parse the + * values as though it is in the {@link TimeZone} represented by the {@code timeZone} parameter + * (if it is non-null). + * + * @param timeZone the time zone to use + * @return A Date object representing when this image was captured, if possible, otherwise null + */ + @Nullable + public Date getDateOriginal(@Nullable TimeZone timeZone) + { + return getDate(TAG_DATETIME_ORIGINAL, getString(TAG_SUBSECOND_TIME_ORIGINAL), timeZone); + } + + /** + * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds + * representing the date and time when this image was digitized. Attempts will be made to parse the + * values as though it is in the GMT {@link TimeZone}. + * + * @return A Date object representing when this image was digitized, if possible, otherwise null + */ + @Nullable + public Date getDateDigitized() + { + return getDateDigitized(null); + } + + /** + * Parses the date/time tag and the subsecond tag to obtain a single Date object with milliseconds + * representing the date and time when this image was digitized. Attempts will be made to parse the + * values as though it is in the {@link TimeZone} represented by the {@code timeZone} parameter + * (if it is non-null). + * + * @param timeZone the time zone to use + * @return A Date object representing when this image was digitized, if possible, otherwise null + */ + @Nullable + public Date getDateDigitized(@Nullable TimeZone timeZone) + { + return getDate(TAG_DATETIME_DIGITIZED, getString(TAG_SUBSECOND_TIME_DIGITIZED), timeZone); + } } diff --git a/Source/com/drew/metadata/exif/ExifThumbnailDescriptor.java b/Source/com/drew/metadata/exif/ExifThumbnailDescriptor.java index 0e36110..eb0014a 100644 --- a/Source/com/drew/metadata/exif/ExifThumbnailDescriptor.java +++ b/Source/com/drew/metadata/exif/ExifThumbnailDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,8 @@ package com.drew.metadata.exif; -import com.drew.lang.Rational; import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; -import com.drew.metadata.TagDescriptor; import static com.drew.metadata.exif.ExifThumbnailDirectory.*; @@ -33,233 +31,28 @@ import static com.drew.metadata.exif.ExifThumbnailDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ -public class ExifThumbnailDescriptor extends TagDescriptor<ExifThumbnailDirectory> +@SuppressWarnings("WeakerAccess") +public class ExifThumbnailDescriptor extends ExifDescriptorBase<ExifThumbnailDirectory> { - /** - * Dictates whether rational values will be represented in decimal format in instances - * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3). - */ - private final boolean _allowDecimalRepresentationOfRationals = true; - public ExifThumbnailDescriptor(@NotNull ExifThumbnailDirectory directory) { super(directory); } - // Note for the potential addition of brightness presentation in eV: - // Brightness of taken subject. To calculate Exposure(Ev) from BrightnessValue(Bv), - // you must add SensitivityValue(Sv). - // Ev=BV+Sv Sv=log2(ISOSpeedRating/3.125) - // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32. - - /** - * Returns a descriptive value of the specified tag for this image. - * Where possible, known values will be substituted here in place of the raw - * tokens actually kept in the Exif segment. If no substitution is - * available, the value provided by getString(int) will be returned. - * - * @param tagType the tag to find a description for - * @return a description of the image's value for the specified tag, or - * <code>null</code> if the tag hasn't been defined. - */ @Override @Nullable public String getDescription(int tagType) { switch (tagType) { - case TAG_ORIENTATION: - return getOrientationDescription(); - case TAG_RESOLUTION_UNIT: - return getResolutionDescription(); - case TAG_YCBCR_POSITIONING: - return getYCbCrPositioningDescription(); - case TAG_X_RESOLUTION: - return getXResolutionDescription(); - case TAG_Y_RESOLUTION: - return getYResolutionDescription(); case TAG_THUMBNAIL_OFFSET: return getThumbnailOffsetDescription(); case TAG_THUMBNAIL_LENGTH: return getThumbnailLengthDescription(); - case TAG_THUMBNAIL_IMAGE_WIDTH: - return getThumbnailImageWidthDescription(); - case TAG_THUMBNAIL_IMAGE_HEIGHT: - return getThumbnailImageHeightDescription(); - case TAG_BITS_PER_SAMPLE: - return getBitsPerSampleDescription(); - case TAG_THUMBNAIL_COMPRESSION: - return getCompressionDescription(); - case TAG_PHOTOMETRIC_INTERPRETATION: - return getPhotometricInterpretationDescription(); - case TAG_ROWS_PER_STRIP: - return getRowsPerStripDescription(); - case TAG_STRIP_BYTE_COUNTS: - return getStripByteCountsDescription(); - case TAG_SAMPLES_PER_PIXEL: - return getSamplesPerPixelDescription(); - case TAG_PLANAR_CONFIGURATION: - return getPlanarConfigurationDescription(); - case TAG_YCBCR_SUBSAMPLING: - return getYCbCrSubsamplingDescription(); - case TAG_REFERENCE_BLACK_WHITE: - return getReferenceBlackWhiteDescription(); default: return super.getDescription(tagType); } } - @Nullable - public String getReferenceBlackWhiteDescription() - { - int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE); - if (ints == null || ints.length < 6) - return null; - int blackR = ints[0]; - int whiteR = ints[1]; - int blackG = ints[2]; - int whiteG = ints[3]; - int blackB = ints[4]; - int whiteB = ints[5]; - return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB); - } - - @Nullable - public String getYCbCrSubsamplingDescription() - { - int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING); - if (positions == null || positions.length < 2) - return null; - if (positions[0] == 2 && positions[1] == 1) { - return "YCbCr4:2:2"; - } else if (positions[0] == 2 && positions[1] == 2) { - return "YCbCr4:2:0"; - } else { - return "(Unknown)"; - } - } - - @Nullable - public String getPlanarConfigurationDescription() - { - // When image format is no compression YCbCr, this value shows byte aligns of YCbCr - // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling - // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr - // plane format. - return getIndexedDescription(TAG_PLANAR_CONFIGURATION, - 1, - "Chunky (contiguous for each subsampling pixel)", - "Separate (Y-plane/Cb-plane/Cr-plane format)" - ); - } - - @Nullable - public String getSamplesPerPixelDescription() - { - String value = _directory.getString(TAG_SAMPLES_PER_PIXEL); - return value == null ? null : value + " samples/pixel"; - } - - @Nullable - public String getRowsPerStripDescription() - { - final String value = _directory.getString(TAG_ROWS_PER_STRIP); - return value == null ? null : value + " rows/strip"; - } - - @Nullable - public String getStripByteCountsDescription() - { - final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS); - return value == null ? null : value + " bytes"; - } - - @Nullable - public String getPhotometricInterpretationDescription() - { - // Shows the color space of the image data components - Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION); - if (value == null) - return null; - switch (value) { - case 0: return "WhiteIsZero"; - case 1: return "BlackIsZero"; - case 2: return "RGB"; - case 3: return "RGB Palette"; - case 4: return "Transparency Mask"; - case 5: return "CMYK"; - case 6: return "YCbCr"; - case 8: return "CIELab"; - case 9: return "ICCLab"; - case 10: return "ITULab"; - case 32803: return "Color Filter Array"; - case 32844: return "Pixar LogL"; - case 32845: return "Pixar LogLuv"; - case 32892: return "Linear Raw"; - default: - return "Unknown colour space"; - } - } - - @Nullable - public String getCompressionDescription() - { - Integer value = _directory.getInteger(TAG_THUMBNAIL_COMPRESSION); - if (value == null) - return null; - switch (value) { - case 1: return "Uncompressed"; - case 2: return "CCITT 1D"; - case 3: return "T4/Group 3 Fax"; - case 4: return "T6/Group 4 Fax"; - case 5: return "LZW"; - case 6: return "JPEG (old-style)"; - case 7: return "JPEG"; - case 8: return "Adobe Deflate"; - case 9: return "JBIG B&W"; - case 10: return "JBIG Color"; - case 32766: return "Next"; - case 32771: return "CCIRLEW"; - case 32773: return "PackBits"; - case 32809: return "Thunderscan"; - case 32895: return "IT8CTPAD"; - case 32896: return "IT8LW"; - case 32897: return "IT8MP"; - case 32898: return "IT8BL"; - case 32908: return "PixarFilm"; - case 32909: return "PixarLog"; - case 32946: return "Deflate"; - case 32947: return "DCS"; - case 32661: return "JBIG"; - case 32676: return "SGILog"; - case 32677: return "SGILog24"; - case 32712: return "JPEG 2000"; - case 32713: return "Nikon NEF Compressed"; - default: - return "Unknown compression"; - } - } - - @Nullable - public String getBitsPerSampleDescription() - { - String value = _directory.getString(TAG_BITS_PER_SAMPLE); - return value == null ? null : value + " bits/component/pixel"; - } - - @Nullable - public String getThumbnailImageWidthDescription() - { - String value = _directory.getString(TAG_THUMBNAIL_IMAGE_WIDTH); - return value == null ? null : value + " pixels"; - } - - @Nullable - public String getThumbnailImageHeightDescription() - { - String value = _directory.getString(TAG_THUMBNAIL_IMAGE_HEIGHT); - return value == null ? null : value + " pixels"; - } - @Nullable public String getThumbnailLengthDescription() { @@ -273,55 +66,4 @@ public class ExifThumbnailDescriptor extends TagDescriptor<ExifThumbnailDirector String value = _directory.getString(TAG_THUMBNAIL_OFFSET); return value == null ? null : value + " bytes"; } - - @Nullable - public String getYResolutionDescription() - { - Rational value = _directory.getRational(TAG_Y_RESOLUTION); - if (value == null) - return null; - final String unit = getResolutionDescription(); - return value.toSimpleString(_allowDecimalRepresentationOfRationals) + - " dots per " + - (unit == null ? "unit" : unit.toLowerCase()); - } - - @Nullable - public String getXResolutionDescription() - { - Rational value = _directory.getRational(TAG_X_RESOLUTION); - if (value == null) - return null; - final String unit = getResolutionDescription(); - return value.toSimpleString(_allowDecimalRepresentationOfRationals) + - " dots per " + - (unit == null ? "unit" : unit.toLowerCase()); - } - - @Nullable - public String getYCbCrPositioningDescription() - { - return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point"); - } - - @Nullable - public String getOrientationDescription() - { - return getIndexedDescription(TAG_ORIENTATION, 1, - "Top, left side (Horizontal / normal)", - "Top, right side (Mirror horizontal)", - "Bottom, right side (Rotate 180)", - "Bottom, left side (Mirror vertical)", - "Left side, top (Mirror horizontal and rotate 270 CW)", - "Right side, top (Rotate 90 CW)", - "Right side, bottom (Mirror horizontal and rotate 90 CW)", - "Left side, bottom (Rotate 270 CW)"); - } - - @Nullable - public String getResolutionDescription() - { - // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch) - return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm"); - } } diff --git a/Source/com/drew/metadata/exif/ExifThumbnailDirectory.java b/Source/com/drew/metadata/exif/ExifThumbnailDirectory.java index 86e9023..56122e3 100644 --- a/Source/com/drew/metadata/exif/ExifThumbnailDirectory.java +++ b/Source/com/drew/metadata/exif/ExifThumbnailDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,12 +22,7 @@ package com.drew.metadata.exif; import com.drew.lang.annotations.NotNull; -import com.drew.lang.annotations.Nullable; -import com.drew.metadata.Directory; -import com.drew.metadata.MetadataException; -import java.io.FileOutputStream; -import java.io.IOException; import java.util.HashMap; /** @@ -35,95 +30,9 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ -public class ExifThumbnailDirectory extends Directory +@SuppressWarnings("WeakerAccess") +public class ExifThumbnailDirectory extends ExifDirectoryBase { - public static final int TAG_THUMBNAIL_IMAGE_WIDTH = 0x0100; - public static final int TAG_THUMBNAIL_IMAGE_HEIGHT = 0x0101; - - /** - * When image format is no compression, this value shows the number of bits - * per component for each pixel. Usually this value is '8,8,8'. - */ - public static final int TAG_BITS_PER_SAMPLE = 0x0102; - - /** - * Shows compression method for Thumbnail. - * 1 = Uncompressed - * 2 = CCITT 1D - * 3 = T4/Group 3 Fax - * 4 = T6/Group 4 Fax - * 5 = LZW - * 6 = JPEG (old-style) - * 7 = JPEG - * 8 = Adobe Deflate - * 9 = JBIG B&W - * 10 = JBIG Color - * 32766 = Next - * 32771 = CCIRLEW - * 32773 = PackBits - * 32809 = Thunderscan - * 32895 = IT8CTPAD - * 32896 = IT8LW - * 32897 = IT8MP - * 32898 = IT8BL - * 32908 = PixarFilm - * 32909 = PixarLog - * 32946 = Deflate - * 32947 = DCS - * 34661 = JBIG - * 34676 = SGILog - * 34677 = SGILog24 - * 34712 = JPEG 2000 - * 34713 = Nikon NEF Compressed - */ - public static final int TAG_THUMBNAIL_COMPRESSION = 0x0103; - - /** - * Shows the color space of the image data components. - * 0 = WhiteIsZero - * 1 = BlackIsZero - * 2 = RGB - * 3 = RGB Palette - * 4 = Transparency Mask - * 5 = CMYK - * 6 = YCbCr - * 8 = CIELab - * 9 = ICCLab - * 10 = ITULab - * 32803 = Color Filter Array - * 32844 = Pixar LogL - * 32845 = Pixar LogLuv - * 34892 = Linear Raw - */ - public static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106; - - /** - * The position in the file of raster data. - */ - public static final int TAG_STRIP_OFFSETS = 0x0111; - public static final int TAG_ORIENTATION = 0x0112; - /** - * Each pixel is composed of this many samples. - */ - public static final int TAG_SAMPLES_PER_PIXEL = 0x0115; - /** - * The raster is codified by a single block of data holding this many rows. - */ - public static final int TAG_ROWS_PER_STRIP = 0x116; - /** - * The size of the raster data in bytes. - */ - public static final int TAG_STRIP_BYTE_COUNTS = 0x0117; - /** - * When image format is no compression YCbCr, this value shows byte aligns of - * YCbCr data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for - * each subsampling pixel. If value is '2', Y/Cb/Cr value is separated and - * stored to Y plane/Cb plane/Cr plane format. - */ - public static final int TAG_X_RESOLUTION = 0x011A; - public static final int TAG_Y_RESOLUTION = 0x011B; - public static final int TAG_PLANAR_CONFIGURATION = 0x011C; - public static final int TAG_RESOLUTION_UNIT = 0x0128; /** * The offset to thumbnail image bytes. */ @@ -132,40 +41,24 @@ public class ExifThumbnailDirectory extends Directory * The size of the thumbnail image data in bytes. */ public static final int TAG_THUMBNAIL_LENGTH = 0x0202; - public static final int TAG_YCBCR_COEFFICIENTS = 0x0211; - public static final int TAG_YCBCR_SUBSAMPLING = 0x0212; - public static final int TAG_YCBCR_POSITIONING = 0x0213; - public static final int TAG_REFERENCE_BLACK_WHITE = 0x0214; + + /** + * @deprecated use {@link com.drew.metadata.exif.ExifDirectoryBase#TAG_COMPRESSION} instead. + */ + @Deprecated + public static final int TAG_THUMBNAIL_COMPRESSION = 0x0103; @NotNull protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); - static { - _tagNameMap.put(TAG_THUMBNAIL_IMAGE_WIDTH, "Thumbnail Image Width"); - _tagNameMap.put(TAG_THUMBNAIL_IMAGE_HEIGHT, "Thumbnail Image Height"); - _tagNameMap.put(TAG_BITS_PER_SAMPLE, "Bits Per Sample"); - _tagNameMap.put(TAG_THUMBNAIL_COMPRESSION, "Thumbnail Compression"); - _tagNameMap.put(TAG_PHOTOMETRIC_INTERPRETATION, "Photometric Interpretation"); - _tagNameMap.put(TAG_STRIP_OFFSETS, "Strip Offsets"); - _tagNameMap.put(TAG_ORIENTATION, "Orientation"); - _tagNameMap.put(TAG_SAMPLES_PER_PIXEL, "Samples Per Pixel"); - _tagNameMap.put(TAG_ROWS_PER_STRIP, "Rows Per Strip"); - _tagNameMap.put(TAG_STRIP_BYTE_COUNTS, "Strip Byte Counts"); - _tagNameMap.put(TAG_X_RESOLUTION, "X Resolution"); - _tagNameMap.put(TAG_Y_RESOLUTION, "Y Resolution"); - _tagNameMap.put(TAG_PLANAR_CONFIGURATION, "Planar Configuration"); - _tagNameMap.put(TAG_RESOLUTION_UNIT, "Resolution Unit"); + static + { + addExifTagNames(_tagNameMap); + _tagNameMap.put(TAG_THUMBNAIL_OFFSET, "Thumbnail Offset"); _tagNameMap.put(TAG_THUMBNAIL_LENGTH, "Thumbnail Length"); - _tagNameMap.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients"); - _tagNameMap.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling"); - _tagNameMap.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning"); - _tagNameMap.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White"); } - @Nullable - private byte[] _thumbnailData; - public ExifThumbnailDirectory() { this.setDescriptor(new ExifThumbnailDescriptor(this)); @@ -184,226 +77,4 @@ public class ExifThumbnailDirectory extends Directory { return _tagNameMap; } - - public boolean hasThumbnailData() - { - return _thumbnailData != null; - } - - @Nullable - public byte[] getThumbnailData() - { - return _thumbnailData; - } - - public void setThumbnailData(@Nullable byte[] data) - { - _thumbnailData = data; - } - - public void writeThumbnail(@NotNull String filename) throws MetadataException, IOException - { - byte[] data = _thumbnailData; - - if (data == null) - throw new MetadataException("No thumbnail data exists."); - - FileOutputStream stream = null; - try { - stream = new FileOutputStream(filename); - stream.write(data); - } finally { - if (stream != null) - stream.close(); - } - } - -/* - // This thumbnail extraction code is not complete, and is included to assist anyone who feels like looking into - // it. Please share any progress with the original author, and hence the community. Thanks. - - public Image getThumbnailImage() throws MetadataException - { - if (!hasThumbnailData()) - return null; - - int compression = 0; - try { - compression = this.getInt(ExifSubIFDDirectory.TAG_COMPRESSION); - } catch (Throwable e) { - this.addError("Unable to determine thumbnail type " + e.getMessage()); - } - - final byte[] thumbnailBytes = getThumbnailData(); - - if (compression == ExifSubIFDDirectory.COMPRESSION_JPEG) - { - // JPEG Thumbnail - // operate directly on thumbnailBytes - return decodeBytesAsImage(thumbnailBytes); - } - else if (compression == ExifSubIFDDirectory.COMPRESSION_NONE) - { - // uncompressed thumbnail (raw RGB data) - if (!this.containsTag(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION)) - return null; - - try - { - // If the image is RGB format, then convert it to a bitmap - final int photometricInterpretation = this.getInt(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION); - if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_RGB) - { - // RGB - Image image = createImageFromRawRgb(thumbnailBytes); - return image; - } - else if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_YCBCR) - { - // YCbCr - Image image = createImageFromRawYCbCr(thumbnailBytes); - return image; - } - else if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_MONOCHROME) - { - // Monochrome - return null; - } - } catch (Throwable e) { - this.addError("Unable to extract thumbnail: " + e.getMessage()); - } - } - return null; - } - - /** - * Handle the YCbCr thumbnail encoding used by Ricoh RDC4200/4300, Fuji DS-7/300 and DX-5/7/9 cameras. - * - * At DX-5/7/9, YCbCrSubsampling(0x0212) has values of '2,1', PlanarConfiguration(0x011c) has a value '1'. So the - * data align of this image is below. - * - * Y(0,0),Y(1,0),Cb(0,0),Cr(0,0), Y(2,0),Y(3,0),Cb(2,0),Cr(3.0), Y(4,0),Y(5,0),Cb(4,0),Cr(4,0). . . . - * - * The numbers in parenthesis are pixel coordinates. DX series' YCbCrCoefficients(0x0211) has values '0.299/0.587/0.114', - * ReferenceBlackWhite(0x0214) has values '0,255,128,255,128,255'. Therefore to convert from Y/Cb/Cr to RGB is; - * - * B(0,0)=(Cb-128)*(2-0.114*2)+Y(0,0) - * R(0,0)=(Cr-128)*(2-0.299*2)+Y(0,0) - * G(0,0)=(Y(0,0)-0.114*B(0,0)-0.299*R(0,0))/0.587 - * - * Horizontal subsampling is a value '2', so you can calculate B(1,0)/R(1,0)/G(1,0) by using the Y(1,0) and Cr(0,0)/Cb(0,0). - * Repeat this conversion by value of ImageWidth(0x0100) and ImageLength(0x0101). - * - * @param thumbnailBytes - * @return - * @throws com.drew.metadata.MetadataException - * / - private Image createImageFromRawYCbCr(byte[] thumbnailBytes) throws MetadataException - { - /* - Y = 0.257R + 0.504G + 0.098B + 16 - Cb = -0.148R - 0.291G + 0.439B + 128 - Cr = 0.439R - 0.368G - 0.071B + 128 - - G = 1.164(Y-16) - 0.391(Cb-128) - 0.813(Cr-128) - R = 1.164(Y-16) + 1.596(Cr-128) - B = 1.164(Y-16) + 2.018(Cb-128) - - R, G and B range from 0 to 255. - Y ranges from 16 to 235. - Cb and Cr range from 16 to 240. - - http://www.faqs.org/faqs/graphics/colorspace-faq/ - * / - - int length = thumbnailBytes.length; // this.getInt(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS); - final int imageWidth = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_WIDTH); - final int imageHeight = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT); -// final int headerLength = 54; -// byte[] result = new byte[length + headerLength]; -// // Add a windows BMP header described: -// // http://www.onicos.com/staff/iz/formats/bmp.html -// result[0] = 'B'; -// result[1] = 'M'; // File Type identifier -// result[3] = (byte)(result.length / 256); -// result[2] = (byte)result.length; -// result[10] = (byte)headerLength; -// result[14] = 40; // MS Windows BMP header -// result[18] = (byte)imageWidth; -// result[22] = (byte)imageHeight; -// result[26] = 1; // 1 Plane -// result[28] = 24; // Colour depth -// result[34] = (byte)length; -// result[35] = (byte)(length / 256); - - final BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB); - - // order is YCbCr and image is upside down, bitmaps are BGR -//// for (int i = headerLength, dataOffset = length; i<result.length; i += 3, dataOffset -= 3) -// { -// final int y = thumbnailBytes[dataOffset - 2] & 0xFF; -// final int cb = thumbnailBytes[dataOffset - 1] & 0xFF; -// final int cr = thumbnailBytes[dataOffset] & 0xFF; -// if (y<16 || y>235 || cb<16 || cb>240 || cr<16 || cr>240) -// "".toString(); -// -// int g = (int)(1.164*(y-16) - 0.391*(cb-128) - 0.813*(cr-128)); -// int r = (int)(1.164*(y-16) + 1.596*(cr-128)); -// int b = (int)(1.164*(y-16) + 2.018*(cb-128)); -// -//// result[i] = (byte)b; -//// result[i + 1] = (byte)g; -//// result[i + 2] = (byte)r; -// -// // TODO compose the image here -// image.setRGB(1, 2, 3); -// } - - return image; - } - - /** - * Creates a thumbnail image in (Windows) BMP format from raw RGB data. - * @param thumbnailBytes - * @return - * @throws com.drew.metadata.MetadataException - * / - private Image createImageFromRawRgb(byte[] thumbnailBytes) throws MetadataException - { - final int length = thumbnailBytes.length; // this.getInt(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS); - final int imageWidth = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_WIDTH); - final int imageHeight = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT); -// final int headerLength = 54; -// final byte[] result = new byte[length + headerLength]; -// // Add a windows BMP header described: -// // http://www.onicos.com/staff/iz/formats/bmp.html -// result[0] = 'B'; -// result[1] = 'M'; // File Type identifier -// result[3] = (byte)(result.length / 256); -// result[2] = (byte)result.length; -// result[10] = (byte)headerLength; -// result[14] = 40; // MS Windows BMP header -// result[18] = (byte)imageWidth; -// result[22] = (byte)imageHeight; -// result[26] = 1; // 1 Plane -// result[28] = 24; // Colour depth -// result[34] = (byte)length; -// result[35] = (byte)(length / 256); - - final BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB); - - // order is RGB and image is upside down, bitmaps are BGR -// for (int i = headerLength, dataOffset = length; i<result.length; i += 3, dataOffset -= 3) -// { -// byte b = thumbnailBytes[dataOffset - 2]; -// byte g = thumbnailBytes[dataOffset - 1]; -// byte r = thumbnailBytes[dataOffset]; -// -// // TODO compose the image here -// image.setRGB(1, 2, 3); -// } - - return image; - } -*/ } diff --git a/Source/com/drew/metadata/exif/ExifTiffHandler.java b/Source/com/drew/metadata/exif/ExifTiffHandler.java index 2add05a..c02b60f 100644 --- a/Source/com/drew/metadata/exif/ExifTiffHandler.java +++ b/Source/com/drew/metadata/exif/ExifTiffHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,16 +22,25 @@ package com.drew.metadata.exif; import com.drew.imaging.tiff.TiffProcessingException; import com.drew.imaging.tiff.TiffReader; +import com.drew.imaging.jpeg.JpegMetadataReader; +import com.drew.imaging.jpeg.JpegProcessingException; + +import com.drew.lang.Charsets; import com.drew.lang.RandomAccessReader; 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.StringValue; import com.drew.metadata.exif.makernotes.*; import com.drew.metadata.iptc.IptcReader; import com.drew.metadata.tiff.DirectoryTiffHandler; +import com.drew.metadata.xmp.XmpReader; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.Arrays; import java.util.Set; /** @@ -44,36 +53,91 @@ import java.util.Set; */ public class ExifTiffHandler extends DirectoryTiffHandler { - private final boolean _storeThumbnailBytes; - - public ExifTiffHandler(@NotNull Metadata metadata, boolean storeThumbnailBytes) + public ExifTiffHandler(@NotNull Metadata metadata, @Nullable Directory parentDirectory) { - super(metadata, ExifIFD0Directory.class); - _storeThumbnailBytes = storeThumbnailBytes; + super(metadata); + + if (parentDirectory != null) + _currentDirectory.setParent(parentDirectory); } public void setTiffMarker(int marker) throws TiffProcessingException { final int standardTiffMarker = 0x002A; final int olympusRawTiffMarker = 0x4F52; // for ORF files + final int olympusRawTiffMarker2 = 0x5352; // for ORF files final int panasonicRawTiffMarker = 0x0055; // for RW2 files - if (marker != standardTiffMarker && marker != olympusRawTiffMarker && marker != panasonicRawTiffMarker) { - throw new TiffProcessingException("Unexpected TIFF marker: 0x" + Integer.toHexString(marker)); + switch (marker) + { + case standardTiffMarker: + case olympusRawTiffMarker: // Todo: implement an IFD0, if there is one + case olympusRawTiffMarker2: // Todo: implement an IFD0, if there is one + pushDirectory(ExifIFD0Directory.class); + break; + case panasonicRawTiffMarker: + pushDirectory(PanasonicRawIFD0Directory.class); + break; + default: + throw new TiffProcessingException(String.format("Unexpected TIFF marker: 0x%X", marker)); } } - public boolean isTagIfdPointer(int tagType) + public boolean tryEnterSubIfd(int tagId) { - if (tagType == ExifIFD0Directory.TAG_EXIF_SUB_IFD_OFFSET && _currentDirectory instanceof ExifIFD0Directory) { + if (tagId == ExifDirectoryBase.TAG_SUB_IFD_OFFSET) { pushDirectory(ExifSubIFDDirectory.class); return true; - } else if (tagType == ExifIFD0Directory.TAG_GPS_INFO_OFFSET && _currentDirectory instanceof ExifIFD0Directory) { - pushDirectory(GpsDirectory.class); - return true; - } else if (tagType == ExifSubIFDDirectory.TAG_INTEROP_OFFSET && _currentDirectory instanceof ExifSubIFDDirectory) { - pushDirectory(ExifInteropDirectory.class); - return true; + } + + if (_currentDirectory instanceof ExifIFD0Directory || _currentDirectory instanceof PanasonicRawIFD0Directory) { + if (tagId == ExifIFD0Directory.TAG_EXIF_SUB_IFD_OFFSET) { + pushDirectory(ExifSubIFDDirectory.class); + return true; + } + + if (tagId == ExifIFD0Directory.TAG_GPS_INFO_OFFSET) { + pushDirectory(GpsDirectory.class); + return true; + } + } + + if (_currentDirectory instanceof ExifSubIFDDirectory) { + if (tagId == ExifSubIFDDirectory.TAG_INTEROP_OFFSET) { + pushDirectory(ExifInteropDirectory.class); + return true; + } + } + + if (_currentDirectory instanceof OlympusMakernoteDirectory) { + // Note: these also appear in customProcessTag because some are IFD pointers while others begin immediately + // for the same directories + switch(tagId) { + case OlympusMakernoteDirectory.TAG_EQUIPMENT: + pushDirectory(OlympusEquipmentMakernoteDirectory.class); + return true; + case OlympusMakernoteDirectory.TAG_CAMERA_SETTINGS: + pushDirectory(OlympusCameraSettingsMakernoteDirectory.class); + return true; + case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT: + pushDirectory(OlympusRawDevelopmentMakernoteDirectory.class); + return true; + case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT_2: + pushDirectory(OlympusRawDevelopment2MakernoteDirectory.class); + return true; + case OlympusMakernoteDirectory.TAG_IMAGE_PROCESSING: + pushDirectory(OlympusImageProcessingMakernoteDirectory.class); + return true; + case OlympusMakernoteDirectory.TAG_FOCUS_INFO: + pushDirectory(OlympusFocusInfoMakernoteDirectory.class); + return true; + case OlympusMakernoteDirectory.TAG_RAW_INFO: + pushDirectory(OlympusRawInfoMakernoteDirectory.class); + return true; + case OlympusMakernoteDirectory.TAG_MAIN_INFO: + pushDirectory(OlympusMakernoteDirectory.class); + return true; + } } return false; @@ -82,8 +146,14 @@ public class ExifTiffHandler extends DirectoryTiffHandler public boolean hasFollowerIfd() { // In Exif, the only known 'follower' IFD is the thumbnail one, however this may not be the case. - if (_currentDirectory instanceof ExifIFD0Directory) { - pushDirectory(ExifThumbnailDirectory.class); + // UPDATE: In multipage TIFFs, the 'follower' IFD points to the next image in the set + if (_currentDirectory instanceof ExifIFD0Directory || _currentDirectory instanceof ExifImageDirectory) { + // If the PageNumber tag is defined, assume this is a multipage TIFF or similar + // TODO: Find better ways to know which follower Directory should be used + if (_currentDirectory.containsTag(ExifDirectoryBase.TAG_PAGE_NUMBER)) + pushDirectory(ExifImageDirectory.class); + else + pushDirectory(ExifThumbnailDirectory.class); return true; } @@ -96,6 +166,19 @@ public class ExifTiffHandler extends DirectoryTiffHandler return false; } + @Nullable + public Long tryCustomProcessFormat(final int tagId, final int formatCode, final long componentCount) + { + if (formatCode == 13) + return componentCount * 4; + + // an unknown (0) formatCode needs to be potentially handled later as a highly custom directory tag + if(formatCode == 0) + return 0L; + + return null; + } + public boolean customProcessTag(final int tagOffset, final @NotNull Set<Integer> processedIfdOffsets, final int tiffHeaderOffset, @@ -103,6 +186,20 @@ public class ExifTiffHandler extends DirectoryTiffHandler final int tagId, final int byteCount) throws IOException { + // Some 0x0000 tags have a 0 byteCount. Determine whether it's bad. + if (tagId == 0) + { + if (_currentDirectory.containsTag(tagId)) + { + // Let it go through for now. Some directories handle it, some don't + return false; + } + + // Skip over 0x0000 tags that don't have any associated bytes. No idea what it contains in this case, if anything. + if (byteCount == 0) + return true; + } + // Custom processing for the Makernote tag if (tagId == ExifSubIFDDirectory.TAG_MAKERNOTE && _currentDirectory instanceof ExifSubIFDDirectory) { return processMakernote(tagOffset, processedIfdOffsets, tiffHeaderOffset, reader); @@ -113,30 +210,157 @@ public class ExifTiffHandler extends DirectoryTiffHandler // NOTE Adobe sets type 4 for IPTC instead of 7 if (reader.getInt8(tagOffset) == 0x1c) { final byte[] iptcBytes = reader.getBytes(tagOffset, byteCount); - new IptcReader().extract(new SequentialByteArrayReader(iptcBytes), _metadata, iptcBytes.length); + new IptcReader().extract(new SequentialByteArrayReader(iptcBytes), _metadata, iptcBytes.length, _currentDirectory); return true; } return false; } + // Custom processing for embedded XMP data + if (tagId == ExifSubIFDDirectory.TAG_APPLICATION_NOTES && _currentDirectory instanceof ExifIFD0Directory) { + new XmpReader().extract(reader.getNullTerminatedBytes(tagOffset, byteCount), _metadata, _currentDirectory); + return true; + } + + if (HandlePrintIM(_currentDirectory, tagId)) + { + PrintIMDirectory printIMDirectory = new PrintIMDirectory(); + printIMDirectory.setParent(_currentDirectory); + _metadata.addDirectory(printIMDirectory); + ProcessPrintIM(printIMDirectory, tagOffset, reader, byteCount); + return true; + } + + // Note: these also appear in tryEnterSubIfd because some are IFD pointers while others begin immediately + // for the same directories + if(_currentDirectory instanceof OlympusMakernoteDirectory) + { + switch (tagId) + { + case OlympusMakernoteDirectory.TAG_EQUIPMENT: + pushDirectory(OlympusEquipmentMakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset); + return true; + case OlympusMakernoteDirectory.TAG_CAMERA_SETTINGS: + pushDirectory(OlympusCameraSettingsMakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset); + return true; + case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT: + pushDirectory(OlympusRawDevelopmentMakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset); + return true; + case OlympusMakernoteDirectory.TAG_RAW_DEVELOPMENT_2: + pushDirectory(OlympusRawDevelopment2MakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset); + return true; + case OlympusMakernoteDirectory.TAG_IMAGE_PROCESSING: + pushDirectory(OlympusImageProcessingMakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset); + return true; + case OlympusMakernoteDirectory.TAG_FOCUS_INFO: + pushDirectory(OlympusFocusInfoMakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset); + return true; + case OlympusMakernoteDirectory.TAG_RAW_INFO: + pushDirectory(OlympusRawInfoMakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset); + return true; + case OlympusMakernoteDirectory.TAG_MAIN_INFO: + pushDirectory(OlympusMakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, tagOffset, tiffHeaderOffset); + return true; + } + } + + if (_currentDirectory instanceof PanasonicRawIFD0Directory) + { + // these contain binary data with specific offsets, and can't be processed as regular ifd's. + // The binary data is broken into 'fake' tags and there is a pattern. + switch (tagId) + { + case PanasonicRawIFD0Directory.TagWbInfo: + PanasonicRawWbInfoDirectory dirWbInfo = new PanasonicRawWbInfoDirectory(); + dirWbInfo.setParent(_currentDirectory); + _metadata.addDirectory(dirWbInfo); + ProcessBinary(dirWbInfo, tagOffset, reader, byteCount, false, 2); + return true; + case PanasonicRawIFD0Directory.TagWbInfo2: + PanasonicRawWbInfo2Directory dirWbInfo2 = new PanasonicRawWbInfo2Directory(); + dirWbInfo2.setParent(_currentDirectory); + _metadata.addDirectory(dirWbInfo2); + ProcessBinary(dirWbInfo2, tagOffset, reader, byteCount, false, 3); + return true; + case PanasonicRawIFD0Directory.TagDistortionInfo: + PanasonicRawDistortionDirectory dirDistort = new PanasonicRawDistortionDirectory(); + dirDistort.setParent(_currentDirectory); + _metadata.addDirectory(dirDistort); + ProcessBinary(dirDistort, tagOffset, reader, byteCount, true, 1); + return true; + } + } + + // Panasonic RAW sometimes contains an embedded version of the data as a JPG file. + if (tagId == PanasonicRawIFD0Directory.TagJpgFromRaw && _currentDirectory instanceof PanasonicRawIFD0Directory) + { + byte[] jpegrawbytes = reader.getBytes(tagOffset, byteCount); + + // Extract information from embedded image since it is metadata-rich + ByteArrayInputStream jpegmem = new ByteArrayInputStream(jpegrawbytes); + try { + Metadata jpegDirectory = JpegMetadataReader.readMetadata(jpegmem); + for (Directory directory : jpegDirectory.getDirectories()) { + directory.setParent(_currentDirectory); + _metadata.addDirectory(directory); + } + return true; + } catch (JpegProcessingException e) { + _currentDirectory.addError("Error processing JpgFromRaw: " + e.getMessage()); + } catch (IOException e) { + _currentDirectory.addError("Error reading JpgFromRaw: " + e.getMessage()); + } + } + return false; } - public void completed(@NotNull final RandomAccessReader reader, final int tiffHeaderOffset) + private static void ProcessBinary(@NotNull final Directory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader, final int byteCount, final Boolean issigned, final int arrayLength) throws IOException { - if (_storeThumbnailBytes) { - // after the extraction process, if we have the correct tags, we may be able to store thumbnail information - ExifThumbnailDirectory thumbnailDirectory = _metadata.getDirectory(ExifThumbnailDirectory.class); - if (thumbnailDirectory != null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)) { - Integer offset = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET); - Integer length = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH); - if (offset != null && length != null) { - try { - byte[] thumbnailData = reader.getBytes(tiffHeaderOffset + offset, length); - thumbnailDirectory.setThumbnailData(thumbnailData); - } catch (IOException ex) { - thumbnailDirectory.addError("Invalid thumbnail data specification: " + ex.getMessage()); + // expects signed/unsigned int16 (for now) + //int byteSize = issigned ? sizeof(short) : sizeof(ushort); + int byteSize = 2; + + // 'directory' is assumed to contain tags that correspond to the byte position unless it's a set of bytes + for (int i = 0; i < byteCount; i++) + { + if (directory.hasTagName(i)) + { + // only process this tag if the 'next' integral tag exists. Otherwise, it's a set of bytes + if (i < byteCount - 1 && directory.hasTagName(i + 1)) + { + if(issigned) + directory.setObject(i, reader.getInt16(tagValueOffset + (i* byteSize))); + else + directory.setObject(i, reader.getUInt16(tagValueOffset + (i* byteSize))); + } + else + { + // the next arrayLength bytes are a multi-byte value + if (issigned) + { + short[] val = new short[arrayLength]; + for (int j = 0; j<val.length; j++) + val[j] = reader.getInt16(tagValueOffset + ((i + j) * byteSize)); + directory.setObjectArray(i, val); + } + else + { + int[] val = new int[arrayLength]; + for (int j = 0; j<val.length; j++) + val[j] = reader.getUInt16(tagValueOffset + ((i + j) * byteSize)); + directory.setObjectArray(i, val); } + + i += arrayLength - 1; } } } @@ -148,29 +372,34 @@ public class ExifTiffHandler extends DirectoryTiffHandler final @NotNull RandomAccessReader reader) throws IOException { // Determine the camera model and makernote format. - Directory ifd0Directory = _metadata.getDirectory(ExifIFD0Directory.class); - - if (ifd0Directory == null) - return false; + Directory ifd0Directory = _metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); - String cameraMake = ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE); + String cameraMake = ifd0Directory == null ? null : ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE); - final String firstTwoChars = reader.getString(makernoteOffset, 2); - final String firstThreeChars = reader.getString(makernoteOffset, 3); - final String firstFourChars = reader.getString(makernoteOffset, 4); - final String firstFiveChars = reader.getString(makernoteOffset, 5); - final String firstSixChars = reader.getString(makernoteOffset, 6); - final String firstSevenChars = reader.getString(makernoteOffset, 7); - final String firstEightChars = reader.getString(makernoteOffset, 8); - final String firstTwelveChars = reader.getString(makernoteOffset, 12); + final String firstTwoChars = reader.getString(makernoteOffset, 2, Charsets.UTF_8); + final String firstThreeChars = reader.getString(makernoteOffset, 3, Charsets.UTF_8); + final String firstFourChars = reader.getString(makernoteOffset, 4, Charsets.UTF_8); + final String firstFiveChars = reader.getString(makernoteOffset, 5, Charsets.UTF_8); + final String firstSixChars = reader.getString(makernoteOffset, 6, Charsets.UTF_8); + final String firstSevenChars = reader.getString(makernoteOffset, 7, Charsets.UTF_8); + final String firstEightChars = reader.getString(makernoteOffset, 8, Charsets.UTF_8); + final String firstNineChars = reader.getString(makernoteOffset, 9, Charsets.UTF_8); + final String firstTenChars = reader.getString(makernoteOffset, 10, Charsets.UTF_8); + final String firstTwelveChars = reader.getString(makernoteOffset, 12, Charsets.UTF_8); boolean byteOrderBefore = reader.isMotorolaByteOrder(); - if ("OLYMP".equals(firstFiveChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) { + if ("OLYMP\0".equals(firstSixChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) { // Olympus Makernote // Epson and Agfa use Olympus makernote standard: http://www.ozhiker.com/electronics/pjmt/jpeg_info/ pushDirectory(OlympusMakernoteDirectory.class); TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset); + } else if ("OLYMPUS\0II".equals(firstTenChars)) { + // Olympus Makernote (alternate) + // Note that data is relative to the beginning of the makernote + // http://exiv2.org/makernote.html + pushDirectory(OlympusMakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, makernoteOffset); } else if (cameraMake != null && cameraMake.toUpperCase().startsWith("MINOLTA")) { // Cases seen with the model starting with MINOLTA in capitals seem to have a valid Olympus makernote // area that commences immediately. @@ -196,7 +425,7 @@ public class ExifTiffHandler extends DirectoryTiffHandler TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 18, makernoteOffset + 10); break; default: - ifd0Directory.addError("Unsupported Nikon makernote data ignored."); + _currentDirectory.addError("Unsupported Nikon makernote data ignored."); break; } } else { @@ -207,6 +436,12 @@ public class ExifTiffHandler extends DirectoryTiffHandler } else if ("SONY CAM".equals(firstEightChars) || "SONY DSC".equals(firstEightChars)) { pushDirectory(SonyType1MakernoteDirectory.class); TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 12, tiffHeaderOffset); + // Do this check LAST after most other Sony checks + } else if (cameraMake != null && cameraMake.startsWith("SONY") && + !Arrays.equals(reader.getBytes(makernoteOffset, 2), new byte[]{ 0x01, 0x00 }) ) { + // The IFD begins with the first Makernote byte (no ASCII name). Used in SR2 and ARW images + pushDirectory(SonyType1MakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset); } else if ("SEMC MS\u0000\u0000\u0000\u0000\u0000".equals(firstTwelveChars)) { // force MM for this directory reader.setMotorolaByteOrder(true); @@ -218,7 +453,9 @@ public class ExifTiffHandler extends DirectoryTiffHandler TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 10, tiffHeaderOffset); } else if ("KDK".equals(firstThreeChars)) { reader.setMotorolaByteOrder(firstSevenChars.equals("KDK INFO")); - processKodakMakernote(_metadata.getOrCreateDirectory(KodakMakernoteDirectory.class), makernoteOffset, reader); + KodakMakernoteDirectory directory = new KodakMakernoteDirectory(); + _metadata.addDirectory(directory); + processKodakMakernote(directory, makernoteOffset, reader); } else if ("Canon".equalsIgnoreCase(cameraMake)) { pushDirectory(CanonMakernoteDirectory.class); TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset); @@ -245,7 +482,23 @@ public class ExifTiffHandler extends DirectoryTiffHandler TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 22, tiffHeaderOffset); } else if ("LEICA".equals(firstFiveChars)) { reader.setMotorolaByteOrder(false); - if ("Leica Camera AG".equals(cameraMake)) { + + // used by the X1/X2/X VARIO/T + // (X1 starts with "LEICA\0\x01\0", Make is "LEICA CAMERA AG") + // (X2 starts with "LEICA\0\x05\0", Make is "LEICA CAMERA AG") + // (X VARIO starts with "LEICA\0\x04\0", Make is "LEICA CAMERA AG") + // (T (Typ 701) starts with "LEICA\0\0x6", Make is "LEICA CAMERA AG") + // (X (Typ 113) starts with "LEICA\0\0x7", Make is "LEICA CAMERA AG") + + if ("LEICA\0\u0001\0".equals(firstEightChars) || + "LEICA\0\u0004\0".equals(firstEightChars) || + "LEICA\0\u0005\0".equals(firstEightChars) || + "LEICA\0\u0006\0".equals(firstEightChars) || + "LEICA\0\u0007\0".equals(firstEightChars)) + { + pushDirectory(LeicaType5MakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset); + } else if ("Leica Camera AG".equals(cameraMake)) { pushDirectory(LeicaMakernoteDirectory.class); TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, tiffHeaderOffset); } else if ("LEICA".equals(cameraMake)) { @@ -255,7 +508,7 @@ public class ExifTiffHandler extends DirectoryTiffHandler } else { return false; } - } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(makernoteOffset, 12))) { + } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(makernoteOffset, 12, Charsets.UTF_8))) { // NON-Standard TIFF IFD Data using Panasonic Tags. There is no Next-IFD pointer after the IFD // Offsets are relative to the start of the TIFF header at the beginning of the EXIF segment // more information here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html @@ -300,6 +553,25 @@ public class ExifTiffHandler extends DirectoryTiffHandler pushDirectory(RicohMakernoteDirectory.class); TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 8, makernoteOffset); } + } else if (firstTenChars.equals("Apple iOS\0")) { + // Always in Motorola byte order + boolean orderBefore = reader.isMotorolaByteOrder(); + reader.setMotorolaByteOrder(true); + pushDirectory(AppleMakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset + 14, makernoteOffset); + reader.setMotorolaByteOrder(orderBefore); + } else if (reader.getUInt16(makernoteOffset) == ReconyxHyperFireMakernoteDirectory.MAKERNOTE_VERSION) { + ReconyxHyperFireMakernoteDirectory directory = new ReconyxHyperFireMakernoteDirectory(); + _metadata.addDirectory(directory); + processReconyxHyperFireMakernote(directory, makernoteOffset, reader); + } else if (firstNineChars.equalsIgnoreCase("RECONYXUF")) { + ReconyxUltraFireMakernoteDirectory directory = new ReconyxUltraFireMakernoteDirectory(); + _metadata.addDirectory(directory); + processReconyxUltraFireMakernote(directory, makernoteOffset, reader); + } else if ("SAMSUNG".equals(cameraMake)) { + // Only handles Type2 notes correctly. Others aren't implemented, and it's complex to determine which ones to use + pushDirectory(SamsungType2MakernoteDirectory.class); + TiffReader.processIfd(this, reader, processedIfdOffsets, makernoteOffset, tiffHeaderOffset); } else { // The makernote is not comprehended by this library. // If you are reading this and believe a particular camera's image should be processed, get in touch. @@ -310,12 +582,97 @@ public class ExifTiffHandler extends DirectoryTiffHandler return true; } + private static Boolean HandlePrintIM(@NotNull final Directory directory, final int tagId) + { + if (tagId == ExifDirectoryBase.TAG_PRINT_IMAGE_MATCHING_INFO) + return true; + + if (tagId == 0x0E00) + { + // Tempting to say every tagid of 0x0E00 is a PIM tag, but can't be 100% sure + if (directory instanceof CasioType2MakernoteDirectory || + directory instanceof KyoceraMakernoteDirectory || + directory instanceof NikonType2MakernoteDirectory || + directory instanceof OlympusMakernoteDirectory || + directory instanceof PanasonicMakernoteDirectory || + directory instanceof PentaxMakernoteDirectory || + directory instanceof RicohMakernoteDirectory || + directory instanceof SanyoMakernoteDirectory || + directory instanceof SonyType1MakernoteDirectory) + return true; + } + + return false; + } + + /// <summary> + /// Process PrintIM IFD + /// </summary> + /// <remarks> + /// Converted from Exiftool version 10.33 created by Phil Harvey + /// http://www.sno.phy.queensu.ca/~phil/exiftool/ + /// lib\Image\ExifTool\PrintIM.pm + /// </remarks> + private static void ProcessPrintIM(@NotNull final PrintIMDirectory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader, final int byteCount) throws IOException + { + Boolean resetByteOrder = null; + + if (byteCount == 0) + { + directory.addError("Empty PrintIM data"); + return; + } + + if (byteCount <= 15) + { + directory.addError("Bad PrintIM data"); + return; + } + + String header = reader.getString(tagValueOffset, 12, Charsets.UTF_8); + + if (!header.startsWith("PrintIM")) //, StringComparison.Ordinal)) + { + directory.addError("Invalid PrintIM header"); + return; + } + + // check size of PrintIM block + int num = reader.getUInt16(tagValueOffset + 14); + if (byteCount < 16 + num * 6) + { + // size is too big, maybe byte ordering is wrong + resetByteOrder = reader.isMotorolaByteOrder(); + reader.setMotorolaByteOrder(!reader.isMotorolaByteOrder()); + num = reader.getUInt16(tagValueOffset + 14); + if (byteCount < 16 + num * 6) + { + directory.addError("Bad PrintIM size"); + return; + } + } + + directory.setObject(PrintIMDirectory.TagPrintImVersion, header.substring(8, 12)); + + for (int n = 0; n < num; n++) + { + int pos = tagValueOffset + 16 + n * 6; + int tag = reader.getUInt16(pos); + long val = reader.getUInt32(pos + 2); + + directory.setObject(tag, val); + } + + if (resetByteOrder != null) + reader.setMotorolaByteOrder(resetByteOrder); + } + private static void processKodakMakernote(@NotNull final KodakMakernoteDirectory directory, final int tagValueOffset, @NotNull final RandomAccessReader reader) { // Kodak's makernote is not in IFD format. It has values at fixed offsets. int dataOffset = tagValueOffset + 8; try { - directory.setString(KodakMakernoteDirectory.TAG_KODAK_MODEL, reader.getString(dataOffset, 8)); + directory.setStringValue(KodakMakernoteDirectory.TAG_KODAK_MODEL, reader.getStringValue(dataOffset, 8, Charsets.UTF_8)); directory.setInt(KodakMakernoteDirectory.TAG_QUALITY, reader.getUInt8(dataOffset + 9)); directory.setInt(KodakMakernoteDirectory.TAG_BURST_MODE, reader.getUInt8(dataOffset + 10)); directory.setInt(KodakMakernoteDirectory.TAG_IMAGE_WIDTH, reader.getUInt16(dataOffset + 12)); @@ -345,5 +702,148 @@ public class ExifTiffHandler extends DirectoryTiffHandler directory.addError("Error processing Kodak makernote data: " + ex.getMessage()); } } + + private static void processReconyxHyperFireMakernote(@NotNull final ReconyxHyperFireMakernoteDirectory directory, final int makernoteOffset, @NotNull final RandomAccessReader reader) throws IOException + { + directory.setObject(ReconyxHyperFireMakernoteDirectory.TAG_MAKERNOTE_VERSION, reader.getUInt16(makernoteOffset)); + + int major = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION); + int minor = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 2); + int revision = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 4); + String buildYear = String.format("%04X", reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 6)); + String buildDate = String.format("%04X", reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION + 8)); + String buildYearAndDate = buildYear + buildDate; + Integer build; + try { + build = Integer.parseInt(buildYearAndDate); + } catch (NumberFormatException e) { + build = null; + } + if (build != null) + { + directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION, String.format("%d.%d.%d.%s", major, minor, revision, build)); + } + else + { + directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_FIRMWARE_VERSION, String.format("%d.%d.%d", major, minor, revision)); + directory.addError("Error processing Reconyx HyperFire makernote data: build '" + buildYearAndDate + "' is not in the expected format and will be omitted from Firmware Version."); + } + + directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_TRIGGER_MODE, String.valueOf((char)reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_TRIGGER_MODE))); + directory.setIntArray(ReconyxHyperFireMakernoteDirectory.TAG_SEQUENCE, + new int[] + { + reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SEQUENCE), + reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SEQUENCE + 2) + }); + + int eventNumberHigh = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_EVENT_NUMBER); + int eventNumberLow = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_EVENT_NUMBER + 2); + directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_EVENT_NUMBER, (eventNumberHigh << 16) + eventNumberLow); + + int seconds = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL); + int minutes = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 2); + int hour = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 4); + int month = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 6); + int day = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 8); + int year = reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 10); + + if ((seconds >= 0 && seconds < 60) && + (minutes >= 0 && minutes < 60) && + (hour >= 0 && hour < 24) && + (month >= 1 && month < 13) && + (day >= 1 && day < 32) && + (year >= 1 && year <= 9999)) + { + directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL, + String.format("%4d:%2d:%2d %2d:%2d:%2d", year, month, day, hour, minutes, seconds)); + } + else + { + directory.addError("Error processing Reconyx HyperFire makernote data: Date/Time Original " + year + "-" + month + "-" + day + " " + hour + ":" + minutes + ":" + seconds + " is not a valid date/time."); + } + + directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_MOON_PHASE, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_MOON_PHASE)); + directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE_FAHRENHEIT, reader.getInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE_FAHRENHEIT)); + directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE, reader.getInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_AMBIENT_TEMPERATURE)); + //directory.setByteArray(ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, reader.getBytes(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, 28)); + directory.setStringValue(ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, new StringValue(reader.getBytes(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SERIAL_NUMBER, 28), Charsets.UTF_16LE)); + // two unread bytes: the serial number's terminating null + + directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_CONTRAST, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_CONTRAST)); + directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_BRIGHTNESS, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_BRIGHTNESS)); + directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_SHARPNESS, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SHARPNESS)); + directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_SATURATION, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_SATURATION)); + directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_INFRARED_ILLUMINATOR, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_INFRARED_ILLUMINATOR)); + directory.setInt(ReconyxHyperFireMakernoteDirectory.TAG_MOTION_SENSITIVITY, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_MOTION_SENSITIVITY)); + directory.setDouble(ReconyxHyperFireMakernoteDirectory.TAG_BATTERY_VOLTAGE, reader.getUInt16(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_BATTERY_VOLTAGE) / 1000.0); + directory.setString(ReconyxHyperFireMakernoteDirectory.TAG_USER_LABEL, reader.getNullTerminatedString(makernoteOffset + ReconyxHyperFireMakernoteDirectory.TAG_USER_LABEL, 44, Charsets.UTF_8)); + } + + private static void processReconyxUltraFireMakernote(@NotNull final ReconyxUltraFireMakernoteDirectory directory, final int makernoteOffset, @NotNull final RandomAccessReader reader) throws IOException + { + directory.setString(ReconyxUltraFireMakernoteDirectory.TAG_LABEL, reader.getString(makernoteOffset, 9, Charsets.UTF_8)); + /*uint makernoteID = ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernoteID)); + directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernoteID, makernoteID); + if (makernoteID != ReconyxUltraFireMakernoteDirectory.MAKERNOTE_ID) + { + directory.addError("Error processing Reconyx UltraFire makernote data: unknown Makernote ID 0x" + makernoteID.ToString("x8")); + return; + } + directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernoteSize, ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernoteSize))); + uint makernotePublicID = ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernotePublicID)); + directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernotePublicID, makernotePublicID); + if (makernotePublicID != ReconyxUltraFireMakernoteDirectory.MAKERNOTE_PUBLIC_ID) + { + directory.addError("Error processing Reconyx UltraFire makernote data: unknown Makernote Public ID 0x" + makernotePublicID.ToString("x8")); + return; + }*/ + //directory.Set(ReconyxUltraFireMakernoteDirectory.TagMakernotePublicSize, ByteConvert.FromBigEndianToNative(reader.GetUInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagMakernotePublicSize))); + + //directory.Set(ReconyxUltraFireMakernoteDirectory.TagCameraVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagCameraVersion, reader)); + //directory.Set(ReconyxUltraFireMakernoteDirectory.TagUibVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagUibVersion, reader)); + //directory.Set(ReconyxUltraFireMakernoteDirectory.TagBtlVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagBtlVersion, reader)); + //directory.Set(ReconyxUltraFireMakernoteDirectory.TagPexVersion, ProcessReconyxUltraFireVersion(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagPexVersion, reader)); + + directory.setString(ReconyxUltraFireMakernoteDirectory.TAG_EVENT_TYPE, reader.getString(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_EVENT_TYPE, 1, Charsets.UTF_8)); + directory.setIntArray(ReconyxUltraFireMakernoteDirectory.TAG_SEQUENCE, + new int[] + { + reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_SEQUENCE), + reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_SEQUENCE + 1) + }); + //directory.Set(ReconyxUltraFireMakernoteDirectory.TagEventNumber, ByteConvert.FromBigEndianToNative(reader.GetUInt32(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagEventNumber))); + + byte seconds = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL); + byte minutes = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 1); + byte hour = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 2); + byte day = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 3); + byte month = reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL + 4); + /*ushort year = ByteConvert.FromBigEndianToNative(reader.GetUInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagDateTimeOriginal + 5)); + if ((seconds >= 0 && seconds < 60) && + (minutes >= 0 && minutes < 60) && + (hour >= 0 && hour < 24) && + (month >= 1 && month < 13) && + (day >= 1 && day < 32) && + (year >= 1 && year <= 9999)) + { + directory.Set(ReconyxUltraFireMakernoteDirectory.TAG_DATE_TIME_ORIGINAL, new DateTime(year, month, day, hour, minutes, seconds, DateTimeKind.Unspecified)); + } + else + { + directory.addError("Error processing Reconyx UltraFire makernote data: Date/Time Original " + year + "-" + month + "-" + day + " " + hour + ":" + minutes + ":" + seconds + " is not a valid date/time."); + }*/ + //directory.Set(ReconyxUltraFireMakernoteDirectory.TagDayOfWeek, reader.GetByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagDayOfWeek)); + + directory.setInt(ReconyxUltraFireMakernoteDirectory.TAG_MOON_PHASE, reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_MOON_PHASE)); + //directory.Set(ReconyxUltraFireMakernoteDirectory.TagAmbientTemperatureFahrenheit, ByteConvert.FromBigEndianToNative(reader.GetInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagAmbientTemperatureFahrenheit))); + //directory.Set(ReconyxUltraFireMakernoteDirectory.TagAmbientTemperature, ByteConvert.FromBigEndianToNative(reader.GetInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagAmbientTemperature))); + + directory.setInt(ReconyxUltraFireMakernoteDirectory.TAG_FLASH, reader.getByte(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_FLASH)); + //directory.Set(ReconyxUltraFireMakernoteDirectory.TagBatteryVoltage, ByteConvert.FromBigEndianToNative(reader.GetUInt16(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TagBatteryVoltage)) / 1000.0); + directory.setStringValue(ReconyxUltraFireMakernoteDirectory.TAG_SERIAL_NUMBER, new StringValue(reader.getBytes(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_SERIAL_NUMBER, 14), Charsets.UTF_8)); + // unread byte: the serial number's terminating null + directory.setString(ReconyxUltraFireMakernoteDirectory.TAG_USER_LABEL, reader.getNullTerminatedString(makernoteOffset + ReconyxUltraFireMakernoteDirectory.TAG_USER_LABEL, 20, Charsets.UTF_8)); + } } diff --git a/Source/com/drew/metadata/exif/GpsDescriptor.java b/Source/com/drew/metadata/exif/GpsDescriptor.java index 4cdae31..0887a35 100644 --- a/Source/com/drew/metadata/exif/GpsDescriptor.java +++ b/Source/com/drew/metadata/exif/GpsDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import static com.drew.metadata.exif.GpsDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class GpsDescriptor extends TagDescriptor<GpsDirectory> { public GpsDescriptor(@NotNull GpsDirectory directory) @@ -108,8 +109,14 @@ public class GpsDescriptor extends TagDescriptor<GpsDirectory> public String getGpsTimeStampDescription() { // time in hour, min, sec - int[] timeComponents = _directory.getIntArray(TAG_TIME_STAMP); - return timeComponents == null ? null : String.format("%d:%d:%d UTC", timeComponents[0], timeComponents[1], timeComponents[2]); + Rational[] timeComponents = _directory.getRationalArray(TAG_TIME_STAMP); + DecimalFormat df = new DecimalFormat("00.000"); + return timeComponents == null + ? null + : String.format("%02d:%02d:%s UTC", + timeComponents[0].intValue(), + timeComponents[1].intValue(), + df.format(timeComponents[2].doubleValue())); } @Nullable diff --git a/Source/com/drew/metadata/exif/GpsDirectory.java b/Source/com/drew/metadata/exif/GpsDirectory.java index cdaad00..e9ce932 100644 --- a/Source/com/drew/metadata/exif/GpsDirectory.java +++ b/Source/com/drew/metadata/exif/GpsDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,16 +24,21 @@ import com.drew.lang.GeoLocation; import com.drew.lang.Rational; import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; -import com.drew.metadata.Directory; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.HashMap; +import java.util.Locale; /** * Describes Exif tags that contain Global Positioning System (GPS) data. * * @author Drew Noakes https://drewnoakes.com */ -public class GpsDirectory extends Directory +@SuppressWarnings("WeakerAccess") +public class GpsDirectory extends ExifDirectoryBase { /** GPS tag version GPSVersionID 0 0 BYTE 4 */ public static final int TAG_VERSION_ID = 0x0000; @@ -101,6 +106,8 @@ public class GpsDirectory extends Directory static { + addExifTagNames(_tagNameMap); + _tagNameMap.put(TAG_VERSION_ID, "GPS Version ID"); _tagNameMap.put(TAG_LATITUDE_REF, "GPS Latitude Ref"); _tagNameMap.put(TAG_LATITUDE, "GPS Latitude"); @@ -162,10 +169,10 @@ public class GpsDirectory extends Directory @Nullable public GeoLocation getGeoLocation() { - Rational[] latitudes = getRationalArray(GpsDirectory.TAG_LATITUDE); - Rational[] longitudes = getRationalArray(GpsDirectory.TAG_LONGITUDE); - String latitudeRef = getString(GpsDirectory.TAG_LATITUDE_REF); - String longitudeRef = getString(GpsDirectory.TAG_LONGITUDE_REF); + Rational[] latitudes = getRationalArray(TAG_LATITUDE); + Rational[] longitudes = getRationalArray(TAG_LONGITUDE); + String latitudeRef = getString(TAG_LATITUDE_REF); + String longitudeRef = getString(TAG_LONGITUDE_REF); // Make sure we have the required values if (latitudes == null || latitudes.length != 3) @@ -184,4 +191,32 @@ public class GpsDirectory extends Directory return new GeoLocation(lat, lon); } + + /** + * Parses the date stamp tag and the time stamp tag to obtain a single Date object representing the + * date and time when this image was captured. + * + * @return A Date object representing when this image was captured, if possible, otherwise null + */ + @Nullable + public Date getGpsDate() + { + String date = getString(TAG_DATE_STAMP); + Rational[] timeComponents = getRationalArray(TAG_TIME_STAMP); + + // Make sure we have the required values + if (date == null) + return null; + if (timeComponents == null || timeComponents.length != 3) + return null; + + String dateTime = String.format(Locale.US, "%s %02d:%02d:%02.3f UTC", + date, timeComponents[0].intValue(), timeComponents[1].intValue(), timeComponents[2].doubleValue()); + try { + DateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss.S z"); + return parser.parse(dateTime); + } catch (ParseException e) { + return null; + } + } } diff --git a/Source/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java b/Source/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java new file mode 100644 index 0000000..dd08840 --- /dev/null +++ b/Source/com/drew/metadata/exif/PanasonicRawDistortionDescriptor.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.Rational; +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.PanasonicRawDistortionDirectory.*; + +/** + * Provides human-readable string representations of tag values stored in a {@link PanasonicRawDistortionDirectory}. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PanasonicRawDistortionDescriptor extends TagDescriptor<PanasonicRawDistortionDirectory> +{ + public PanasonicRawDistortionDescriptor(@NotNull PanasonicRawDistortionDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagDistortionParam02: + return getDistortionParam02Description(); + case TagDistortionParam04: + return getDistortionParam04Description(); + case TagDistortionScale: + return getDistortionScaleDescription(); + case TagDistortionCorrection: + return getDistortionCorrectionDescription(); + case TagDistortionParam08: + return getDistortionParam08Description(); + case TagDistortionParam09: + return getDistortionParam09Description(); + case TagDistortionParam11: + return getDistortionParam11Description(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getWbTypeDescription(int tagType) + { + Integer wbtype = _directory.getInteger(tagType); + if (wbtype == null) + return null; + + return super.getLightSourceDescription(wbtype.shortValue()); + } + + @Nullable + public String getDistortionParam02Description() + { + Integer value = _directory.getInteger(TagDistortionParam02); + if (value == null) + return null; + + return new Rational(value, 32678).toString(); + } + + @Nullable + public String getDistortionParam04Description() + { + Integer value = _directory.getInteger(TagDistortionParam04); + if (value == null) + return null; + + return new Rational(value, 32678).toString(); + } + + @Nullable + public String getDistortionScaleDescription() + { + Integer value = _directory.getInteger(TagDistortionScale); + if (value == null) + return null; + + //return (1 / (1 + value / 32768)).toString(); + return Integer.toString(1 / (1 + value / 32768)); + } + + @Nullable + public String getDistortionCorrectionDescription() + { + Integer value = _directory.getInteger(TagDistortionCorrection); + if (value == null) + return null; + + // (have seen the upper 4 bits set for GF5 and GX1, giving a value of -4095 - PH) + int mask = 0x000f; + switch (value & mask) + { + case 0: + return "Off"; + case 1: + return "On"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getDistortionParam08Description() + { + Integer value = _directory.getInteger(TagDistortionParam08); + if (value == null) + return null; + + return new Rational(value, 32678).toString(); + } + + @Nullable + public String getDistortionParam09Description() + { + Integer value = _directory.getInteger(TagDistortionParam09); + if (value == null) + return null; + + return new Rational(value, 32678).toString(); + } + + @Nullable + public String getDistortionParam11Description() + { + Integer value = _directory.getInteger(TagDistortionParam11); + if (value == null) + return null; + + return new Rational(value, 32678).toString(); + } +} diff --git a/Source/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java b/Source/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java new file mode 100644 index 0000000..c86e664 --- /dev/null +++ b/Source/com/drew/metadata/exif/PanasonicRawDistortionDirectory.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * These tags can be found in Panasonic/Leica RAW, RW2 and RWL images. The index values are 'fake' but + * chosen specifically to make processing easier + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PanasonicRawDistortionDirectory extends Directory +{ + // 0 and 1 are checksums + + public static final int TagDistortionParam02 = 2; + + public static final int TagDistortionParam04 = 4; + public static final int TagDistortionScale = 5; + + public static final int TagDistortionCorrection = 7; + public static final int TagDistortionParam08 = 8; + public static final int TagDistortionParam09 = 9; + + public static final int TagDistortionParam11 = 11; + public static final int TagDistortionN = 12; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TagDistortionParam02, "Distortion Param 2"); + _tagNameMap.put(TagDistortionParam04, "Distortion Param 4"); + _tagNameMap.put(TagDistortionScale, "Distortion Scale"); + _tagNameMap.put(TagDistortionCorrection, "Distortion Correction"); + _tagNameMap.put(TagDistortionParam08, "Distortion Param 8"); + _tagNameMap.put(TagDistortionParam09, "Distortion Param 9"); + _tagNameMap.put(TagDistortionParam11, "Distortion Param 11"); + _tagNameMap.put(TagDistortionN, "Distortion N"); + } + + public PanasonicRawDistortionDirectory() + { + this.setDescriptor(new PanasonicRawDistortionDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "PanasonicRaw DistortionInfo"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java b/Source/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java new file mode 100644 index 0000000..61efbac --- /dev/null +++ b/Source/com/drew/metadata/exif/PanasonicRawIFD0Descriptor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.PanasonicRawIFD0Directory.*; + +/** + * Provides human-readable string representations of tag values stored in a {@link PanasonicRawIFD0Directory}. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PanasonicRawIFD0Descriptor extends TagDescriptor<PanasonicRawIFD0Directory> +{ + public PanasonicRawIFD0Descriptor(@NotNull PanasonicRawIFD0Directory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) + { + case TagPanasonicRawVersion: + return getVersionBytesDescription(TagPanasonicRawVersion, 2); + case TagOrientation: + return getOrientationDescription(TagOrientation); + default: + return super.getDescription(tagType); + } + } +} diff --git a/Source/com/drew/metadata/exif/PanasonicRawIFD0Directory.java b/Source/com/drew/metadata/exif/PanasonicRawIFD0Directory.java new file mode 100644 index 0000000..bc94052 --- /dev/null +++ b/Source/com/drew/metadata/exif/PanasonicRawIFD0Directory.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * These tags are found in IFD0 of Panasonic/Leica RAW, RW2 and RWL images. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PanasonicRawIFD0Directory extends Directory +{ + public static final int TagPanasonicRawVersion = 0x0001; + public static final int TagSensorWidth = 0x0002; + public static final int TagSensorHeight = 0x0003; + public static final int TagSensorTopBorder = 0x0004; + public static final int TagSensorLeftBorder = 0x0005; + public static final int TagSensorBottomBorder = 0x0006; + public static final int TagSensorRightBorder = 0x0007; + + public static final int TagBlackLevel1 = 0x0008; + public static final int TagBlackLevel2 = 0x0009; + public static final int TagBlackLevel3 = 0x000a; + public static final int TagLinearityLimitRed = 0x000e; + public static final int TagLinearityLimitGreen = 0x000f; + public static final int TagLinearityLimitBlue = 0x0010; + public static final int TagRedBalance = 0x0011; + public static final int TagBlueBalance = 0x0012; + public static final int TagWbInfo = 0x0013; + + public static final int TagIso = 0x0017; + public static final int TagHighIsoMultiplierRed = 0x0018; + public static final int TagHighIsoMultiplierGreen = 0x0019; + public static final int TagHighIsoMultiplierBlue = 0x001a; + public static final int TagBlackLevelRed = 0x001c; + public static final int TagBlackLevelGreen = 0x001d; + public static final int TagBlackLevelBlue = 0x001e; + public static final int TagWbRedLevel = 0x0024; + public static final int TagWbGreenLevel = 0x0025; + public static final int TagWbBlueLevel = 0x0026; + + public static final int TagWbInfo2 = 0x0027; + + public static final int TagJpgFromRaw = 0x002e; + + public static final int TagCropTop = 0x002f; + public static final int TagCropLeft = 0x0030; + public static final int TagCropBottom = 0x0031; + public static final int TagCropRight = 0x0032; + + public static final int TagMake = 0x010f; + public static final int TagModel = 0x0110; + public static final int TagStripOffsets = 0x0111; + public static final int TagOrientation = 0x0112; + public static final int TagRowsPerStrip = 0x0116; + public static final int TagStripByteCounts = 0x0117; + public static final int TagRawDataOffset = 0x0118; + + public static final int TagDistortionInfo = 0x0119; + + public PanasonicRawIFD0Directory() + { + this.setDescriptor(new PanasonicRawIFD0Descriptor(this)); + } + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TagPanasonicRawVersion, "Panasonic Raw Version"); + _tagNameMap.put(TagSensorWidth, "Sensor Width"); + _tagNameMap.put(TagSensorHeight, "Sensor Height"); + _tagNameMap.put(TagSensorTopBorder, "Sensor Top Border"); + _tagNameMap.put(TagSensorLeftBorder, "Sensor Left Border"); + _tagNameMap.put(TagSensorBottomBorder, "Sensor Bottom Border"); + _tagNameMap.put(TagSensorRightBorder, "Sensor Right Border"); + + _tagNameMap.put(TagBlackLevel1, "Black Level 1"); + _tagNameMap.put(TagBlackLevel2, "Black Level 2"); + _tagNameMap.put(TagBlackLevel3, "Black Level 3"); + _tagNameMap.put(TagLinearityLimitRed, "Linearity Limit Red"); + _tagNameMap.put(TagLinearityLimitGreen, "Linearity Limit Green"); + _tagNameMap.put(TagLinearityLimitBlue, "Linearity Limit Blue"); + _tagNameMap.put(TagRedBalance, "Red Balance"); + _tagNameMap.put(TagBlueBalance, "Blue Balance"); + + _tagNameMap.put(TagIso, "ISO"); + _tagNameMap.put(TagHighIsoMultiplierRed, "High ISO Multiplier Red"); + _tagNameMap.put(TagHighIsoMultiplierGreen, "High ISO Multiplier Green"); + _tagNameMap.put(TagHighIsoMultiplierBlue, "High ISO Multiplier Blue"); + _tagNameMap.put(TagBlackLevelRed, "Black Level Red"); + _tagNameMap.put(TagBlackLevelGreen, "Black Level Green"); + _tagNameMap.put(TagBlackLevelBlue, "Black Level Blue"); + _tagNameMap.put(TagWbRedLevel, "WB Red Level"); + _tagNameMap.put(TagWbGreenLevel, "WB Green Level"); + _tagNameMap.put(TagWbBlueLevel, "WB Blue Level"); + + _tagNameMap.put(TagJpgFromRaw, "Jpg From Raw"); + + _tagNameMap.put(TagCropTop, "Crop Top"); + _tagNameMap.put(TagCropLeft, "Crop Left"); + _tagNameMap.put(TagCropBottom, "Crop Bottom"); + _tagNameMap.put(TagCropRight, "Crop Right"); + + _tagNameMap.put(TagMake, "Make"); + _tagNameMap.put(TagModel, "Model"); + _tagNameMap.put(TagStripOffsets, "Strip Offsets"); + _tagNameMap.put(TagOrientation, "Orientation"); + _tagNameMap.put(TagRowsPerStrip, "Rows Per Strip"); + _tagNameMap.put(TagStripByteCounts, "Strip Byte Counts"); + _tagNameMap.put(TagRawDataOffset, "Raw Data Offset"); + } + + @Override + @NotNull + public String getName() + { + return "PanasonicRaw Exif IFD0"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java b/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java new file mode 100644 index 0000000..60af647 --- /dev/null +++ b/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Descriptor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.PanasonicRawWbInfo2Directory.*; + +/** + * Provides human-readable string representations of tag values stored in a {@link PanasonicRawWbInfo2Directory}. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PanasonicRawWbInfo2Descriptor extends TagDescriptor<PanasonicRawWbInfo2Directory> +{ + public PanasonicRawWbInfo2Descriptor(@NotNull PanasonicRawWbInfo2Directory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagWbType1: + case TagWbType2: + case TagWbType3: + case TagWbType4: + case TagWbType5: + case TagWbType6: + case TagWbType7: + return getWbTypeDescription(tagType); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getWbTypeDescription(int tagType) + { + Integer wbtype = _directory.getInteger(tagType); + if (wbtype == null) + return null; + + return super.getLightSourceDescription(wbtype.shortValue()); + } +} diff --git a/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java b/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java new file mode 100644 index 0000000..2f29ab1 --- /dev/null +++ b/Source/com/drew/metadata/exif/PanasonicRawWbInfo2Directory.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * These tags can be found in Panasonic/Leica RAW, RW2 and RWL images. The index values are 'fake' but + * chosen specifically to make processing easier + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PanasonicRawWbInfo2Directory extends Directory +{ + public static final int TagNumWbEntries = 0; + + public static final int TagWbType1 = 1; + public static final int TagWbRgbLevels1 = 2; + + public static final int TagWbType2 = 5; + public static final int TagWbRgbLevels2 = 6; + + public static final int TagWbType3 = 9; + public static final int TagWbRgbLevels3 = 10; + + public static final int TagWbType4 = 13; + public static final int TagWbRgbLevels4 = 14; + + public static final int TagWbType5 = 17; + public static final int TagWbRgbLevels5 = 18; + + public static final int TagWbType6 = 21; + public static final int TagWbRgbLevels6 = 22; + + public static final int TagWbType7 = 25; + public static final int TagWbRgbLevels7 = 26; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TagNumWbEntries, "Num WB Entries"); + _tagNameMap.put(TagNumWbEntries, "Num WB Entries"); + _tagNameMap.put(TagWbType1, "WB Type 1"); + _tagNameMap.put(TagWbRgbLevels1, "WB RGB Levels 1"); + _tagNameMap.put(TagWbType2, "WB Type 2"); + _tagNameMap.put(TagWbRgbLevels2, "WB RGB Levels 2"); + _tagNameMap.put(TagWbType3, "WB Type 3"); + _tagNameMap.put(TagWbRgbLevels3, "WB RGB Levels 3"); + _tagNameMap.put(TagWbType4, "WB Type 4"); + _tagNameMap.put(TagWbRgbLevels4, "WB RGB Levels 4"); + _tagNameMap.put(TagWbType5, "WB Type 5"); + _tagNameMap.put(TagWbRgbLevels5, "WB RGB Levels 5"); + _tagNameMap.put(TagWbType6, "WB Type 6"); + _tagNameMap.put(TagWbRgbLevels6, "WB RGB Levels 6"); + _tagNameMap.put(TagWbType7, "WB Type 7"); + _tagNameMap.put(TagWbRgbLevels7, "WB RGB Levels 7"); + } + + public PanasonicRawWbInfo2Directory() + { + this.setDescriptor(new PanasonicRawWbInfo2Descriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "PanasonicRaw WbInfo2"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java b/Source/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java new file mode 100644 index 0000000..61ff4a2 --- /dev/null +++ b/Source/com/drew/metadata/exif/PanasonicRawWbInfoDescriptor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.PanasonicRawWbInfoDirectory.*; + +/** + * Provides human-readable string representations of tag values stored in a {@link PanasonicRawWbInfoDirectory}. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PanasonicRawWbInfoDescriptor extends TagDescriptor<PanasonicRawWbInfoDirectory> +{ + public PanasonicRawWbInfoDescriptor(@NotNull PanasonicRawWbInfoDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagWbType1: + case TagWbType2: + case TagWbType3: + case TagWbType4: + case TagWbType5: + case TagWbType6: + case TagWbType7: + return getWbTypeDescription(tagType); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getWbTypeDescription(int tagType) + { + Integer wbtype = _directory.getInteger(tagType); + if (wbtype == null) + return null; + + return super.getLightSourceDescription(wbtype.shortValue()); + } +} diff --git a/Source/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java b/Source/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java new file mode 100644 index 0000000..036c761 --- /dev/null +++ b/Source/com/drew/metadata/exif/PanasonicRawWbInfoDirectory.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * These tags can be found in Panasonic/Leica RAW, RW2 and RWL images. The index values are 'fake' but + * chosen specifically to make processing easier + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PanasonicRawWbInfoDirectory extends Directory +{ + public static final int TagNumWbEntries = 0; + + public static final int TagWbType1 = 1; + public static final int TagWbRbLevels1 = 2; + + public static final int TagWbType2 = 4; + public static final int TagWbRbLevels2 = 5; + + public static final int TagWbType3 = 7; + public static final int TagWbRbLevels3 = 8; + + public static final int TagWbType4 = 10; + public static final int TagWbRbLevels4 = 11; + + public static final int TagWbType5 = 13; + public static final int TagWbRbLevels5 = 14; + + public static final int TagWbType6 = 16; + public static final int TagWbRbLevels6 = 17; + + public static final int TagWbType7 = 19; + public static final int TagWbRbLevels7 = 20; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TagNumWbEntries, "Num WB Entries"); + _tagNameMap.put(TagWbType1, "WB Type 1"); + _tagNameMap.put(TagWbRbLevels1, "WB RGB Levels 1"); + _tagNameMap.put(TagWbType2, "WB Type 2"); + _tagNameMap.put(TagWbRbLevels2, "WB RGB Levels 2"); + _tagNameMap.put(TagWbType3, "WB Type 3"); + _tagNameMap.put(TagWbRbLevels3, "WB RGB Levels 3"); + _tagNameMap.put(TagWbType4, "WB Type 4"); + _tagNameMap.put(TagWbRbLevels4, "WB RGB Levels 4"); + _tagNameMap.put(TagWbType5, "WB Type 5"); + _tagNameMap.put(TagWbRbLevels5, "WB RGB Levels 5"); + _tagNameMap.put(TagWbType6, "WB Type 6"); + _tagNameMap.put(TagWbRbLevels6, "WB RGB Levels 6"); + _tagNameMap.put(TagWbType7, "WB Type 7"); + _tagNameMap.put(TagWbRbLevels7, "WB RGB Levels 7"); + } + + public PanasonicRawWbInfoDirectory() + { + this.setDescriptor(new PanasonicRawWbInfoDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "PanasonicRaw WbInfo"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/PrintIMDescriptor.java b/Source/com/drew/metadata/exif/PrintIMDescriptor.java new file mode 100644 index 0000000..50400ee --- /dev/null +++ b/Source/com/drew/metadata/exif/PrintIMDescriptor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.PrintIMDirectory.*; + +/** + * Provides human-readable string representations of tag values stored in a {@link PrintIMDirectory}. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PrintIMDescriptor extends TagDescriptor<PrintIMDirectory> +{ + public PrintIMDescriptor(@NotNull PrintIMDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagPrintImVersion: + return super.getDescription(tagType); + default: + Integer value = _directory.getInteger(tagType); + if (value == null) + return null; + return String.format("0x%08x", value); + } + } +} diff --git a/Source/com/drew/metadata/exif/PrintIMDirectory.java b/Source/com/drew/metadata/exif/PrintIMDirectory.java new file mode 100644 index 0000000..abada9c --- /dev/null +++ b/Source/com/drew/metadata/exif/PrintIMDirectory.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * These tags can be found in Epson proprietary metadata. The index values are 'fake' but + * chosen specifically to make processing easier + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PrintIMDirectory extends Directory +{ + public static final int TagPrintImVersion = 0x0000; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TagPrintImVersion, "PrintIM Version"); + } + + public PrintIMDirectory() + { + this.setDescriptor(new PrintIMDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "PrintIM"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java new file mode 100644 index 0000000..daabd18 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDescriptor.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +/** + * Provides human-readable string representations of tag values stored in a {@link AppleMakernoteDirectory}. + * <p> + * Using information from http://owl.phy.queensu.ca/~phil/exiftool/TagNames/Apple.html + * + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class AppleMakernoteDescriptor extends TagDescriptor<AppleMakernoteDirectory> +{ + public AppleMakernoteDescriptor(@NotNull AppleMakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case AppleMakernoteDirectory.TAG_HDR_IMAGE_TYPE: + return getHdrImageTypeDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getHdrImageTypeDescription() + { + return getIndexedDescription(AppleMakernoteDirectory.TAG_HDR_IMAGE_TYPE, 3, "HDR Image", "Original Image"); + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java new file mode 100644 index 0000000..604c73e --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/AppleMakernoteDirectory.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * Describes tags specific to Apple cameras. + * <p> + * Using information from http://owl.phy.queensu.ca/~phil/exiftool/TagNames/Apple.html + * + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class AppleMakernoteDirectory extends Directory +{ + public static final int TAG_RUN_TIME = 0x0003; + public static final int TAG_HDR_IMAGE_TYPE = 0x000a; + public static final int TAG_BURST_UUID = 0x000b; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TAG_RUN_TIME, "Run Time"); + _tagNameMap.put(TAG_HDR_IMAGE_TYPE, "HDR Image Type"); + _tagNameMap.put(TAG_BURST_UUID, "Burst UUID"); + } + + public AppleMakernoteDirectory() + { + this.setDescriptor(new AppleMakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Apple Makernote"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java index 1f495db..4eb035a 100644 --- a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,9 @@ import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; +import java.text.DecimalFormat; +import java.util.HashMap; + import static com.drew.metadata.exif.makernotes.CanonMakernoteDirectory.*; /** @@ -31,6 +34,7 @@ import static com.drew.metadata.exif.makernotes.CanonMakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirectory> { public CanonMakernoteDescriptor(@NotNull CanonMakernoteDirectory directory) @@ -51,6 +55,8 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect return getFocusTypeDescription(); case CameraSettings.TAG_DIGITAL_ZOOM: return getDigitalZoomDescription(); + case CameraSettings.TAG_RECORD_MODE: + return getRecordModeDescription(); case CameraSettings.TAG_QUALITY: return getQualityDescription(); case CameraSettings.TAG_MACRO_MODE: @@ -81,6 +87,8 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect return getAfPointSelectedDescription(); case CameraSettings.TAG_EXPOSURE_MODE: return getExposureModeDescription(); + case CameraSettings.TAG_LENS_TYPE: + return getLensTypeDescription(); case CameraSettings.TAG_LONG_FOCAL_LENGTH: return getLongFocalLengthDescription(); case CameraSettings.TAG_SHORT_FOCAL_LENGTH: @@ -97,6 +105,28 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect return getAfPointUsedDescription(); case FocalLength.TAG_FLASH_BIAS: return getFlashBiasDescription(); + case AFInfo.TAG_AF_POINTS_IN_FOCUS: + return getTagAfPointsInFocus(); + case CameraSettings.TAG_MAX_APERTURE: + return getMaxApertureDescription(); + case CameraSettings.TAG_MIN_APERTURE: + return getMinApertureDescription(); + case CameraSettings.TAG_FOCUS_CONTINUOUS: + return getFocusContinuousDescription(); + case CameraSettings.TAG_AE_SETTING: + return getAESettingDescription(); + case CanonMakernoteDirectory.CameraSettings.TAG_DISPLAY_APERTURE: + return getDisplayApertureDescription(); + case CanonMakernoteDirectory.CameraSettings.TAG_SPOT_METERING_MODE: + return getSpotMeteringModeDescription(); + case CanonMakernoteDirectory.CameraSettings.TAG_PHOTO_EFFECT: + return getPhotoEffectDescription(); + case CanonMakernoteDirectory.CameraSettings.TAG_MANUAL_FLASH_OUTPUT: + return getManualFlashOutputDescription(); + case CanonMakernoteDirectory.CameraSettings.TAG_COLOR_TONE: + return getColorToneDescription(); + case CanonMakernoteDirectory.CameraSettings.TAG_SRAW_QUALITY: + return getSRawQualityDescription(); // 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. @@ -135,6 +165,7 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect @Nullable public String getSerialNumberDescription() { + // http://www.ozhiker.com/electronics/pjmt/jpeg_info/canon_mn.html Integer value = _directory.getInteger(TAG_CANON_SERIAL_NUMBER); if (value == null) return null; @@ -340,7 +371,7 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect // not // 0, 0.33, 0.5, 0.66, 1 - return ((isNegative) ? "-" : "") + Float.toString(value / 32f) + " EV"; + return (isNegative ? "-" : "") + Float.toString(value / 32f) + " EV"; } @Nullable @@ -360,6 +391,28 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect } } + @Nullable + public String getTagAfPointsInFocus() + { + Integer value = _directory.getInteger(AFInfo.TAG_AF_POINTS_IN_FOCUS); + if (value == null) + return null; + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < 16; i++) + { + if ((value & 1 << i) != 0) + { + if (sb.length() != 0) + sb.append(','); + sb.append(i); + } + } + + return sb.length() == 0 ? "None" : sb.toString(); + } + @Nullable public String getWhiteBalanceDescription() { @@ -387,16 +440,16 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect Integer value = _directory.getInteger(CameraSettings.TAG_FLASH_DETAILS); if (value == null) return null; - if (((value >> 14) & 1) > 0) { + if (((value >> 14) & 1) != 0) { return "External E-TTL"; } - if (((value >> 13) & 1) > 0) { + if (((value >> 13) & 1) != 0) { return "Internal flash"; } - if (((value >> 11) & 1) > 0) { + if (((value >> 11) & 1) != 0) { return "FP sync used"; } - if (((value >> 4) & 1) > 0) { + if (((value >> 4) & 1) != 0) { return "FP sync enabled"; } return "Unknown (" + value + ")"; @@ -449,6 +502,39 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect ); } + @Nullable + public String getLensTypeDescription() { + Integer value = _directory.getInteger(CameraSettings.TAG_LENS_TYPE); + if (value == null) + return null; + + return _lensTypeById.containsKey(value) + ? _lensTypeById.get(value) + : String.format("Unknown (%d)", value); + } + + @Nullable + public String getMaxApertureDescription() + { + Integer value = _directory.getInteger(CameraSettings.TAG_MAX_APERTURE); + if (value == null) + return null; + if (value > 512) + return String.format("Unknown (%d)", value); + return getFStopDescription(Math.exp(decodeCanonEv(value) * Math.log(2.0) / 2.0)); + } + + @Nullable + public String getMinApertureDescription() + { + Integer value = _directory.getInteger(CameraSettings.TAG_MIN_APERTURE); + if (value == null) + return null; + if (value > 512) + return String.format("Unknown (%d)", value); + return getFStopDescription(Math.exp(decodeCanonEv(value) * Math.log(2.0) / 2.0)); + } + @Nullable public String getAfPointSelectedDescription() { @@ -484,7 +570,7 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect // Canon PowerShot S3 is special int canonMask = 0x4000; - if ((value & canonMask) > 0) + if ((value & canonMask) != 0) return "" + (value & ~canonMask); switch (value) { @@ -661,8 +747,8 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect if (value == 0) { return "Self timer not used"; } else { - // TODO find an image that tests this calculation - return Double.toString((double)value * 0.1d) + " sec"; + DecimalFormat format = new DecimalFormat("0.##"); + return format.format((double)value * 0.1d) + " sec"; } } @@ -684,6 +770,12 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect return getIndexedDescription(CameraSettings.TAG_DIGITAL_ZOOM, "No digital zoom", "2x", "4x"); } + @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"); + } + @Nullable public String getFocusTypeDescription() { @@ -709,4 +801,355 @@ public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirect { return getIndexedDescription(CameraSettings.TAG_FLASH_ACTIVITY, "Flash did not fire", "Flash fired"); } + + @Nullable + public String getFocusContinuousDescription() + { + return getIndexedDescription(CameraSettings.TAG_FOCUS_CONTINUOUS, 0, + "Single", "Continuous", null, null, null, null, null, null, "Manual"); + } + + @Nullable + public String getAESettingDescription() + { + return getIndexedDescription(CameraSettings.TAG_AE_SETTING, 0, + "Normal AE", "Exposure Compensation", "AE Lock", "AE Lock + Exposure Comp.", "No AE"); + } + + @Nullable + public String getDisplayApertureDescription() + { + Integer value = _directory.getInteger(CameraSettings.TAG_DISPLAY_APERTURE); + if (value == null) + return null; + + if (value == 0xFFFF) + return value.toString(); + return getFStopDescription(value / 10f); + } + + @Nullable + public String getSpotMeteringModeDescription() + { + return getIndexedDescription(CanonMakernoteDirectory.CameraSettings.TAG_SPOT_METERING_MODE, 0, + "Center", "AF Point"); + } + + @Nullable + public String getPhotoEffectDescription() + { + Integer value = _directory.getInteger(CameraSettings.TAG_PHOTO_EFFECT); + if (value == null) + return null; + + switch (value) + { + case 0: + return "Off"; + case 1: + return "Vivid"; + case 2: + return "Neutral"; + case 3: + return "Smooth"; + case 4: + return "Sepia"; + case 5: + return "B&W"; + case 6: + return "Custom"; + case 100: + return "My Color Data"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getManualFlashOutputDescription() + { + Integer value = _directory.getInteger(CameraSettings.TAG_MANUAL_FLASH_OUTPUT); + if (value == null) + return null; + + switch (value) + { + case 0: + return "n/a"; + case 0x500: + return "Full"; + case 0x502: + return "Medium"; + case 0x504: + return "Low"; + case 0x7fff: + return "n/a"; // (EOS models) + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getColorToneDescription() + { + Integer value = _directory.getInteger(CameraSettings.TAG_COLOR_TONE); + if (value == null) + return null; + + return value == 0x7fff ? "n/a" : value.toString(); + } + + @Nullable + public String getSRawQualityDescription() + { + return getIndexedDescription(CanonMakernoteDirectory.CameraSettings.TAG_SRAW_QUALITY, 0, "n/a", "sRAW1 (mRAW)", "sRAW2 (sRAW)"); + } + + /** + * Canon hex-based EV (modulo 0x20) to real number. + * + * Converted from Exiftool version 10.10 created by Phil Harvey + * http://www.sno.phy.queensu.ca/~phil/exiftool/ + * lib\Image\ExifTool\Canon.pm + * + * eg) 0x00 -> 0 + * 0x0c -> 0.33333 + * 0x10 -> 0.5 + * 0x14 -> 0.66666 + * 0x20 -> 1 ... etc + */ + private double decodeCanonEv(int val) + { + int sign = 1; + if (val < 0) + { + val = -val; + sign = -1; + } + + int frac = val & 0x1f; + val -= frac; + + if (frac == 0x0c) + frac = 0x20 / 3; + else if (frac == 0x14) + frac = 0x40 / 3; + + return sign * (val + frac) / (double)0x20; + } + + /** + * Map from <see cref="CanonMakernoteDirectory.CameraSettings.TagLensType"/> to string descriptions. + * + * Data sourced from http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html#LensType + * + * Note that only Canon lenses are listed. Lenses from other manufacturers may identify themselves to the camera + * as being from this set, but in fact may be quite different. This limits the usefulness of this data, + * unfortunately. + */ + private static final HashMap<Integer, String> _lensTypeById = new HashMap<Integer, String>(); + + static { + _lensTypeById.put(1, "Canon EF 50mm f/1.8"); + _lensTypeById.put(2, "Canon EF 28mm f/2.8"); + _lensTypeById.put(3, "Canon EF 135mm f/2.8 Soft"); + _lensTypeById.put(4, "Canon EF 35-105mm f/3.5-4.5 or Sigma Lens"); + _lensTypeById.put(5, "Canon EF 35-70mm f/3.5-4.5"); + _lensTypeById.put(6, "Canon EF 28-70mm f/3.5-4.5 or Sigma or Tokina Lens"); + _lensTypeById.put(7, "Canon EF 100-300mm f/5.6L"); + _lensTypeById.put(8, "Canon EF 100-300mm f/5.6 or Sigma or Tokina Lens"); + _lensTypeById.put(9, "Canon EF 70-210mm f/4"); + _lensTypeById.put(10, "Canon EF 50mm f/2.5 Macro or Sigma Lens"); + _lensTypeById.put(11, "Canon EF 35mm f/2"); + _lensTypeById.put(13, "Canon EF 15mm f/2.8 Fisheye"); + _lensTypeById.put(14, "Canon EF 50-200mm f/3.5-4.5L"); + _lensTypeById.put(15, "Canon EF 50-200mm f/3.5-4.5"); + _lensTypeById.put(16, "Canon EF 35-135mm f/3.5-4.5"); + _lensTypeById.put(17, "Canon EF 35-70mm f/3.5-4.5A"); + _lensTypeById.put(18, "Canon EF 28-70mm f/3.5-4.5"); + _lensTypeById.put(20, "Canon EF 100-200mm f/4.5A"); + _lensTypeById.put(21, "Canon EF 80-200mm f/2.8L"); + _lensTypeById.put(22, "Canon EF 20-35mm f/2.8L or Tokina Lens"); + _lensTypeById.put(23, "Canon EF 35-105mm f/3.5-4.5"); + _lensTypeById.put(24, "Canon EF 35-80mm f/4-5.6 Power Zoom"); + _lensTypeById.put(25, "Canon EF 35-80mm f/4-5.6 Power Zoom"); + _lensTypeById.put(26, "Canon EF 100mm f/2.8 Macro or Other Lens"); + _lensTypeById.put(27, "Canon EF 35-80mm f/4-5.6"); + _lensTypeById.put(28, "Canon EF 80-200mm f/4.5-5.6 or Tamron Lens"); + _lensTypeById.put(29, "Canon EF 50mm f/1.8 II"); + _lensTypeById.put(30, "Canon EF 35-105mm f/4.5-5.6"); + _lensTypeById.put(31, "Canon EF 75-300mm f/4-5.6 or Tamron Lens"); + _lensTypeById.put(32, "Canon EF 24mm f/2.8 or Sigma Lens"); + _lensTypeById.put(33, "Voigtlander or Carl Zeiss Lens"); + _lensTypeById.put(35, "Canon EF 35-80mm f/4-5.6"); + _lensTypeById.put(36, "Canon EF 38-76mm f/4.5-5.6"); + _lensTypeById.put(37, "Canon EF 35-80mm f/4-5.6 or Tamron Lens"); + _lensTypeById.put(38, "Canon EF 80-200mm f/4.5-5.6"); + _lensTypeById.put(39, "Canon EF 75-300mm f/4-5.6"); + _lensTypeById.put(40, "Canon EF 28-80mm f/3.5-5.6"); + _lensTypeById.put(41, "Canon EF 28-90mm f/4-5.6"); + _lensTypeById.put(42, "Canon EF 28-200mm f/3.5-5.6 or Tamron Lens"); + _lensTypeById.put(43, "Canon EF 28-105mm f/4-5.6"); + _lensTypeById.put(44, "Canon EF 90-300mm f/4.5-5.6"); + _lensTypeById.put(45, "Canon EF-S 18-55mm f/3.5-5.6 [II]"); + _lensTypeById.put(46, "Canon EF 28-90mm f/4-5.6"); + _lensTypeById.put(47, "Zeiss Milvus 35mm f/2 or 50mm f/2"); + _lensTypeById.put(48, "Canon EF-S 18-55mm f/3.5-5.6 IS"); + _lensTypeById.put(49, "Canon EF-S 55-250mm f/4-5.6 IS"); + _lensTypeById.put(50, "Canon EF-S 18-200mm f/3.5-5.6 IS"); + _lensTypeById.put(51, "Canon EF-S 18-135mm f/3.5-5.6 IS"); + _lensTypeById.put(52, "Canon EF-S 18-55mm f/3.5-5.6 IS II"); + _lensTypeById.put(53, "Canon EF-S 18-55mm f/3.5-5.6 III"); + _lensTypeById.put(54, "Canon EF-S 55-250mm f/4-5.6 IS II"); + _lensTypeById.put(94, "Canon TS-E 17mm f/4L"); + _lensTypeById.put(95, "Canon TS-E 24.0mm f/3.5 L II"); + _lensTypeById.put(124, "Canon MP-E 65mm f/2.8 1-5x Macro Photo"); + _lensTypeById.put(125, "Canon TS-E 24mm f/3.5L"); + _lensTypeById.put(126, "Canon TS-E 45mm f/2.8"); + _lensTypeById.put(127, "Canon TS-E 90mm f/2.8"); + _lensTypeById.put(129, "Canon EF 300mm f/2.8L"); + _lensTypeById.put(130, "Canon EF 50mm f/1.0L"); + _lensTypeById.put(131, "Canon EF 28-80mm f/2.8-4L or Sigma Lens"); + _lensTypeById.put(132, "Canon EF 1200mm f/5.6L"); + _lensTypeById.put(134, "Canon EF 600mm f/4L IS"); + _lensTypeById.put(135, "Canon EF 200mm f/1.8L"); + _lensTypeById.put(136, "Canon EF 300mm f/2.8L"); + _lensTypeById.put(137, "Canon EF 85mm f/1.2L or Sigma or Tamron Lens"); + _lensTypeById.put(138, "Canon EF 28-80mm f/2.8-4L"); + _lensTypeById.put(139, "Canon EF 400mm f/2.8L"); + _lensTypeById.put(140, "Canon EF 500mm f/4.5L"); + _lensTypeById.put(141, "Canon EF 500mm f/4.5L"); + _lensTypeById.put(142, "Canon EF 300mm f/2.8L IS"); + _lensTypeById.put(143, "Canon EF 500mm f/4L IS or Sigma Lens"); + _lensTypeById.put(144, "Canon EF 35-135mm f/4-5.6 USM"); + _lensTypeById.put(145, "Canon EF 100-300mm f/4.5-5.6 USM"); + _lensTypeById.put(146, "Canon EF 70-210mm f/3.5-4.5 USM"); + _lensTypeById.put(147, "Canon EF 35-135mm f/4-5.6 USM"); + _lensTypeById.put(148, "Canon EF 28-80mm f/3.5-5.6 USM"); + _lensTypeById.put(149, "Canon EF 100mm f/2 USM"); + _lensTypeById.put(150, "Canon EF 14mm f/2.8L or Sigma Lens"); + _lensTypeById.put(151, "Canon EF 200mm f/2.8L"); + _lensTypeById.put(152, "Canon EF 300mm f/4L IS or Sigma Lens"); + _lensTypeById.put(153, "Canon EF 35-350mm f/3.5-5.6L or Sigma or Tamron Lens"); + _lensTypeById.put(154, "Canon EF 20mm f/2.8 USM or Zeiss Lens"); + _lensTypeById.put(155, "Canon EF 85mm f/1.8 USM"); + _lensTypeById.put(156, "Canon EF 28-105mm f/3.5-4.5 USM or Tamron Lens"); + _lensTypeById.put(160, "Canon EF 20-35mm f/3.5-4.5 USM or Tamron or Tokina Lens"); + _lensTypeById.put(161, "Canon EF 28-70mm f/2.8L or Sigma or Tamron Lens"); + _lensTypeById.put(162, "Canon EF 200mm f/2.8L"); + _lensTypeById.put(163, "Canon EF 300mm f/4L"); + _lensTypeById.put(164, "Canon EF 400mm f/5.6L"); + _lensTypeById.put(165, "Canon EF 70-200mm f/2.8 L"); + _lensTypeById.put(166, "Canon EF 70-200mm f/2.8 L + 1.4x"); + _lensTypeById.put(167, "Canon EF 70-200mm f/2.8 L + 2x"); + _lensTypeById.put(168, "Canon EF 28mm f/1.8 USM or Sigma Lens"); + _lensTypeById.put(169, "Canon EF 17-35mm f/2.8L or Sigma Lens"); + _lensTypeById.put(170, "Canon EF 200mm f/2.8L II"); + _lensTypeById.put(171, "Canon EF 300mm f/4L"); + _lensTypeById.put(172, "Canon EF 400mm f/5.6L or Sigma Lens"); + _lensTypeById.put(173, "Canon EF 180mm Macro f/3.5L or Sigma Lens"); + _lensTypeById.put(174, "Canon EF 135mm f/2L or Other Lens"); + _lensTypeById.put(175, "Canon EF 400mm f/2.8L"); + _lensTypeById.put(176, "Canon EF 24-85mm f/3.5-4.5 USM"); + _lensTypeById.put(177, "Canon EF 300mm f/4L IS"); + _lensTypeById.put(178, "Canon EF 28-135mm f/3.5-5.6 IS"); + _lensTypeById.put(179, "Canon EF 24mm f/1.4L"); + _lensTypeById.put(180, "Canon EF 35mm f/1.4L or Other Lens"); + _lensTypeById.put(181, "Canon EF 100-400mm f/4.5-5.6L IS + 1.4x or Sigma Lens"); + _lensTypeById.put(182, "Canon EF 100-400mm f/4.5-5.6L IS + 2x or Sigma Lens"); + _lensTypeById.put(183, "Canon EF 100-400mm f/4.5-5.6L IS or Sigma Lens"); + _lensTypeById.put(184, "Canon EF 400mm f/2.8L + 2x"); + _lensTypeById.put(185, "Canon EF 600mm f/4L IS"); + _lensTypeById.put(186, "Canon EF 70-200mm f/4L"); + _lensTypeById.put(187, "Canon EF 70-200mm f/4L + 1.4x"); + _lensTypeById.put(188, "Canon EF 70-200mm f/4L + 2x"); + _lensTypeById.put(189, "Canon EF 70-200mm f/4L + 2.8x"); + _lensTypeById.put(190, "Canon EF 100mm f/2.8 Macro USM"); + _lensTypeById.put(191, "Canon EF 400mm f/4 DO IS"); + _lensTypeById.put(193, "Canon EF 35-80mm f/4-5.6 USM"); + _lensTypeById.put(194, "Canon EF 80-200mm f/4.5-5.6 USM"); + _lensTypeById.put(195, "Canon EF 35-105mm f/4.5-5.6 USM"); + _lensTypeById.put(196, "Canon EF 75-300mm f/4-5.6 USM"); + _lensTypeById.put(197, "Canon EF 75-300mm f/4-5.6 IS USM"); + _lensTypeById.put(198, "Canon EF 50mm f/1.4 USM or Zeiss Lens"); + _lensTypeById.put(199, "Canon EF 28-80mm f/3.5-5.6 USM"); + _lensTypeById.put(200, "Canon EF 75-300mm f/4-5.6 USM"); + _lensTypeById.put(201, "Canon EF 28-80mm f/3.5-5.6 USM"); + _lensTypeById.put(202, "Canon EF 28-80mm f/3.5-5.6 USM IV"); + _lensTypeById.put(208, "Canon EF 22-55mm f/4-5.6 USM"); + _lensTypeById.put(209, "Canon EF 55-200mm f/4.5-5.6"); + _lensTypeById.put(210, "Canon EF 28-90mm f/4-5.6 USM"); + _lensTypeById.put(211, "Canon EF 28-200mm f/3.5-5.6 USM"); + _lensTypeById.put(212, "Canon EF 28-105mm f/4-5.6 USM"); + _lensTypeById.put(213, "Canon EF 90-300mm f/4.5-5.6 USM or Tamron Lens"); + _lensTypeById.put(214, "Canon EF-S 18-55mm f/3.5-5.6 USM"); + _lensTypeById.put(215, "Canon EF 55-200mm f/4.5-5.6 II USM"); + _lensTypeById.put(217, "Tamron AF 18-270mm f/3.5-6.3 Di II VC PZD"); + _lensTypeById.put(224, "Canon EF 70-200mm f/2.8L IS"); + _lensTypeById.put(225, "Canon EF 70-200mm f/2.8L IS + 1.4x"); + _lensTypeById.put(226, "Canon EF 70-200mm f/2.8L IS + 2x"); + _lensTypeById.put(227, "Canon EF 70-200mm f/2.8L IS + 2.8x"); + _lensTypeById.put(228, "Canon EF 28-105mm f/3.5-4.5 USM"); + _lensTypeById.put(229, "Canon EF 16-35mm f/2.8L"); + _lensTypeById.put(230, "Canon EF 24-70mm f/2.8L"); + _lensTypeById.put(231, "Canon EF 17-40mm f/4L"); + _lensTypeById.put(232, "Canon EF 70-300mm f/4.5-5.6 DO IS USM"); + _lensTypeById.put(233, "Canon EF 28-300mm f/3.5-5.6L IS"); + _lensTypeById.put(234, "Canon EF-S 17-85mm f/4-5.6 IS USM or Tokina Lens"); + _lensTypeById.put(235, "Canon EF-S 10-22mm f/3.5-4.5 USM"); + _lensTypeById.put(236, "Canon EF-S 60mm f/2.8 Macro USM"); + _lensTypeById.put(237, "Canon EF 24-105mm f/4L IS"); + _lensTypeById.put(238, "Canon EF 70-300mm f/4-5.6 IS USM"); + _lensTypeById.put(239, "Canon EF 85mm f/1.2L II"); + _lensTypeById.put(240, "Canon EF-S 17-55mm f/2.8 IS USM"); + _lensTypeById.put(241, "Canon EF 50mm f/1.2L"); + _lensTypeById.put(242, "Canon EF 70-200mm f/4L IS"); + _lensTypeById.put(243, "Canon EF 70-200mm f/4L IS + 1.4x"); + _lensTypeById.put(244, "Canon EF 70-200mm f/4L IS + 2x"); + _lensTypeById.put(245, "Canon EF 70-200mm f/4L IS + 2.8x"); + _lensTypeById.put(246, "Canon EF 16-35mm f/2.8L II"); + _lensTypeById.put(247, "Canon EF 14mm f/2.8L II USM"); + _lensTypeById.put(248, "Canon EF 200mm f/2L IS or Sigma Lens"); + _lensTypeById.put(249, "Canon EF 800mm f/5.6L IS"); + _lensTypeById.put(250, "Canon EF 24mm f/1.4L II or Sigma Lens"); + _lensTypeById.put(251, "Canon EF 70-200mm f/2.8L IS II USM"); + _lensTypeById.put(252, "Canon EF 70-200mm f/2.8L IS II USM + 1.4x"); + _lensTypeById.put(253, "Canon EF 70-200mm f/2.8L IS II USM + 2x"); + _lensTypeById.put(254, "Canon EF 100mm f/2.8L Macro IS USM"); + _lensTypeById.put(255, "Sigma 24-105mm f/4 DG OS HSM | A or Other Sigma Lens"); + _lensTypeById.put(488, "Canon EF-S 15-85mm f/3.5-5.6 IS USM"); + _lensTypeById.put(489, "Canon EF 70-300mm f/4-5.6L IS USM"); + _lensTypeById.put(490, "Canon EF 8-15mm f/4L Fisheye USM"); + _lensTypeById.put(491, "Canon EF 300mm f/2.8L IS II USM"); + _lensTypeById.put(492, "Canon EF 400mm f/2.8L IS II USM"); + _lensTypeById.put(493, "Canon EF 500mm f/4L IS II USM or EF 24-105mm f4L IS USM"); + _lensTypeById.put(494, "Canon EF 600mm f/4.0L IS II USM"); + _lensTypeById.put(495, "Canon EF 24-70mm f/2.8L II USM"); + _lensTypeById.put(496, "Canon EF 200-400mm f/4L IS USM"); + _lensTypeById.put(499, "Canon EF 200-400mm f/4L IS USM + 1.4x"); + _lensTypeById.put(502, "Canon EF 28mm f/2.8 IS USM"); + _lensTypeById.put(503, "Canon EF 24mm f/2.8 IS USM"); + _lensTypeById.put(504, "Canon EF 24-70mm f/4L IS USM"); + _lensTypeById.put(505, "Canon EF 35mm f/2 IS USM"); + _lensTypeById.put(506, "Canon EF 400mm f/4 DO IS II USM"); + _lensTypeById.put(507, "Canon EF 16-35mm f/4L IS USM"); + _lensTypeById.put(508, "Canon EF 11-24mm f/4L USM"); + _lensTypeById.put(747, "Canon EF 100-400mm f/4.5-5.6L IS II USM"); + _lensTypeById.put(750, "Canon EF 35mm f/1.4L II USM"); + _lensTypeById.put(4142, "Canon EF-S 18-135mm f/3.5-5.6 IS STM"); + _lensTypeById.put(4143, "Canon EF-M 18-55mm f/3.5-5.6 IS STM or Tamron Lens"); + _lensTypeById.put(4144, "Canon EF 40mm f/2.8 STM"); + _lensTypeById.put(4145, "Canon EF-M 22mm f/2 STM"); + _lensTypeById.put(4146, "Canon EF-S 18-55mm f/3.5-5.6 IS STM"); + _lensTypeById.put(4147, "Canon EF-M 11-22mm f/4-5.6 IS STM"); + _lensTypeById.put(4148, "Canon EF-S 55-250mm f/4-5.6 IS STM"); + _lensTypeById.put(4149, "Canon EF-M 55-200mm f/4.5-6.3 IS STM"); + _lensTypeById.put(4150, "Canon EF-S 10-18mm f/4.5-5.6 IS STM"); + _lensTypeById.put(4152, "Canon EF 24-105mm f/3.5-5.6 IS STM"); + _lensTypeById.put(4153, "Canon EF-M 15-45mm f/3.5-6.3 IS STM"); + _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"); + _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 72da6b6..cfe9c50 100644 --- a/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/CanonMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class CanonMakernoteDirectory extends Directory { // These TAG_*_ARRAY Exif tags map to arrays of int16 values which are split out into separate 'fake' tags. @@ -158,7 +159,7 @@ public class CanonMakernoteDirectory extends Directory */ public static final int TAG_FOCUS_MODE_1 = OFFSET + 0x07; public static final int TAG_UNKNOWN_3 = OFFSET + 0x08; - public static final int TAG_UNKNOWN_4 = OFFSET + 0x09; + public static final int TAG_RECORD_MODE = OFFSET + 0x09; /** * 0 = Large * 1 = Medium @@ -244,25 +245,36 @@ public class CanonMakernoteDirectory extends Directory */ public static final int TAG_EXPOSURE_MODE = OFFSET + 0x14; public static final int TAG_UNKNOWN_7 = OFFSET + 0x15; - public static final int TAG_UNKNOWN_8 = OFFSET + 0x16; + public static final int TAG_LENS_TYPE = OFFSET + 0x16; public static final int TAG_LONG_FOCAL_LENGTH = OFFSET + 0x17; public static final int TAG_SHORT_FOCAL_LENGTH = OFFSET + 0x18; public static final int TAG_FOCAL_UNITS_PER_MM = OFFSET + 0x19; - public static final int TAG_UNKNOWN_9 = OFFSET + 0x1A; - public static final int TAG_UNKNOWN_10 = OFFSET + 0x1B; + public static final int TAG_MAX_APERTURE = OFFSET + 0x1A; + public static final int TAG_MIN_APERTURE = OFFSET + 0x1B; /** * 0 = Flash Did Not Fire * 1 = Flash Fired */ public static final int TAG_FLASH_ACTIVITY = OFFSET + 0x1C; public static final int TAG_FLASH_DETAILS = OFFSET + 0x1D; - public static final int TAG_UNKNOWN_12 = OFFSET + 0x1E; - public static final int TAG_UNKNOWN_13 = OFFSET + 0x1F; + public static final int TAG_FOCUS_CONTINUOUS = OFFSET + 0x1E; + public static final int TAG_AE_SETTING = OFFSET + 0x1F; /** * 0 = Focus Mode: Single * 1 = Focus Mode: Continuous */ public static final int TAG_FOCUS_MODE_2 = OFFSET + 0x20; + + public static final int TAG_DISPLAY_APERTURE = OFFSET + 0x21; + public static final int TAG_ZOOM_SOURCE_WIDTH = OFFSET + 0x22; + public static final int TAG_ZOOM_TARGET_WIDTH = OFFSET + 0x23; + + public static final int TAG_SPOT_METERING_MODE = OFFSET + 0x25; + public static final int TAG_PHOTO_EFFECT = OFFSET + 0x26; + public static final int TAG_MANUAL_FLASH_OUTPUT = OFFSET + 0x27; + + public static final int TAG_COLOR_TONE = OFFSET + 0x29; + public static final int TAG_SRAW_QUALITY = OFFSET + 0x2D; } public final static class FocalLength @@ -514,16 +526,24 @@ public class CanonMakernoteDirectory extends Directory _tagNameMap.put(CameraSettings.TAG_QUALITY, "Quality"); _tagNameMap.put(CameraSettings.TAG_UNKNOWN_2, "Unknown Camera Setting 2"); _tagNameMap.put(CameraSettings.TAG_UNKNOWN_3, "Unknown Camera Setting 3"); - _tagNameMap.put(CameraSettings.TAG_UNKNOWN_4, "Unknown Camera Setting 4"); + _tagNameMap.put(CameraSettings.TAG_RECORD_MODE, "Record Mode"); _tagNameMap.put(CameraSettings.TAG_DIGITAL_ZOOM, "Digital Zoom"); _tagNameMap.put(CameraSettings.TAG_FOCUS_TYPE, "Focus Type"); _tagNameMap.put(CameraSettings.TAG_UNKNOWN_7, "Unknown Camera Setting 7"); - _tagNameMap.put(CameraSettings.TAG_UNKNOWN_8, "Unknown Camera Setting 8"); - _tagNameMap.put(CameraSettings.TAG_UNKNOWN_9, "Unknown Camera Setting 9"); - _tagNameMap.put(CameraSettings.TAG_UNKNOWN_10, "Unknown Camera Setting 10"); + _tagNameMap.put(CameraSettings.TAG_LENS_TYPE, "Lens Type"); + _tagNameMap.put(CameraSettings.TAG_MAX_APERTURE, "Max Aperture"); + _tagNameMap.put(CameraSettings.TAG_MIN_APERTURE, "Min Aperture"); _tagNameMap.put(CameraSettings.TAG_FLASH_ACTIVITY, "Flash Activity"); - _tagNameMap.put(CameraSettings.TAG_UNKNOWN_12, "Unknown Camera Setting 12"); - _tagNameMap.put(CameraSettings.TAG_UNKNOWN_13, "Unknown Camera Setting 13"); + _tagNameMap.put(CameraSettings.TAG_FOCUS_CONTINUOUS, "Focus Continuous"); + _tagNameMap.put(CameraSettings.TAG_AE_SETTING, "AE Setting"); + _tagNameMap.put(CameraSettings.TAG_DISPLAY_APERTURE, "Display Aperture"); + _tagNameMap.put(CameraSettings.TAG_ZOOM_SOURCE_WIDTH, "Zoom Source Width"); + _tagNameMap.put(CameraSettings.TAG_ZOOM_TARGET_WIDTH, "Zoom Target Width"); + _tagNameMap.put(CameraSettings.TAG_SPOT_METERING_MODE, "Spot Metering Mode"); + _tagNameMap.put(CameraSettings.TAG_PHOTO_EFFECT, "Photo Effect"); + _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(FocalLength.TAG_WHITE_BALANCE, "White Balance"); _tagNameMap.put(FocalLength.TAG_SEQUENCE_NUMBER, "Sequence Number"); @@ -575,7 +595,7 @@ public class CanonMakernoteDirectory extends Directory _tagNameMap.put(AFInfo.TAG_AF_AREA_HEIGHT, "AF Area Height"); _tagNameMap.put(AFInfo.TAG_AF_AREA_X_POSITIONS, "AF Area X Positions"); _tagNameMap.put(AFInfo.TAG_AF_AREA_Y_POSITIONS, "AF Area Y Positions"); - _tagNameMap.put(AFInfo.TAG_AF_POINTS_IN_FOCUS, "AF Points in Focus Count"); + _tagNameMap.put(AFInfo.TAG_AF_POINTS_IN_FOCUS, "AF Points in Focus"); _tagNameMap.put(AFInfo.TAG_PRIMARY_AF_POINT_1, "Primary AF Point 1"); _tagNameMap.put(AFInfo.TAG_PRIMARY_AF_POINT_2, "Primary AF Point 2"); @@ -671,6 +691,12 @@ public class CanonMakernoteDirectory extends Directory { // TODO is there some way to drop out 'null' or 'zero' values that are present in the array to reduce the noise? + if (!(array instanceof int[])) { + // no special handling... + super.setObjectArray(tagType, array); + return; + } + // Certain Canon tags contain arrays of values that we split into 'fake' tags as each // index in the array has its own meaning and decoding. // Pick those tags out here and throw away the original array. @@ -708,9 +734,59 @@ public class CanonMakernoteDirectory extends Directory // setInt(subTagTypeBase + i + 1, ints[i] & 0x0F); // break; case TAG_AF_INFO_ARRAY: { - int[] ints = (int[])array; - for (int i = 0; i < ints.length; i++) - setInt(AFInfo.OFFSET + i, ints[i]); + // Notes from Exiftool 10.10 by Phil Harvey, lib\Image\Exiftool\Canon.pm: + // Auto-focus information used by many older Canon models. The values in this + // record are sequential, and some have variable sizes based on the value of + // numafpoints (which may be 1,5,7,9,15,45, or 53). The AFArea coordinates are + // given in a system where the image has dimensions given by AFImageWidth and + // AFImageHeight, and 0,0 is the image center. The direction of the Y axis + // depends on the camera model, with positive Y upwards for EOS models, but + // apparently downwards for PowerShot models. + + // AFInfo is another array with 'fake' tags. The first int of the array contains + // the number of AF points. Iterate through the array one byte at a time, generally + // assuming one byte corresponds to one tag UNLESS certain tag numbers are encountered. + // For these, read specific subsequent bytes from the array based on the tag type. The + // number of bytes read can vary. + + int[] values = (int[])array; + int numafpoints = values[0]; + int tagnumber = 0; + for (int i = 0; i < values.length; i++) + { + // These two tags store 'numafpoints' bytes of data in the array + if (AFInfo.OFFSET + tagnumber == AFInfo.TAG_AF_AREA_X_POSITIONS || + AFInfo.OFFSET + tagnumber == AFInfo.TAG_AF_AREA_Y_POSITIONS) + { + // There could be incorrect data in the array, so boundary check + if (values.length - 1 >= (i + numafpoints)) + { + short[] areaPositions = new short[numafpoints]; + for (int j = 0; j < areaPositions.length; j++) + areaPositions[j] = (short)values[i + j]; + + super.setObjectArray(AFInfo.OFFSET + tagnumber, areaPositions); + } + i += numafpoints - 1; // assume these bytes are processed and skip + } + else if (AFInfo.OFFSET + tagnumber == AFInfo.TAG_AF_POINTS_IN_FOCUS) + { + short[] pointsInFocus = new short[((numafpoints + 15) / 16)]; + + // There could be incorrect data in the array, so boundary check + if (values.length - 1 >= (i + pointsInFocus.length)) + { + for (int j = 0; j < pointsInFocus.length; j++) + pointsInFocus[j] = (short)values[i + j]; + + super.setObjectArray(AFInfo.OFFSET + tagnumber, pointsInFocus); + } + i += pointsInFocus.length - 1; // assume these bytes are processed and skip + } + else + super.setObjectArray(AFInfo.OFFSET + tagnumber, values[i]); + tagnumber++; + } break; } default: { diff --git a/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java index 148dd79..5207cef 100644 --- a/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import static com.drew.metadata.exif.makernotes.CasioType1MakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class CasioType1MakernoteDescriptor extends TagDescriptor<CasioType1MakernoteDirectory> { public CasioType1MakernoteDescriptor(@NotNull CasioType1MakernoteDirectory directory) @@ -154,11 +155,7 @@ public class CasioType1MakernoteDescriptor extends TagDescriptor<CasioType1Maker public String getObjectDistanceDescription() { Integer value = _directory.getInteger(TAG_OBJECT_DISTANCE); - - if (value == null) - return null; - - return value + " mm"; + return value == null ? null : getFocalLengthDescription(value); } @Nullable diff --git a/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java index 2c4fe85..04fcaad 100644 --- a/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/CasioType1MakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class CasioType1MakernoteDirectory extends Directory { public static final int TAG_RECORDING_MODE = 0x0001; diff --git a/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java index 4a83f0b..3cf2d1b 100644 --- a/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import static com.drew.metadata.exif.makernotes.CasioType2MakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2MakernoteDirectory> { public CasioType2MakernoteDescriptor(@NotNull CasioType2MakernoteDirectory directory) @@ -67,8 +68,6 @@ public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2Maker return getContrastDescription(); case TAG_SHARPNESS: return getSharpnessDescription(); - case TAG_PRINT_IMAGE_MATCHING_INFO: - return getPrintImageMatchingInfoDescription(); case TAG_PREVIEW_THUMBNAIL: return getCasioPreviewThumbnailDescription(); case TAG_WHITE_BALANCE_BIAS: @@ -210,13 +209,6 @@ public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2Maker return "<" + bytes.length + " bytes of image data>"; } - @Nullable - public String getPrintImageMatchingInfoDescription() - { - // TODO research PIM specification http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html - return _directory.getString(TAG_PRINT_IMAGE_MATCHING_INFO); - } - @Nullable public String getSharpnessDescription() { @@ -239,9 +231,7 @@ public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2Maker public String getFocalLengthDescription() { Double value = _directory.getDoubleObject(TAG_FOCAL_LENGTH); - if (value == null) - return null; - return Double.toString(value / 10d) + " mm"; + return value == null ? null : getFocalLengthDescription(value / 10d); } @Nullable diff --git a/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java index 9648e33..55b273e 100644 --- a/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/CasioType2MakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class CasioType2MakernoteDirectory extends Directory { /** diff --git a/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java index 7ae3bbb..e033998 100644 --- a/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,6 +48,7 @@ import static com.drew.metadata.exif.makernotes.FujifilmMakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class FujifilmMakernoteDescriptor extends TagDescriptor<FujifilmMakernoteDirectory> { public FujifilmMakernoteDescriptor(@NotNull FujifilmMakernoteDirectory directory) diff --git a/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java index a5aa44e..047bbdb 100644 --- a/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/FujifilmMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class FujifilmMakernoteDirectory extends Directory { public static final int TAG_MAKERNOTE_VERSION = 0x0000; diff --git a/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java index 8f2c1aa..eedebe1 100644 --- a/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import static com.drew.metadata.exif.makernotes.KodakMakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class KodakMakernoteDescriptor extends TagDescriptor<KodakMakernoteDirectory> { public KodakMakernoteDescriptor(@NotNull KodakMakernoteDirectory directory) diff --git a/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java index b60edc0..dbf9308 100644 --- a/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/KodakMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class KodakMakernoteDirectory extends Directory { public final static int TAG_KODAK_MODEL = 0; diff --git a/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java index c5a2935..65b0b80 100644 --- a/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ import static com.drew.metadata.exif.makernotes.KyoceraMakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class KyoceraMakernoteDescriptor extends TagDescriptor<KyoceraMakernoteDirectory> { public KyoceraMakernoteDescriptor(@NotNull KyoceraMakernoteDirectory directory) @@ -50,8 +51,6 @@ public class KyoceraMakernoteDescriptor extends TagDescriptor<KyoceraMakernoteDi public String getDescription(int tagType) { switch (tagType) { - case TAG_PRINT_IMAGE_MATCHING_INFO: - return getPrintImageMatchingInfoDescription(); case TAG_PROPRIETARY_THUMBNAIL: return getProprietaryThumbnailDataDescription(); default: @@ -59,12 +58,6 @@ public class KyoceraMakernoteDescriptor extends TagDescriptor<KyoceraMakernoteDi } } - @Nullable - public String getPrintImageMatchingInfoDescription() - { - return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO); - } - @Nullable public String getProprietaryThumbnailDataDescription() { diff --git a/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java index 7139f9c..f67c0a7 100644 --- a/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/KyoceraMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class KyoceraMakernoteDirectory extends Directory { public static final int TAG_PROPRIETARY_THUMBNAIL = 0x0001; diff --git a/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java index 1fb494c..af70827 100644 --- a/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import static com.drew.metadata.exif.makernotes.LeicaMakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class LeicaMakernoteDescriptor extends TagDescriptor<LeicaMakernoteDirectory> { public LeicaMakernoteDescriptor(@NotNull LeicaMakernoteDirectory directory) diff --git a/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java index a2e6c38..3d23dc2 100644 --- a/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/LeicaMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class LeicaMakernoteDirectory extends Directory { public static final int TAG_QUALITY = 0x0300; diff --git a/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java new file mode 100644 index 0000000..3096cb1 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDescriptor.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.makernotes.LeicaType5MakernoteDirectory.*; + +/** + * Provides human-readable string representations of tag values stored in a {@link LeicaType5MakernoteDirectory}. + * <p> + * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Panasonic.html + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class LeicaType5MakernoteDescriptor extends TagDescriptor<LeicaType5MakernoteDirectory> +{ + public LeicaType5MakernoteDescriptor(@NotNull LeicaType5MakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagExposureMode: + return getExposureModeDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getExposureModeDescription() + { + byte[] values = _directory.getByteArray(TagExposureMode); + if (values == null || values.length < 4) + return null; + + String join = String.format("%d %d %d %d", values[0], values[1], values[2], values[3]); + + if(join.equals("0 0 0 0")) + return "Program AE"; + else if(join.equals("1 0 0 0")) + return "Aperture-priority AE"; + else if(join.equals("1 1 0 0")) + return "Aperture-priority AE (1)"; + else if(join.equals("2 0 0 0")) + return "Shutter speed priority AE"; // guess + else if(join.equals("3 0 0 0")) + return "Manual"; + else + return String.format("Unknown (%s)", join); + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java new file mode 100644 index 0000000..ad8492e --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/LeicaType5MakernoteDirectory.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * Describes tags specific to certain Leica cameras. + * <p> + * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Panasonic.html + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class LeicaType5MakernoteDirectory extends Directory +{ + public static final int TagLensModel = 0x0303; + public static final int TagOriginalFileName = 0x0407; + public static final int TagOriginalDirectory = 0x0408; + public static final int TagExposureMode = 0x040d; + public static final int TagShotInfo = 0x0410; + public static final int TagFilmMode = 0x0412; + public static final int TagWbRgbLevels = 0x0413; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TagLensModel, "Lens Model"); + _tagNameMap.put(TagOriginalFileName, "Original File Name"); + _tagNameMap.put(TagOriginalDirectory, "Original Directory"); + _tagNameMap.put(TagExposureMode, "Exposure Mode"); + _tagNameMap.put(TagShotInfo, "Shot Info" ); + _tagNameMap.put(TagFilmMode, "Film Mode"); + _tagNameMap.put(TagWbRgbLevels, "WB RGB Levels"); + } + + public LeicaType5MakernoteDirectory() + { + this.setDescriptor(new LeicaType5MakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Leica Makernote"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java index 3df1370..0825052 100644 --- a/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ import static com.drew.metadata.exif.makernotes.NikonType1MakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class NikonType1MakernoteDescriptor extends TagDescriptor<NikonType1MakernoteDirectory> { public NikonType1MakernoteDescriptor(@NotNull NikonType1MakernoteDirectory directory) diff --git a/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java index ad7ac57..53bf7e6 100644 --- a/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/NikonType1MakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,6 +39,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class NikonType1MakernoteDirectory extends Directory { public static final int TAG_UNKNOWN_1 = 0x0002; diff --git a/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java index a55a911..7f1bf46 100644 --- a/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import static com.drew.metadata.exif.makernotes.NikonType2MakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class NikonType2MakernoteDescriptor extends TagDescriptor<NikonType2MakernoteDirectory> { public NikonType2MakernoteDescriptor(@NotNull NikonType2MakernoteDirectory directory) @@ -305,7 +306,7 @@ public class NikonType2MakernoteDescriptor extends TagDescriptor<NikonType2Maker private String getEVDescription(int tagType) { int[] values = _directory.getIntArray(tagType); - if (values == null) + if (values == null || values.length < 2) return null; if (values.length < 3 || values[2] == 0) return null; @@ -328,14 +329,7 @@ public class NikonType2MakernoteDescriptor extends TagDescriptor<NikonType2Maker @Nullable public String getLensDescription() { - Rational[] values = _directory.getRationalArray(TAG_LENS); - - return values == null - ? null - : values.length < 4 - ? _directory.getString(TAG_LENS) - : String.format("%d-%dmm f/%.1f-%.1f", values[0].intValue(), values[1].intValue(), values[2].floatValue(), values[3].floatValue()); - + return getLensSpecificationDescription(TAG_LENS); } @Nullable diff --git a/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java index 39249a1..3ffb795 100644 --- a/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/NikonType2MakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class NikonType2MakernoteDirectory extends Directory { /** @@ -759,7 +760,7 @@ public class NikonType2MakernoteDirectory extends Directory public static final int TAG_UNKNOWN_49 = 0x00BB; public static final int TAG_UNKNOWN_50 = 0x00BD; public static final int TAG_UNKNOWN_51 = 0x0103; - public static final int TAG_PRINT_IM = 0x0E00; + public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00; /** * Data about changes set by Nikon Capture Editor. @@ -892,7 +893,7 @@ public class NikonType2MakernoteDirectory extends Directory _tagNameMap.put(TAG_UNKNOWN_49, "Unknown 49"); _tagNameMap.put(TAG_UNKNOWN_50, "Unknown 50"); _tagNameMap.put(TAG_UNKNOWN_51, "Unknown 51"); - _tagNameMap.put(TAG_PRINT_IM, "Print IM"); + _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print IM"); _tagNameMap.put(TAG_UNKNOWN_52, "Unknown 52"); _tagNameMap.put(TAG_UNKNOWN_53, "Unknown 53"); _tagNameMap.put(TAG_NIKON_CAPTURE_VERSION, "Nikon Capture Version"); diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java new file mode 100644 index 0000000..c674c6a --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDescriptor.java @@ -0,0 +1,1412 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.Rational; +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import java.text.DecimalFormat; +import java.util.HashMap; + +import static com.drew.metadata.exif.makernotes.OlympusCameraSettingsMakernoteDirectory.*; + +/** + * Provides human-readable String representations of tag values stored in a {@link OlympusCameraSettingsMakernoteDirectory}. + * <p> + * Some Description functions and the Extender and Lens types lists converted from Exiftool version 10.10 created by Phil Harvey + * http://www.sno.phy.queensu.ca/~phil/exiftool/ + * lib\Image\ExifTool\Olympus.pm + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusCameraSettingsMakernoteDescriptor extends TagDescriptor<OlympusCameraSettingsMakernoteDirectory> +{ + public OlympusCameraSettingsMakernoteDescriptor(@NotNull OlympusCameraSettingsMakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagCameraSettingsVersion: + return getCameraSettingsVersionDescription(); + case TagPreviewImageValid: + return getPreviewImageValidDescription(); + + case TagExposureMode: + return getExposureModeDescription(); + case TagAeLock: + return getAeLockDescription(); + case TagMeteringMode: + return getMeteringModeDescription(); + case TagExposureShift: + return getExposureShiftDescription(); + case TagNdFilter: + return getNdFilterDescription(); + + case TagMacroMode: + return getMacroModeDescription(); + case TagFocusMode: + return getFocusModeDescription(); + case TagFocusProcess: + return getFocusProcessDescription(); + case TagAfSearch: + return getAfSearchDescription(); + case TagAfAreas: + return getAfAreasDescription(); + case TagAfPointSelected: + return getAfPointSelectedDescription(); + case TagAfFineTune: + return getAfFineTuneDescription(); + + case TagFlashMode: + return getFlashModeDescription(); + case TagFlashRemoteControl: + return getFlashRemoteControlDescription(); + case TagFlashControlMode: + return getFlashControlModeDescription(); + case TagFlashIntensity: + return getFlashIntensityDescription(); + case TagManualFlashStrength: + return getManualFlashStrengthDescription(); + + case TagWhiteBalance2: + return getWhiteBalance2Description(); + case TagWhiteBalanceTemperature: + return getWhiteBalanceTemperatureDescription(); + case TagCustomSaturation: + return getCustomSaturationDescription(); + case TagModifiedSaturation: + return getModifiedSaturationDescription(); + case TagContrastSetting: + return getContrastSettingDescription(); + case TagSharpnessSetting: + return getSharpnessSettingDescription(); + case TagColorSpace: + return getColorSpaceDescription(); + case TagSceneMode: + return getSceneModeDescription(); + case TagNoiseReduction: + return getNoiseReductionDescription(); + case TagDistortionCorrection: + return getDistortionCorrectionDescription(); + case TagShadingCompensation: + return getShadingCompensationDescription(); + case TagGradation: + return getGradationDescription(); + case TagPictureMode: + return getPictureModeDescription(); + case TagPictureModeSaturation: + return getPictureModeSaturationDescription(); + case TagPictureModeContrast: + return getPictureModeContrastDescription(); + case TagPictureModeSharpness: + return getPictureModeSharpnessDescription(); + case TagPictureModeBWFilter: + return getPictureModeBWFilterDescription(); + case TagPictureModeTone: + return getPictureModeToneDescription(); + case TagNoiseFilter: + return getNoiseFilterDescription(); + case TagArtFilter: + return getArtFilterDescription(); + case TagMagicFilter: + return getMagicFilterDescription(); + case TagPictureModeEffect: + return getPictureModeEffectDescription(); + case TagToneLevel: + return getToneLevelDescription(); + case TagArtFilterEffect: + return getArtFilterEffectDescription(); + case TagColorCreatorEffect: + return getColorCreatorEffectDescription(); + + case TagDriveMode: + return getDriveModeDescription(); + case TagPanoramaMode: + return getPanoramaModeDescription(); + case TagImageQuality2: + return getImageQuality2Description(); + case TagImageStabilization: + return getImageStabilizationDescription(); + + case TagStackedImage: + return getStackedImageDescription(); + + case TagManometerPressure: + return getManometerPressureDescription(); + case TagManometerReading: + return getManometerReadingDescription(); + case TagExtendedWBDetect: + return getExtendedWBDetectDescription(); + case TagRollAngle: + return getRollAngleDescription(); + case TagPitchAngle: + return getPitchAngleDescription(); + case TagDateTimeUtc: + return getDateTimeUTCDescription(); + + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getCameraSettingsVersionDescription() + { + return getVersionBytesDescription(TagCameraSettingsVersion, 4); + } + + @Nullable + public String getPreviewImageValidDescription() + { + return getIndexedDescription(TagPreviewImageValid, + "No", "Yes"); + } + + @Nullable + public String getExposureModeDescription() + { + return getIndexedDescription(TagExposureMode, 1, + "Manual", "Program", "Aperture-priority AE", "Shutter speed priority", "Program-shift"); + } + + @Nullable + public String getAeLockDescription() + { + return getIndexedDescription(TagAeLock, + "Off", "On"); + } + + @Nullable + public String getMeteringModeDescription() + { + Integer value = _directory.getInteger(TagMeteringMode); + if (value == null) + return null; + + switch (value) { + case 2: + return "Center-weighted average"; + case 3: + return "Spot"; + case 5: + return "ESP"; + case 261: + return "Pattern+AF"; + case 515: + return "Spot+Highlight control"; + case 1027: + return "Spot+Shadow control"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getExposureShiftDescription() + { + return getRationalOrDoubleString(TagExposureShift); + } + + @Nullable + public String getNdFilterDescription() + { + return getIndexedDescription(TagNdFilter, "Off", "On"); + } + + @Nullable + public String getMacroModeDescription() + { + return getIndexedDescription(TagMacroMode, "Off", "On", "Super Macro"); + } + + @Nullable + public String getFocusModeDescription() + { + int[] values = _directory.getIntArray(TagFocusMode); + if (values == null) { + // check if it's only one value long also + Integer value = _directory.getInteger(TagFocusMode); + if (value == null) + return null; + + values = new int[]{value}; + } + + if (values.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + switch (values[0]) { + case 0: + sb.append("Single AF"); + break; + case 1: + sb.append("Sequential shooting AF"); + break; + case 2: + sb.append("Continuous AF"); + break; + case 3: + sb.append("Multi AF"); + break; + case 4: + sb.append("Face detect"); + break; + case 10: + sb.append("MF"); + break; + default: + sb.append("Unknown (" + values[0] + ")"); + break; + } + + if (values.length > 1) { + sb.append("; "); + int value1 = values[1]; + + if (value1 == 0) { + sb.append("(none)"); + } else { + if (( value1 & 1) > 0) sb.append("S-AF, "); + if (((value1 >> 2) & 1) > 0) sb.append("C-AF, "); + if (((value1 >> 4) & 1) > 0) sb.append("MF, "); + if (((value1 >> 5) & 1) > 0) sb.append("Face detect, "); + if (((value1 >> 6) & 1) > 0) sb.append("Imager AF, "); + if (((value1 >> 7) & 1) > 0) sb.append("Live View Magnification Frame, "); + if (((value1 >> 8) & 1) > 0) sb.append("AF sensor, "); + + sb.setLength(sb.length() - 2); + } + } + + return sb.toString(); + } + + @Nullable + public String getFocusProcessDescription() + { + int[] values = _directory.getIntArray(TagFocusProcess); + if (values == null) { + // check if it's only one value long also + Integer value = _directory.getInteger(TagFocusProcess); + if (value == null) + return null; + + values = new int[]{value}; + } + + if (values.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + + switch (values[0]) { + case 0: + sb.append("AF not used"); + break; + case 1: + sb.append("AF used"); + break; + default: + sb.append("Unknown (" + values[0] + ")"); + break; + } + + if (values.length > 1) + sb.append("; " + values[1]); + + return sb.toString(); + } + + @Nullable + public String getAfSearchDescription() + { + return getIndexedDescription(TagAfSearch, "Not Ready", "Ready"); + } + + /// <summary> + /// coordinates range from 0 to 255 + /// </summary> + /// <returns></returns> + @Nullable + public String getAfAreasDescription() + { + Object obj = _directory.getObject(TagAfAreas); + if (obj == null || !(obj instanceof long[])) + return null; + + StringBuilder sb = new StringBuilder(); + for (long point : (long[]) obj) { + if (point == 0L) + continue; + if (sb.length() != 0) + sb.append(", "); + + if (point == 0x36794285L) + sb.append("Left "); + else if (point == 0x79798585L) + sb.append("Center "); + else if (point == 0xBD79C985L) + sb.append("Right "); + + sb.append(String.format("(%d/255,%d/255)-(%d/255,%d/255)", + (point >> 24) & 0xFF, + (point >> 16) & 0xFF, + (point >> 8) & 0xFF, + point & 0xFF)); + } + + return sb.length() == 0 ? null : sb.toString(); + } + + /// <summary> + /// coordinates expressed as a percent + /// </summary> + /// <returns></returns> + @Nullable + public String getAfPointSelectedDescription() + { + Rational[] values = _directory.getRationalArray(TagAfPointSelected); + if (values == null) + return "n/a"; + + if (values.length < 4) + return null; + + int index = 0; + if (values.length == 5 && values[0].longValue() == 0) + index = 1; + + int p1 = (int)(values[index].doubleValue() * 100); + int p2 = (int)(values[index + 1].doubleValue() * 100); + int p3 = (int)(values[index + 2].doubleValue() * 100); + int p4 = (int)(values[index + 3].doubleValue() * 100); + + if(p1 + p2 + p3 + p4 == 0) + return "n/a"; + + return String.format("(%d%%,%d%%) (%d%%,%d%%)", p1, p2, p3, p4); + } + + @Nullable + public String getAfFineTuneDescription() + { + return getIndexedDescription(TagAfFineTune, "Off", "On"); + } + + @Nullable + public String getFlashModeDescription() + { + Integer value = _directory.getInteger(TagFlashMode); + if (value == null) + return null; + + if (value == 0) + return "Off"; + + StringBuilder sb = new StringBuilder(); + int v = value; + + if (( v & 1) != 0) sb.append("On, "); + if (((v >> 1) & 1) != 0) sb.append("Fill-in, "); + if (((v >> 2) & 1) != 0) sb.append("Red-eye, "); + if (((v >> 3) & 1) != 0) sb.append("Slow-sync, "); + if (((v >> 4) & 1) != 0) sb.append("Forced On, "); + if (((v >> 5) & 1) != 0) sb.append("2nd Curtain, "); + + return sb.substring(0, sb.length() - 2); + } + + @Nullable + public String getFlashRemoteControlDescription() + { + Integer value = _directory.getInteger(TagFlashRemoteControl); + if (value == null) + return null; + + switch (value) { + case 0: + return "Off"; + case 0x01: + return "Channel 1, Low"; + case 0x02: + return "Channel 2, Low"; + case 0x03: + return "Channel 3, Low"; + case 0x04: + return "Channel 4, Low"; + case 0x09: + return "Channel 1, Mid"; + case 0x0a: + return "Channel 2, Mid"; + case 0x0b: + return "Channel 3, Mid"; + case 0x0c: + return "Channel 4, Mid"; + case 0x11: + return "Channel 1, High"; + case 0x12: + return "Channel 2, High"; + case 0x13: + return "Channel 3, High"; + case 0x14: + return "Channel 4, High"; + + default: + return "Unknown (" + value + ")"; + } + } + + /// <summary> + /// 3 or 4 values + /// </summary> + /// <returns></returns> + @Nullable + public String getFlashControlModeDescription() + { + int[] values = _directory.getIntArray(TagFlashControlMode); + if (values == null) + return null; + + if (values.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + + switch (values[0]) { + case 0: + sb.append("Off"); + break; + case 3: + sb.append("TTL"); + break; + case 4: + sb.append("Auto"); + break; + case 5: + sb.append("Manual"); + break; + default: + sb.append("Unknown (").append(values[0]).append(")"); + break; + } + + for (int i = 1; i < values.length; i++) + sb.append("; ").append(values[i]); + + return sb.toString(); + } + + /// <summary> + /// 3 or 4 values + /// </summary> + /// <returns></returns> + @Nullable + public String getFlashIntensityDescription() + { + Rational[] values = _directory.getRationalArray(TagFlashIntensity); + if (values == null || values.length == 0) + return null; + + if (values.length == 3) { + if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0) + return "n/a"; + } else if (values.length == 4) { + if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0 && values[3].getDenominator() == 0) + return "n/a (x4)"; + } + + StringBuilder sb = new StringBuilder(); + for (Rational t : values) + sb.append(t).append(", "); + + return sb.substring(0, sb.length() - 2); + } + + @Nullable + public String getManualFlashStrengthDescription() + { + Rational[] values = _directory.getRationalArray(TagManualFlashStrength); + if (values == null || values.length == 0) + return "n/a"; + + if (values.length == 3) { + if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0) + return "n/a"; + } else if (values.length == 4) { + if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0 && values[3].getDenominator() == 0) + return "n/a (x4)"; + } + + StringBuilder sb = new StringBuilder(); + for (Rational t : values) + sb.append(t).append(", "); + + return sb.substring(0, sb.length() - 2); + } + + @Nullable + public String getWhiteBalance2Description() + { + Integer value = _directory.getInteger(TagWhiteBalance2); + if (value == null) + return null; + + switch (value) { + case 0: + return "Auto"; + case 1: + return "Auto (Keep Warm Color Off)"; + case 16: + return "7500K (Fine Weather with Shade)"; + case 17: + return "6000K (Cloudy)"; + case 18: + return "5300K (Fine Weather)"; + case 20: + return "3000K (Tungsten light)"; + case 21: + return "3600K (Tungsten light-like)"; + case 22: + return "Auto Setup"; + case 23: + return "5500K (Flash)"; + case 33: + return "6600K (Daylight fluorescent)"; + case 34: + return "4500K (Neutral white fluorescent)"; + case 35: + return "4000K (Cool white fluorescent)"; + case 36: + return "White Fluorescent"; + case 48: + return "3600K (Tungsten light-like)"; + case 67: + return "Underwater"; + case 256: + return "One Touch WB 1"; + case 257: + return "One Touch WB 2"; + case 258: + return "One Touch WB 3"; + case 259: + return "One Touch WB 4"; + case 512: + return "Custom WB 1"; + case 513: + return "Custom WB 2"; + case 514: + return "Custom WB 3"; + case 515: + return "Custom WB 4"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getWhiteBalanceTemperatureDescription() + { + Integer value = _directory.getInteger(TagWhiteBalanceTemperature); + if (value == null) + return null; + if (value == 0) + return "Auto"; + return value.toString(); + } + + @Nullable + public String getCustomSaturationDescription() + { + // TODO: if model is /^E-1\b/ then + // $a-=$b; $c-=$b; + // return "CS$a (min CS0, max CS$c)" + return getValueMinMaxDescription(TagCustomSaturation); + } + + @Nullable + public String getModifiedSaturationDescription() + { + return getIndexedDescription(TagModifiedSaturation, + "Off", "CM1 (Red Enhance)", "CM2 (Green Enhance)", "CM3 (Blue Enhance)", "CM4 (Skin Tones)"); + } + + @Nullable + public String getContrastSettingDescription() + { + return getValueMinMaxDescription(TagContrastSetting); + } + + @Nullable + public String getSharpnessSettingDescription() + { + return getValueMinMaxDescription(TagSharpnessSetting); + } + + @Nullable + public String getColorSpaceDescription() + { + return getIndexedDescription(TagColorSpace, + "sRGB", "Adobe RGB", "Pro Photo RGB"); + } + + @Nullable + public String getSceneModeDescription() + { + Integer value = _directory.getInteger(TagSceneMode); + if (value == null) + return null; + + switch (value) { + case 0: + return "Standard"; + case 6: + return "Auto"; + case 7: + return "Sport"; + case 8: + return "Portrait"; + case 9: + return "Landscape+Portrait"; + case 10: + return "Landscape"; + case 11: + return "Night Scene"; + case 12: + return "Self Portrait"; + case 13: + return "Panorama"; + case 14: + return "2 in 1"; + case 15: + return "Movie"; + case 16: + return "Landscape+Portrait"; + case 17: + return "Night+Portrait"; + case 18: + return "Indoor"; + case 19: + return "Fireworks"; + case 20: + return "Sunset"; + case 21: + return "Beauty Skin"; + case 22: + return "Macro"; + case 23: + return "Super Macro"; + case 24: + return "Food"; + case 25: + return "Documents"; + case 26: + return "Museum"; + case 27: + return "Shoot & Select"; + case 28: + return "Beach & Snow"; + case 29: + return "Self Portrait+Timer"; + case 30: + return "Candle"; + case 31: + return "Available Light"; + case 32: + return "Behind Glass"; + case 33: + return "My Mode"; + case 34: + return "Pet"; + case 35: + return "Underwater Wide1"; + case 36: + return "Underwater Macro"; + case 37: + return "Shoot & Select1"; + case 38: + return "Shoot & Select2"; + case 39: + return "High Key"; + case 40: + return "Digital Image Stabilization"; + case 41: + return "Auction"; + case 42: + return "Beach"; + case 43: + return "Snow"; + case 44: + return "Underwater Wide2"; + case 45: + return "Low Key"; + case 46: + return "Children"; + case 47: + return "Vivid"; + case 48: + return "Nature Macro"; + case 49: + return "Underwater Snapshot"; + case 50: + return "Shooting Guide"; + case 54: + return "Face Portrait"; + case 57: + return "Bulb"; + case 59: + return "Smile Shot"; + case 60: + return "Quick Shutter"; + case 63: + return "Slow Shutter"; + case 64: + return "Bird Watching"; + case 65: + return "Multiple Exposure"; + case 66: + return "e-Portrait"; + case 67: + return "Soft Background Shot"; + case 142: + return "Hand-held Starlight"; + case 154: + return "HDR"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getNoiseReductionDescription() + { + Integer value = _directory.getInteger(TagNoiseReduction); + if (value == null) + return null; + + if (value == 0) + return "(none)"; + + StringBuilder sb = new StringBuilder(); + int v = value; + + if ((v & 1) != 0) sb.append("Noise Reduction, "); + if (((v >> 1) & 1) != 0) sb.append("Noise Filter, "); + if (((v >> 2) & 1) != 0) sb.append("Noise Filter (ISO Boost), "); + if (((v >> 3) & 1) != 0) sb.append("Auto, "); + + return sb.length() != 0 + ? sb.substring(0, sb.length() - 2) + : "(none)"; + } + + @Nullable + public String getDistortionCorrectionDescription() + { + return getIndexedDescription(TagDistortionCorrection, "Off", "On"); + } + + @Nullable + public String getShadingCompensationDescription() + { + return getIndexedDescription(TagShadingCompensation, "Off", "On"); + } + + /// <summary> + /// 3 or 4 values + /// </summary> + /// <returns></returns> + @Nullable + public String getGradationDescription() + { + int[] values = _directory.getIntArray(TagGradation); + if (values == null || values.length < 3) + return null; + + String join = String.format("%d %d %d", values[0], values[1], values[2]); + + String ret; + if (join.equals("0 0 0")) { + ret = "n/a"; + } else if (join.equals("-1 -1 1")) { + ret = "Low Key"; + } else if (join.equals("0 -1 1")) { + ret = "Normal"; + } else if (join.equals("1 -1 1")) { + ret = "High Key"; + } else { + ret = "Unknown (" + join + ")"; + } + + if (values.length > 3) { + if (values[3] == 0) + ret += "; User-Selected"; + else if (values[3] == 1) + ret += "; Auto-Override"; + } + + return ret; + } + + /// <summary> + /// 1 or 2 values + /// </summary> + /// <returns></returns> + @Nullable + public String getPictureModeDescription() + { + int[] values = _directory.getIntArray(TagPictureMode); + if (values == null) { + // check if it's only one value long also + Integer value = _directory.getInteger(TagNoiseReduction); + if (value == null) + return null; + + values = new int[]{value}; + } + + if (values.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + switch (values[0]) { + case 1: + sb.append("Vivid"); + break; + case 2: + sb.append("Natural"); + break; + case 3: + sb.append("Muted"); + break; + case 4: + sb.append("Portrait"); + break; + case 5: + sb.append("i-Enhance"); + break; + case 256: + sb.append("Monotone"); + break; + case 512: + sb.append("Sepia"); + break; + default: + sb.append("Unknown (").append(values[0]).append(")"); + break; + } + + if (values.length > 1) + sb.append("; ").append(values[1]); + + return sb.toString(); + } + + @Nullable + public String getPictureModeSaturationDescription() + { + return getValueMinMaxDescription(TagPictureModeSaturation); + } + + @Nullable + public String getPictureModeContrastDescription() + { + return getValueMinMaxDescription(TagPictureModeContrast); + } + + @Nullable + public String getPictureModeSharpnessDescription() + { + return getValueMinMaxDescription(TagPictureModeSharpness); + } + + @Nullable + public String getPictureModeBWFilterDescription() + { + return getIndexedDescription(TagPictureModeBWFilter, + "n/a", "Neutral", "Yellow", "Orange", "Red", "Green"); + } + + @Nullable + public String getPictureModeToneDescription() + { + return getIndexedDescription(TagPictureModeTone, + "n/a", "Neutral", "Sepia", "Blue", "Purple", "Green"); + } + + @Nullable + public String getNoiseFilterDescription() + { + int[] values = _directory.getIntArray(TagNoiseFilter); + if (values == null) + return null; + + String join = String.format("%d %d %d", values[0], values[1], values[2]); + + if (join.equals("0 0 0")) + return "n/a"; + if (join.equals("-2 -2 1")) + return "Off"; + if (join.equals("-1 -2 1")) + return "Low"; + if (join.equals("0 -2 1")) + return "Standard"; + if (join.equals("1 -2 1")) + return "High"; + return "Unknown (" + join + ")"; + } + + @Nullable + public String getArtFilterDescription() + { + return getFiltersDescription(TagArtFilter); + } + + @Nullable + public String getMagicFilterDescription() + { + return getFiltersDescription(TagMagicFilter); + } + + @Nullable + public String getPictureModeEffectDescription() + { + int[] values = _directory.getIntArray(TagPictureModeEffect); + if (values == null) + return null; + + String key = String.format("%d %d %d", values[0], values[1], values[2]); + if (key.equals("0 0 0")) + return "n/a"; + if (key.equals("-1 -1 1")) + return "Low"; + if (key.equals("0 -1 1")) + return "Standard"; + if (key.equals("1 -1 1")) + return "High"; + return "Unknown (" + key + ")"; + } + + @Nullable + public String getToneLevelDescription() + { + int[] values = _directory.getIntArray(TagToneLevel); + if (values == null || values.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + if (i == 0 || i == 4 || i == 8 || i == 12 || i == 16 || i == 20 || i == 24) + sb.append(_toneLevelType.get(values[i])).append("; "); + else + sb.append(values[i]).append("; "); + } + + return sb.substring(0, sb.length() - 2); + } + + @Nullable + public String getArtFilterEffectDescription() + { + int[] values = _directory.getIntArray(TagArtFilterEffect); + if (values == null) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + if (i == 0) { + sb.append((_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]")).append("; "); + } else if (i == 3) { + sb.append("Partial Color ").append(values[i]).append("; "); + } else if (i == 4) { + switch (values[i]) { + case 0x0000: + sb.append("No Effect"); + break; + case 0x8010: + sb.append("Star Light"); + break; + case 0x8020: + sb.append("Pin Hole"); + break; + case 0x8030: + sb.append("Frame"); + break; + case 0x8040: + sb.append("Soft Focus"); + break; + case 0x8050: + sb.append("White Edge"); + break; + case 0x8060: + sb.append("B&W"); + break; + default: + sb.append("Unknown (").append(values[i]).append(")"); + break; + } + sb.append("; "); + } else if (i == 6) { + switch (values[i]) { + case 0: + sb.append("No Color Filter"); + break; + case 1: + sb.append("Yellow Color Filter"); + break; + case 2: + sb.append("Orange Color Filter"); + break; + case 3: + sb.append("Red Color Filter"); + break; + case 4: + sb.append("Green Color Filter"); + break; + default: + sb.append("Unknown (").append(values[i]).append(")"); + break; + } + sb.append("; "); + } else { + sb.append(values[i]).append("; "); + } + } + + return sb.substring(0, sb.length() - 2); + } + + @Nullable + public String getColorCreatorEffectDescription() + { + int[] values = _directory.getIntArray(TagColorCreatorEffect); + if (values == null) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + if (i == 0) { + sb.append("Color ").append(values[i]).append("; "); + } else if (i == 3) { + sb.append("Strength ").append(values[i]).append("; "); + } else { + sb.append(values[i]).append("; "); + } + } + + return sb.substring(0, sb.length() - 2); + } + + /// <summary> + /// 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits + /// </summary> + /// <returns></returns> + @Nullable + public String getDriveModeDescription() + { + int[] values = _directory.getIntArray(TagDriveMode); + if (values == null) + return null; + + if (values.length == 0 || values[0] == 0) + return "Single Shot"; + + StringBuilder a = new StringBuilder(); + + if (values[0] == 5 && values.length >= 3) { + int c = values[2]; + if (( c & 1) > 0) a.append("AE"); + if (((c >> 1) & 1) > 0) a.append("WB"); + if (((c >> 2) & 1) > 0) a.append("FL"); + if (((c >> 3) & 1) > 0) a.append("MF"); + if (((c >> 6) & 1) > 0) a.append("Focus"); + + a.append(" Bracketing"); + } else { + switch (values[0]) { + case 1: + a.append("Continuous Shooting"); + break; + case 2: + a.append("Exposure Bracketing"); + break; + case 3: + a.append("White Balance Bracketing"); + break; + case 4: + a.append("Exposure+WB Bracketing"); + break; + default: + a.append("Unknown (").append(values[0]).append(")"); + break; + } + } + + a.append(", Shot ").append(values[1]); + + return a.toString(); + } + + /// <summary> + /// 2 numbers: 1. Mode, 2. Shot number + /// </summary> + /// <returns></returns> + @Nullable + public String getPanoramaModeDescription() + { + int[] values = _directory.getIntArray(TagPanoramaMode); + if (values == null) + return null; + + if (values.length == 0 || values[0] == 0) + return "Off"; + + String a; + switch (values[0]) { + case 1: + a = "Left to Right"; + break; + case 2: + a = "Right to Left"; + break; + case 3: + a = "Bottom to Top"; + break; + case 4: + a = "Top to Bottom"; + break; + default: + a = "Unknown (" + values[0] + ")"; + break; + } + + return String.format("%s, Shot %d", a, values[1]); + } + + @Nullable + public String getImageQuality2Description() + { + return getIndexedDescription(TagImageQuality2, 1, + "SQ", "HQ", "SHQ", "RAW", "SQ (5)"); + } + + @Nullable + public String getImageStabilizationDescription() + { + return getIndexedDescription(TagImageStabilization, + "Off", "On, Mode 1", "On, Mode 2", "On, Mode 3", "On, Mode 4"); + } + + @Nullable + public String getStackedImageDescription() + { + int[] values = _directory.getIntArray(TagStackedImage); + if (values == null || values.length < 2) + return null; + + int v1 = values[0]; + int v2 = values[1]; + + if (v1 == 0 && v2 == 0) + return "No"; + if (v1 == 9 && v2 == 8) + return "Focus-stacked (8 images)"; + + return String.format("Unknown (%d %d)", v1, v2); + } + + /// <remarks> + /// TODO: need better image examples to test this function + /// </remarks> + /// <returns></returns> + @Nullable + public String getManometerPressureDescription() + { + Integer value = _directory.getInteger(TagManometerPressure); + if (value == null) + return null; + + return String.format("%s kPa", new DecimalFormat("#.##").format(value / 10.0)); + } + + /// <remarks> + /// TODO: need better image examples to test this function + /// </remarks> + /// <returns></returns> + @Nullable + public String getManometerReadingDescription() + { + int[] values = _directory.getIntArray(TagManometerReading); + if (values == null || values.length < 2) + return null; + + DecimalFormat format = new DecimalFormat("#.##"); + return String.format("%s m, %s ft", + format.format(values[0] / 10.0), + format.format(values[1] / 10.0)); + } + + @Nullable + public String getExtendedWBDetectDescription() + { + return getIndexedDescription(TagExtendedWBDetect, "Off", "On"); + } + + /// <summary> + /// converted to degrees of clockwise camera rotation + /// </summary> + /// <remarks> + /// TODO: need better image examples to test this function + /// </remarks> + /// <returns></returns> + @Nullable + public String getRollAngleDescription() + { + int[] values = _directory.getIntArray(TagRollAngle); + if (values == null || values.length < 2) + return null; + + String ret = values[0] != 0 + ? Double.toString(-values[0] / 10.0) + : "n/a"; + + return String.format("%s %d", ret, values[1]); + } + + /// <summary> + /// converted to degrees of upward camera tilt + /// </summary> + /// <remarks> + /// TODO: need better image examples to test this function + /// </remarks> + /// <returns></returns> + @Nullable + public String getPitchAngleDescription() + { + int[] values = _directory.getIntArray(TagPitchAngle); + if (values == null || values.length < 2) + return null; + + // (second value is 0 if level gauge is off) + String ret = values[0] != 0 + ? Double.toString(values[0] / 10.0) + : "n/a"; + + return String.format("%s %d", ret, values[1]); + } + + @Nullable + public String getDateTimeUTCDescription() + { + Object value = _directory.getObject(TagDateTimeUtc); + if (value == null) + return null; + return value.toString(); + } + + @Nullable + private String getValueMinMaxDescription(int tagId) + { + int[] values = _directory.getIntArray(tagId); + if (values == null || values.length < 3) + return null; + + return String.format("%d (min %d, max %d)", values[0], values[1], values[2]); + } + + @Nullable + private String getFiltersDescription(int tagId) + { + int[] values = _directory.getIntArray(tagId); + if (values == null || values.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + if (i == 0) + sb.append(_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]"); + else + sb.append(values[i]); + sb.append("; "); + } + + return sb.substring(0, sb.length() - 2); + } + + private static final HashMap<Integer, String> _toneLevelType = new HashMap<Integer, String>(); + // ArtFilter, ArtFilterEffect and MagicFilter values + private static final HashMap<Integer, String> _filters = new HashMap<Integer, String>(); + + static { + _filters.put(0, "Off"); + _filters.put(1, "Soft Focus"); + _filters.put(2, "Pop Art"); + _filters.put(3, "Pale & Light Color"); + _filters.put(4, "Light Tone"); + _filters.put(5, "Pin Hole"); + _filters.put(6, "Grainy Film"); + _filters.put(9, "Diorama"); + _filters.put(10, "Cross Process"); + _filters.put(12, "Fish Eye"); + _filters.put(13, "Drawing"); + _filters.put(14, "Gentle Sepia"); + _filters.put(15, "Pale & Light Color II"); + _filters.put(16, "Pop Art II"); + _filters.put(17, "Pin Hole II"); + _filters.put(18, "Pin Hole III"); + _filters.put(19, "Grainy Film II"); + _filters.put(20, "Dramatic Tone"); + _filters.put(21, "Punk"); + _filters.put(22, "Soft Focus 2"); + _filters.put(23, "Sparkle"); + _filters.put(24, "Watercolor"); + _filters.put(25, "Key Line"); + _filters.put(26, "Key Line II"); + _filters.put(27, "Miniature"); + _filters.put(28, "Reflection"); + _filters.put(29, "Fragmented"); + _filters.put(31, "Cross Process II"); + _filters.put(32, "Dramatic Tone II"); + _filters.put(33, "Watercolor I"); + _filters.put(34, "Watercolor II"); + _filters.put(35, "Diorama II"); + _filters.put(36, "Vintage"); + _filters.put(37, "Vintage II"); + _filters.put(38, "Vintage III"); + _filters.put(39, "Partial Color"); + _filters.put(40, "Partial Color II"); + _filters.put(41, "Partial Color III"); + + _toneLevelType.put(0, "0"); + _toneLevelType.put(-31999, "Highlights "); + _toneLevelType.put(-31998, "Shadows "); + _toneLevelType.put(-31997, "Midtones "); + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java new file mode 100644 index 0000000..90247c5 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusCameraSettingsMakernoteDirectory.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * The Olympus camera settings makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags + * that appear specific to those manufacturers. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusCameraSettingsMakernoteDirectory extends Directory +{ + public static final int TagCameraSettingsVersion = 0x0000; + public static final int TagPreviewImageValid = 0x0100; + public static final int TagPreviewImageStart = 0x0101; + public static final int TagPreviewImageLength = 0x0102; + + public static final int TagExposureMode = 0x0200; + public static final int TagAeLock = 0x0201; + public static final int TagMeteringMode = 0x0202; + public static final int TagExposureShift = 0x0203; + public static final int TagNdFilter = 0x0204; + + public static final int TagMacroMode = 0x0300; + public static final int TagFocusMode = 0x0301; + public static final int TagFocusProcess = 0x0302; + public static final int TagAfSearch = 0x0303; + public static final int TagAfAreas = 0x0304; + public static final int TagAfPointSelected = 0x0305; + public static final int TagAfFineTune = 0x0306; + public static final int TagAfFineTuneAdj = 0x0307; + + public static final int TagFlashMode = 0x400; + public static final int TagFlashExposureComp = 0x401; + public static final int TagFlashRemoteControl = 0x403; + public static final int TagFlashControlMode = 0x404; + public static final int TagFlashIntensity = 0x405; + public static final int TagManualFlashStrength = 0x406; + + public static final int TagWhiteBalance2 = 0x500; + public static final int TagWhiteBalanceTemperature = 0x501; + public static final int TagWhiteBalanceBracket = 0x502; + public static final int TagCustomSaturation = 0x503; + public static final int TagModifiedSaturation = 0x504; + public static final int TagContrastSetting = 0x505; + public static final int TagSharpnessSetting = 0x506; + public static final int TagColorSpace = 0x507; + public static final int TagSceneMode = 0x509; + public static final int TagNoiseReduction = 0x50a; + public static final int TagDistortionCorrection = 0x50b; + public static final int TagShadingCompensation = 0x50c; + public static final int TagCompressionFactor = 0x50d; + public static final int TagGradation = 0x50f; + public static final int TagPictureMode = 0x520; + public static final int TagPictureModeSaturation = 0x521; + public static final int TagPictureModeHue = 0x522; + public static final int TagPictureModeContrast = 0x523; + public static final int TagPictureModeSharpness = 0x524; + public static final int TagPictureModeBWFilter = 0x525; + public static final int TagPictureModeTone = 0x526; + public static final int TagNoiseFilter = 0x527; + public static final int TagArtFilter = 0x529; + public static final int TagMagicFilter = 0x52c; + public static final int TagPictureModeEffect = 0x52d; + public static final int TagToneLevel = 0x52e; + public static final int TagArtFilterEffect = 0x52f; + public static final int TagColorCreatorEffect = 0x532; + + public static final int TagDriveMode = 0x600; + public static final int TagPanoramaMode = 0x601; + public static final int TagImageQuality2 = 0x603; + public static final int TagImageStabilization = 0x604; + + public static final int TagStackedImage = 0x804; + + public static final int TagManometerPressure = 0x900; + public static final int TagManometerReading = 0x901; + public static final int TagExtendedWBDetect = 0x902; + public static final int TagRollAngle = 0x903; + public static final int TagPitchAngle = 0x904; + public static final int TagDateTimeUtc = 0x908; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TagCameraSettingsVersion, "Camera Settings Version"); + _tagNameMap.put(TagPreviewImageValid, "Preview Image Valid"); + _tagNameMap.put(TagPreviewImageStart, "Preview Image Start"); + _tagNameMap.put(TagPreviewImageLength, "Preview Image Length"); + + _tagNameMap.put(TagExposureMode, "Exposure Mode"); + _tagNameMap.put(TagAeLock, "AE Lock"); + _tagNameMap.put(TagMeteringMode, "Metering Mode"); + _tagNameMap.put(TagExposureShift, "Exposure Shift"); + _tagNameMap.put(TagNdFilter, "ND Filter"); + + _tagNameMap.put(TagMacroMode, "Macro Mode"); + _tagNameMap.put(TagFocusMode, "Focus Mode"); + _tagNameMap.put(TagFocusProcess, "Focus Process"); + _tagNameMap.put(TagAfSearch, "AF Search"); + _tagNameMap.put(TagAfAreas, "AF Areas"); + _tagNameMap.put(TagAfPointSelected, "AF Point Selected"); + _tagNameMap.put(TagAfFineTune, "AF Fine Tune"); + _tagNameMap.put(TagAfFineTuneAdj, "AF Fine Tune Adj"); + + _tagNameMap.put(TagFlashMode, "Flash Mode"); + _tagNameMap.put(TagFlashExposureComp, "Flash Exposure Comp"); + _tagNameMap.put(TagFlashRemoteControl, "Flash Remote Control"); + _tagNameMap.put(TagFlashControlMode, "Flash Control Mode"); + _tagNameMap.put(TagFlashIntensity, "Flash Intensity"); + _tagNameMap.put(TagManualFlashStrength, "Manual Flash Strength"); + + _tagNameMap.put(TagWhiteBalance2, "White Balance 2"); + _tagNameMap.put(TagWhiteBalanceTemperature, "White Balance Temperature"); + _tagNameMap.put(TagWhiteBalanceBracket, "White Balance Bracket"); + _tagNameMap.put(TagCustomSaturation, "Custom Saturation"); + _tagNameMap.put(TagModifiedSaturation, "Modified Saturation"); + _tagNameMap.put(TagContrastSetting, "Contrast Setting"); + _tagNameMap.put(TagSharpnessSetting, "Sharpness Setting"); + _tagNameMap.put(TagColorSpace, "Color Space"); + _tagNameMap.put(TagSceneMode, "Scene Mode"); + _tagNameMap.put(TagNoiseReduction, "Noise Reduction"); + _tagNameMap.put(TagDistortionCorrection, "Distortion Correction"); + _tagNameMap.put(TagShadingCompensation, "Shading Compensation"); + _tagNameMap.put(TagCompressionFactor, "Compression Factor"); + _tagNameMap.put(TagGradation, "Gradation"); + _tagNameMap.put(TagPictureMode, "Picture Mode"); + _tagNameMap.put(TagPictureModeSaturation, "Picture Mode Saturation"); + _tagNameMap.put(TagPictureModeHue, "Picture Mode Hue"); + _tagNameMap.put(TagPictureModeContrast, "Picture Mode Contrast"); + _tagNameMap.put(TagPictureModeSharpness, "Picture Mode Sharpness"); + _tagNameMap.put(TagPictureModeBWFilter, "Picture Mode BW Filter"); + _tagNameMap.put(TagPictureModeTone, "Picture Mode Tone"); + _tagNameMap.put(TagNoiseFilter, "Noise Filter"); + _tagNameMap.put(TagArtFilter, "Art Filter"); + _tagNameMap.put(TagMagicFilter, "Magic Filter"); + _tagNameMap.put(TagPictureModeEffect, "Picture Mode Effect"); + _tagNameMap.put(TagToneLevel, "Tone Level"); + _tagNameMap.put(TagArtFilterEffect, "Art Filter Effect"); + _tagNameMap.put(TagColorCreatorEffect, "Color Creator Effect"); + + _tagNameMap.put(TagDriveMode, "Drive Mode"); + _tagNameMap.put(TagPanoramaMode, "Panorama Mode"); + _tagNameMap.put(TagImageQuality2, "Image Quality 2"); + _tagNameMap.put(TagImageStabilization, "Image Stabilization"); + + _tagNameMap.put(TagStackedImage, "Stacked Image"); + + _tagNameMap.put(TagManometerPressure, "Manometer Pressure"); + _tagNameMap.put(TagManometerReading, "Manometer Reading"); + _tagNameMap.put(TagExtendedWBDetect, "Extended WB Detect"); + _tagNameMap.put(TagRollAngle, "Roll Angle"); + _tagNameMap.put(TagPitchAngle, "Pitch Angle"); + _tagNameMap.put(TagDateTimeUtc, "Date Time UTC"); + } + + public OlympusCameraSettingsMakernoteDirectory() + { + this.setDescriptor(new OlympusCameraSettingsMakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Olympus Camera Settings"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java new file mode 100644 index 0000000..e37302d --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDescriptor.java @@ -0,0 +1,386 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import java.text.DecimalFormat; +import java.util.HashMap; + +import static com.drew.metadata.exif.makernotes.OlympusEquipmentMakernoteDirectory.*; + +/** + * Provides human-readable String representations of tag values stored in a {@link OlympusEquipmentMakernoteDirectory}. + * <p> + * Some Description functions and the Extender and Lens types lists converted from Exiftool version 10.10 created by Phil Harvey + * http://www.sno.phy.queensu.ca/~phil/exiftool/ + * lib\Image\ExifTool\Olympus.pm + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusEquipmentMakernoteDescriptor extends TagDescriptor<OlympusEquipmentMakernoteDirectory> +{ + public OlympusEquipmentMakernoteDescriptor(@NotNull OlympusEquipmentMakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TAG_EQUIPMENT_VERSION: + return getEquipmentVersionDescription(); + case TAG_CAMERA_TYPE_2: + return getCameraType2Description(); + case TAG_FOCAL_PLANE_DIAGONAL: + return getFocalPlaneDiagonalDescription(); + case TAG_BODY_FIRMWARE_VERSION: + return getBodyFirmwareVersionDescription(); + case TAG_LENS_TYPE: + return getLensTypeDescription(); + case TAG_LENS_FIRMWARE_VERSION: + return getLensFirmwareVersionDescription(); + case TAG_MAX_APERTURE_AT_MIN_FOCAL: + return getMaxApertureAtMinFocalDescription(); + case TAG_MAX_APERTURE_AT_MAX_FOCAL: + return getMaxApertureAtMaxFocalDescription(); + case TAG_MAX_APERTURE: + return getMaxApertureDescription(); + case TAG_LENS_PROPERTIES: + return getLensPropertiesDescription(); + case TAG_EXTENDER: + return getExtenderDescription(); + case TAG_FLASH_TYPE: + return getFlashTypeDescription(); + case TAG_FLASH_MODEL: + return getFlashModelDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getEquipmentVersionDescription() + { + return getVersionBytesDescription(TAG_EQUIPMENT_VERSION, 4); + } + + @Nullable + public String getCameraType2Description() + { + String cameratype = _directory.getString(TAG_CAMERA_TYPE_2); + if(cameratype == null) + return null; + + if(OlympusMakernoteDirectory.OlympusCameraTypes.containsKey(cameratype)) + return OlympusMakernoteDirectory.OlympusCameraTypes.get(cameratype); + + return cameratype; + } + + @Nullable + public String getFocalPlaneDiagonalDescription() + { + return _directory.getString(TAG_FOCAL_PLANE_DIAGONAL) + " mm"; + } + + @Nullable + public String getBodyFirmwareVersionDescription() + { + Integer value = _directory.getInteger(TAG_BODY_FIRMWARE_VERSION); + if (value == null) + return null; + + String hex = String.format("%04X", value); + return String.format("%s.%s", + hex.substring(0, hex.length() - 3), + hex.substring(hex.length() - 3)); + } + + @Nullable + public String getLensTypeDescription() + { + String str = _directory.getString(TAG_LENS_TYPE); + + if (str == null) + return null; + + // The String contains six numbers: + // + // - Make + // - Unknown + // - Model + // - Sub-model + // - Unknown + // - Unknown + // + // Only the Make, Model and Sub-model are used to identify the lens type + String[] values = str.split(" "); + + if (values.length < 6) + return null; + + try { + int num1 = Integer.parseInt(values[0]); + int num2 = Integer.parseInt(values[2]); + int num3 = Integer.parseInt(values[3]); + return _olympusLensTypes.get(String.format("%X %02X %02X", num1, num2, num3)); + } catch (NumberFormatException e) { + return null; + } + } + + @Nullable + public String getLensFirmwareVersionDescription() + { + Integer value = _directory.getInteger(TAG_LENS_FIRMWARE_VERSION); + if (value == null) + return null; + + String hex = String.format("%04X", value); + return String.format("%s.%s", + hex.substring(0, hex.length() - 3), + hex.substring(hex.length() - 3)); + } + + @Nullable + public String getMaxApertureAtMinFocalDescription() + { + Integer value = _directory.getInteger(TAG_MAX_APERTURE_AT_MIN_FOCAL); + if (value == null) + return null; + + DecimalFormat format = new DecimalFormat("0.#"); + return format.format(CalcMaxAperture(value)); + } + + @Nullable + public String getMaxApertureAtMaxFocalDescription() + { + Integer value = _directory.getInteger(TAG_MAX_APERTURE_AT_MAX_FOCAL); + if (value == null) + return null; + + DecimalFormat format = new DecimalFormat("0.#"); + return format.format(CalcMaxAperture(value)); + } + + @Nullable + public String getMaxApertureDescription() + { + Integer value = _directory.getInteger(TAG_MAX_APERTURE); + if (value == null) + return null; + + DecimalFormat format = new DecimalFormat("0.#"); + return format.format(CalcMaxAperture(value)); + } + + private static double CalcMaxAperture(int value) + { + return Math.pow(Math.sqrt(2.00), value / 256.0); + } + + @Nullable + public String getLensPropertiesDescription() + { + Integer value = _directory.getInteger(TAG_LENS_PROPERTIES); + if (value == null) + return null; + + return String.format("0x%04X", value); + } + + @Nullable + public String getExtenderDescription() + { + String str = _directory.getString(TAG_EXTENDER); + + if (str == null) + return null; + + // The String contains six numbers: + // + // - Make + // - Unknown + // - Model + // - Sub-model + // - Unknown + // - Unknown + // + // Only the Make and Model are used to identify the extender + String[] values = str.split(" "); + + if (values.length < 6) + return null; + + try { + int num1 = Integer.parseInt(values[0]); + int num2 = Integer.parseInt(values[2]); + String extenderType = String.format("%X %02X", num1, num2); + return _olympusExtenderTypes.get(extenderType); + } catch (NumberFormatException e) { + return null; + } + } + + @Nullable + public String getFlashTypeDescription() + { + return getIndexedDescription(TAG_FLASH_TYPE, + "None", null, "Simple E-System", "E-System"); + } + + @Nullable + public String getFlashModelDescription() + { + return getIndexedDescription(TAG_FLASH_MODEL, + "None", "FL-20", "FL-50", "RF-11", "TF-22", "FL-36", "FL-50R", "FL-36R"); + } + + private static final HashMap<String, String> _olympusLensTypes = new HashMap<String, String>(); + private static final HashMap<String, String> _olympusExtenderTypes = new HashMap<String, String>(); + + static { + _olympusLensTypes.put("0 00 00", "None"); + // Olympus lenses (also Kenko Tokina) + _olympusLensTypes.put("0 01 00", "Olympus Zuiko Digital ED 50mm F2.0 Macro"); + _olympusLensTypes.put("0 01 01", "Olympus Zuiko Digital 40-150mm F3.5-4.5"); //8 + _olympusLensTypes.put("0 01 10", "Olympus M.Zuiko Digital ED 14-42mm F3.5-5.6"); //PH (E-P1 pre-production) + _olympusLensTypes.put("0 02 00", "Olympus Zuiko Digital ED 150mm F2.0"); + _olympusLensTypes.put("0 02 10", "Olympus M.Zuiko Digital 17mm F2.8 Pancake"); //PH (E-P1 pre-production) + _olympusLensTypes.put("0 03 00", "Olympus Zuiko Digital ED 300mm F2.8"); + _olympusLensTypes.put("0 03 10", "Olympus M.Zuiko Digital ED 14-150mm F4.0-5.6 [II]"); //11 (The second version of this lens seems to have the same lens ID number as the first version #20) + _olympusLensTypes.put("0 04 10", "Olympus M.Zuiko Digital ED 9-18mm F4.0-5.6"); //11 + _olympusLensTypes.put("0 05 00", "Olympus Zuiko Digital 14-54mm F2.8-3.5"); + _olympusLensTypes.put("0 05 01", "Olympus Zuiko Digital Pro ED 90-250mm F2.8"); //9 + _olympusLensTypes.put("0 05 10", "Olympus M.Zuiko Digital ED 14-42mm F3.5-5.6 L"); //11 (E-PL1) + _olympusLensTypes.put("0 06 00", "Olympus Zuiko Digital ED 50-200mm F2.8-3.5"); + _olympusLensTypes.put("0 06 01", "Olympus Zuiko Digital ED 8mm F3.5 Fisheye"); //9 + _olympusLensTypes.put("0 06 10", "Olympus M.Zuiko Digital ED 40-150mm F4.0-5.6"); //PH + _olympusLensTypes.put("0 07 00", "Olympus Zuiko Digital 11-22mm F2.8-3.5"); + _olympusLensTypes.put("0 07 01", "Olympus Zuiko Digital 18-180mm F3.5-6.3"); //6 + _olympusLensTypes.put("0 07 10", "Olympus M.Zuiko Digital ED 12mm F2.0"); //PH + _olympusLensTypes.put("0 08 01", "Olympus Zuiko Digital 70-300mm F4.0-5.6"); //7 (seen as release 1 - PH) + _olympusLensTypes.put("0 08 10", "Olympus M.Zuiko Digital ED 75-300mm F4.8-6.7"); //PH + _olympusLensTypes.put("0 09 10", "Olympus M.Zuiko Digital 14-42mm F3.5-5.6 II"); //PH (E-PL2) + _olympusLensTypes.put("0 10 01", "Kenko Tokina Reflex 300mm F6.3 MF Macro"); //20 + _olympusLensTypes.put("0 10 10", "Olympus M.Zuiko Digital ED 12-50mm F3.5-6.3 EZ"); //PH + _olympusLensTypes.put("0 11 10", "Olympus M.Zuiko Digital 45mm F1.8"); //17 + _olympusLensTypes.put("0 12 10", "Olympus M.Zuiko Digital ED 60mm F2.8 Macro"); //20 + _olympusLensTypes.put("0 13 10", "Olympus M.Zuiko Digital 14-42mm F3.5-5.6 II R"); //PH/20 + _olympusLensTypes.put("0 14 10", "Olympus M.Zuiko Digital ED 40-150mm F4.0-5.6 R"); //19 + // '0 14 10.1", "Olympus M.Zuiko Digital ED 14-150mm F4.0-5.6 II"); //11 (questionable & unconfirmed -- all samples I can find are '0 3 10' - PH) + _olympusLensTypes.put("0 15 00", "Olympus Zuiko Digital ED 7-14mm F4.0"); + _olympusLensTypes.put("0 15 10", "Olympus M.Zuiko Digital ED 75mm F1.8"); //PH + _olympusLensTypes.put("0 16 10", "Olympus M.Zuiko Digital 17mm F1.8"); //20 + _olympusLensTypes.put("0 17 00", "Olympus Zuiko Digital Pro ED 35-100mm F2.0"); //7 + _olympusLensTypes.put("0 18 00", "Olympus Zuiko Digital 14-45mm F3.5-5.6"); + _olympusLensTypes.put("0 18 10", "Olympus M.Zuiko Digital ED 75-300mm F4.8-6.7 II"); //20 + _olympusLensTypes.put("0 19 10", "Olympus M.Zuiko Digital ED 12-40mm F2.8 Pro"); //PH + _olympusLensTypes.put("0 20 00", "Olympus Zuiko Digital 35mm F3.5 Macro"); //9 + _olympusLensTypes.put("0 20 10", "Olympus M.Zuiko Digital ED 40-150mm F2.8 Pro"); //20 + _olympusLensTypes.put("0 21 10", "Olympus M.Zuiko Digital ED 14-42mm F3.5-5.6 EZ"); //20 + _olympusLensTypes.put("0 22 00", "Olympus Zuiko Digital 17.5-45mm F3.5-5.6"); //9 + _olympusLensTypes.put("0 22 10", "Olympus M.Zuiko Digital 25mm F1.8"); //20 + _olympusLensTypes.put("0 23 00", "Olympus Zuiko Digital ED 14-42mm F3.5-5.6"); //PH + _olympusLensTypes.put("0 23 10", "Olympus M.Zuiko Digital ED 7-14mm F2.8 Pro"); //20 + _olympusLensTypes.put("0 24 00", "Olympus Zuiko Digital ED 40-150mm F4.0-5.6"); //PH + _olympusLensTypes.put("0 24 10", "Olympus M.Zuiko Digital ED 300mm F4.0 IS Pro"); //20 + _olympusLensTypes.put("0 25 10", "Olympus M.Zuiko Digital ED 8mm F1.8 Fisheye Pro"); //20 + _olympusLensTypes.put("0 30 00", "Olympus Zuiko Digital ED 50-200mm F2.8-3.5 SWD"); //7 + _olympusLensTypes.put("0 31 00", "Olympus Zuiko Digital ED 12-60mm F2.8-4.0 SWD"); //7 + _olympusLensTypes.put("0 32 00", "Olympus Zuiko Digital ED 14-35mm F2.0 SWD"); //PH + _olympusLensTypes.put("0 33 00", "Olympus Zuiko Digital 25mm F2.8"); //PH + _olympusLensTypes.put("0 34 00", "Olympus Zuiko Digital ED 9-18mm F4.0-5.6"); //7 + _olympusLensTypes.put("0 35 00", "Olympus Zuiko Digital 14-54mm F2.8-3.5 II"); //PH + // Sigma lenses + _olympusLensTypes.put("1 01 00", "Sigma 18-50mm F3.5-5.6 DC"); //8 + _olympusLensTypes.put("1 01 10", "Sigma 30mm F2.8 EX DN"); //20 + _olympusLensTypes.put("1 02 00", "Sigma 55-200mm F4.0-5.6 DC"); + _olympusLensTypes.put("1 02 10", "Sigma 19mm F2.8 EX DN"); //20 + _olympusLensTypes.put("1 03 00", "Sigma 18-125mm F3.5-5.6 DC"); + _olympusLensTypes.put("1 03 10", "Sigma 30mm F2.8 DN | A"); //20 + _olympusLensTypes.put("1 04 00", "Sigma 18-125mm F3.5-5.6 DC"); //7 + _olympusLensTypes.put("1 04 10", "Sigma 19mm F2.8 DN | A"); //20 + _olympusLensTypes.put("1 05 00", "Sigma 30mm F1.4 EX DC HSM"); //10 + _olympusLensTypes.put("1 05 10", "Sigma 60mm F2.8 DN | A"); //20 + _olympusLensTypes.put("1 06 00", "Sigma APO 50-500mm F4.0-6.3 EX DG HSM"); //6 + _olympusLensTypes.put("1 07 00", "Sigma Macro 105mm F2.8 EX DG"); //PH + _olympusLensTypes.put("1 08 00", "Sigma APO Macro 150mm F2.8 EX DG HSM"); //PH + _olympusLensTypes.put("1 09 00", "Sigma 18-50mm F2.8 EX DC Macro"); //20 + _olympusLensTypes.put("1 10 00", "Sigma 24mm F1.8 EX DG Aspherical Macro"); //PH + _olympusLensTypes.put("1 11 00", "Sigma APO 135-400mm F4.5-5.6 DG"); //11 + _olympusLensTypes.put("1 12 00", "Sigma APO 300-800mm F5.6 EX DG HSM"); //11 + _olympusLensTypes.put("1 13 00", "Sigma 30mm F1.4 EX DC HSM"); //11 + _olympusLensTypes.put("1 14 00", "Sigma APO 50-500mm F4.0-6.3 EX DG HSM"); //11 + _olympusLensTypes.put("1 15 00", "Sigma 10-20mm F4.0-5.6 EX DC HSM"); //11 + _olympusLensTypes.put("1 16 00", "Sigma APO 70-200mm F2.8 II EX DG Macro HSM"); //11 + _olympusLensTypes.put("1 17 00", "Sigma 50mm F1.4 EX DG HSM"); //11 + // Panasonic/Leica lenses + _olympusLensTypes.put("2 01 00", "Leica D Vario Elmarit 14-50mm F2.8-3.5 Asph."); //11 + _olympusLensTypes.put("2 01 10", "Lumix G Vario 14-45mm F3.5-5.6 Asph. Mega OIS"); //16 + _olympusLensTypes.put("2 02 00", "Leica D Summilux 25mm F1.4 Asph."); //11 + _olympusLensTypes.put("2 02 10", "Lumix G Vario 45-200mm F4.0-5.6 Mega OIS"); //16 + _olympusLensTypes.put("2 03 00", "Leica D Vario Elmar 14-50mm F3.8-5.6 Asph. Mega OIS"); //11 + _olympusLensTypes.put("2 03 01", "Leica D Vario Elmar 14-50mm F3.8-5.6 Asph."); //14 (L10 kit) + _olympusLensTypes.put("2 03 10", "Lumix G Vario HD 14-140mm F4.0-5.8 Asph. Mega OIS"); //16 + _olympusLensTypes.put("2 04 00", "Leica D Vario Elmar 14-150mm F3.5-5.6"); //13 + _olympusLensTypes.put("2 04 10", "Lumix G Vario 7-14mm F4.0 Asph."); //PH (E-P1 pre-production) + _olympusLensTypes.put("2 05 10", "Lumix G 20mm F1.7 Asph."); //16 + _olympusLensTypes.put("2 06 10", "Leica DG Macro-Elmarit 45mm F2.8 Asph. Mega OIS"); //PH + _olympusLensTypes.put("2 07 10", "Lumix G Vario 14-42mm F3.5-5.6 Asph. Mega OIS"); //20 + _olympusLensTypes.put("2 08 10", "Lumix G Fisheye 8mm F3.5"); //PH + _olympusLensTypes.put("2 09 10", "Lumix G Vario 100-300mm F4.0-5.6 Mega OIS"); //11 + _olympusLensTypes.put("2 10 10", "Lumix G 14mm F2.5 Asph."); //17 + _olympusLensTypes.put("2 11 10", "Lumix G 12.5mm F12 3D"); //20 (H-FT012) + _olympusLensTypes.put("2 12 10", "Leica DG Summilux 25mm F1.4 Asph."); //20 + _olympusLensTypes.put("2 13 10", "Lumix G X Vario PZ 45-175mm F4.0-5.6 Asph. Power OIS"); //20 + _olympusLensTypes.put("2 14 10", "Lumix G X Vario PZ 14-42mm F3.5-5.6 Asph. Power OIS"); //20 + _olympusLensTypes.put("2 15 10", "Lumix G X Vario 12-35mm F2.8 Asph. Power OIS"); //PH + _olympusLensTypes.put("2 16 10", "Lumix G Vario 45-150mm F4.0-5.6 Asph. Mega OIS"); //20 + _olympusLensTypes.put("2 17 10", "Lumix G X Vario 35-100mm F2.8 Power OIS"); //PH + _olympusLensTypes.put("2 18 10", "Lumix G Vario 14-42mm F3.5-5.6 II Asph. Mega OIS"); //20 + _olympusLensTypes.put("2 19 10", "Lumix G Vario 14-140mm F3.5-5.6 Asph. Power OIS"); //20 + _olympusLensTypes.put("2 20 10", "Lumix G Vario 12-32mm F3.5-5.6 Asph. Mega OIS"); //20 + _olympusLensTypes.put("2 21 10", "Leica DG Nocticron 42.5mm F1.2 Asph. Power OIS"); //20 + _olympusLensTypes.put("2 22 10", "Leica DG Summilux 15mm F1.7 Asph."); //20 + // '2 23 10", "Lumix G Vario 35-100mm F4.0-5.6 Asph. Mega OIS"); //20 (guess) + _olympusLensTypes.put("2 24 10", "Lumix G Macro 30mm F2.8 Asph. Mega OIS"); //20 + _olympusLensTypes.put("2 25 10", "Lumix G 42.5mm F1.7 Asph. Power OIS"); //20 + _olympusLensTypes.put("3 01 00", "Leica D Vario Elmarit 14-50mm F2.8-3.5 Asph."); //11 + _olympusLensTypes.put("3 02 00", "Leica D Summilux 25mm F1.4 Asph."); //11 + // Tamron lenses + _olympusLensTypes.put("5 01 10", "Tamron 14-150mm F3.5-5.8 Di III"); //20 (model C001) + + + _olympusExtenderTypes.put("0 00", "None"); + _olympusExtenderTypes.put("0 04", "Olympus Zuiko Digital EC-14 1.4x Teleconverter"); + _olympusExtenderTypes.put("0 08", "Olympus EX-25 Extension Tube"); + _olympusExtenderTypes.put("0 10", "Olympus Zuiko Digital EC-20 2.0x Teleconverter"); + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java new file mode 100644 index 0000000..eb3d50c --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusEquipmentMakernoteDirectory.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * The Olympus equipment makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags + * that appear specific to those manufacturers. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusEquipmentMakernoteDirectory extends Directory +{ + public static final int TAG_EQUIPMENT_VERSION = 0x0000; + public static final int TAG_CAMERA_TYPE_2 = 0x0100; + public static final int TAG_SERIAL_NUMBER = 0x0101; + + public static final int TAG_INTERNAL_SERIAL_NUMBER = 0x0102; + public static final int TAG_FOCAL_PLANE_DIAGONAL = 0x0103; + public static final int TAG_BODY_FIRMWARE_VERSION = 0x0104; + + public static final int TAG_LENS_TYPE = 0x0201; + public static final int TAG_LENS_SERIAL_NUMBER = 0x0202; + public static final int TAG_LENS_MODEL = 0x0203; + public static final int TAG_LENS_FIRMWARE_VERSION = 0x0204; + public static final int TAG_MAX_APERTURE_AT_MIN_FOCAL = 0x0205; + public static final int TAG_MAX_APERTURE_AT_MAX_FOCAL = 0x0206; + public static final int TAG_MIN_FOCAL_LENGTH = 0x0207; + public static final int TAG_MAX_FOCAL_LENGTH = 0x0208; + public static final int TAG_MAX_APERTURE = 0x020A; + public static final int TAG_LENS_PROPERTIES = 0x020B; + + public static final int TAG_EXTENDER = 0x0301; + public static final int TAG_EXTENDER_SERIAL_NUMBER = 0x0302; + public static final int TAG_EXTENDER_MODEL = 0x0303; + public static final int TAG_EXTENDER_FIRMWARE_VERSION = 0x0304; + + public static final int TAG_CONVERSION_LENS = 0x0403; + + public static final int TAG_FLASH_TYPE = 0x1000; + public static final int TAG_FLASH_MODEL = 0x1001; + public static final int TAG_FLASH_FIRMWARE_VERSION = 0x1002; + public static final int TAG_FLASH_SERIAL_NUMBER = 0x1003; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TAG_EQUIPMENT_VERSION, "Equipment Version"); + _tagNameMap.put(TAG_CAMERA_TYPE_2, "Camera Type 2"); + _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number"); + _tagNameMap.put(TAG_INTERNAL_SERIAL_NUMBER, "Internal Serial Number"); + _tagNameMap.put(TAG_FOCAL_PLANE_DIAGONAL, "Focal Plane Diagonal"); + _tagNameMap.put(TAG_BODY_FIRMWARE_VERSION, "Body Firmware Version"); + _tagNameMap.put(TAG_LENS_TYPE, "Lens Type"); + _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number"); + _tagNameMap.put(TAG_LENS_MODEL, "Lens Model"); + _tagNameMap.put(TAG_LENS_FIRMWARE_VERSION, "Lens Firmware Version"); + _tagNameMap.put(TAG_MAX_APERTURE_AT_MIN_FOCAL, "Max Aperture At Min Focal"); + _tagNameMap.put(TAG_MAX_APERTURE_AT_MAX_FOCAL, "Max Aperture At Max Focal"); + _tagNameMap.put(TAG_MIN_FOCAL_LENGTH, "Min Focal Length"); + _tagNameMap.put(TAG_MAX_FOCAL_LENGTH, "Max Focal Length"); + _tagNameMap.put(TAG_MAX_APERTURE, "Max Aperture"); + _tagNameMap.put(TAG_LENS_PROPERTIES, "Lens Properties"); + _tagNameMap.put(TAG_EXTENDER, "Extender"); + _tagNameMap.put(TAG_EXTENDER_SERIAL_NUMBER, "Extender Serial Number"); + _tagNameMap.put(TAG_EXTENDER_MODEL, "Extender Model"); + _tagNameMap.put(TAG_EXTENDER_FIRMWARE_VERSION, "Extender Firmware Version"); + _tagNameMap.put(TAG_CONVERSION_LENS, "Conversion Lens"); + _tagNameMap.put(TAG_FLASH_TYPE, "Flash Type"); + _tagNameMap.put(TAG_FLASH_MODEL, "Flash Model"); + _tagNameMap.put(TAG_FLASH_FIRMWARE_VERSION, "Flash Firmware Version"); + _tagNameMap.put(TAG_FLASH_SERIAL_NUMBER, "Flash Serial Number"); + } + + public OlympusEquipmentMakernoteDirectory() + { + this.setDescriptor(new OlympusEquipmentMakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Olympus Equipment"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java new file mode 100644 index 0000000..b219b1d --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDescriptor.java @@ -0,0 +1,217 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.Rational; +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.makernotes.OlympusFocusInfoMakernoteDirectory.*; + +/** + * Provides human-readable String representations of tag values stored in a {@link OlympusFocusInfoMakernoteDirectory}. + * <p> + * Some Description functions converted from Exiftool version 10.10 created by Phil Harvey + * http://www.sno.phy.queensu.ca/~phil/exiftool/ + * lib\Image\ExifTool\Olympus.pm + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusFocusInfoMakernoteDescriptor extends TagDescriptor<OlympusFocusInfoMakernoteDirectory> +{ + public OlympusFocusInfoMakernoteDescriptor(@NotNull OlympusFocusInfoMakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagFocusInfoVersion: + return getFocusInfoVersionDescription(); + case TagAutoFocus: + return getAutoFocusDescription(); + case TagFocusDistance: + return getFocusDistanceDescription(); + case TagAfPoint: + return getAfPointDescription(); + case TagExternalFlash: + return getExternalFlashDescription(); + case TagExternalFlashBounce: + return getExternalFlashBounceDescription(); + case TagExternalFlashZoom: + return getExternalFlashZoomDescription(); + case TagManualFlash: + return getManualFlashDescription(); + case TagMacroLed: + return getMacroLedDescription(); + case TagSensorTemperature: + return getSensorTemperatureDescription(); + case TagImageStabilization: + return getImageStabilizationDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getFocusInfoVersionDescription() + { + return getVersionBytesDescription(TagFocusInfoVersion, 4); + } + + @Nullable + public String getAutoFocusDescription() + { + return getIndexedDescription(TagAutoFocus, + "Off", "On"); + } + + @Nullable + public String getFocusDistanceDescription() + { + Rational value = _directory.getRational(TagFocusDistance); + if (value == null) + return "inf"; + if (value.getNumerator() == 0xFFFFFFFFL || value.getNumerator() == 0x00000000L) + return "inf"; + + return value.getNumerator() / 1000.0 + " m"; + } + + @Nullable + public String getAfPointDescription() + { + Integer value = _directory.getInteger(TagAfPoint); + if (value == null) + return null; + + return value.toString(); + } + + @Nullable + public String getExternalFlashDescription() + { + int[] values = _directory.getIntArray(TagExternalFlash); + if (values == null || values.length < 2) + return null; + + String join = String.format("%d %d", (short)values[0], (short)values[1]); + + if(join.equals("0 0")) + return "Off"; + else if(join.equals("1 0")) + return "On"; + else + return "Unknown (" + join + ")"; + } + + @Nullable + public String getExternalFlashBounceDescription() + { + return getIndexedDescription(TagExternalFlashBounce, + "Bounce or Off", "Direct"); + } + + @Nullable + public String getExternalFlashZoomDescription() + { + int[] values = _directory.getIntArray(TagExternalFlashZoom); + if (values == null) + { + // check if it's only one value long also + Integer value = _directory.getInteger(TagExternalFlashZoom); + if(value == null) + return null; + + values = new int[1]; + values[0] = value; + } + + if (values.length == 0) + return null; + + String join = String.format("%d", (short)values[0]); + if(values.length > 1) + join += " " + String.format("%d", (short)values[1]); + + if(join.equals("0")) + return "Off"; + else if(join.equals("1")) + return "On"; + else if(join.equals("0 0")) + return "Off"; + else if(join.equals("1 0")) + return "On"; + else + return "Unknown (" + join + ")"; + + } + + @Nullable + public String getManualFlashDescription() + { + int[] values = _directory.getIntArray(TagManualFlash); + if (values == null) + return null; + + if ((short)values[0] == 0) + return "Off"; + + if ((short)values[1] == 1) + return "Full"; + return "On (1/" + (short)values[1] + " strength)"; + } + + @Nullable + public String getMacroLedDescription() + { + return getIndexedDescription(TagMacroLed, + "Off", "On"); + } + + /// <remarks> + /// <para>TODO: Complete when Camera Model is available.</para> + /// <para>There are differences in how to interpret this tag that can only be reconciled by knowing the model.</para> + /// </remarks> + @Nullable + public String getSensorTemperatureDescription() + { + return _directory.getString(TagSensorTemperature); + } + + @Nullable + public String getImageStabilizationDescription() + { + byte[] values = _directory.getByteArray(TagImageStabilization); + if (values == null) + return null; + + if((values[0] | values[1] | values[2] | values[3]) == 0x0) + return "Off"; + return "On, " + ((values[43] & 1) > 0 ? "Mode 1" : "Mode 2"); + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java new file mode 100644 index 0000000..898ae3d --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusFocusInfoMakernoteDirectory.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * The Olympus focus info makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags + * that appear specific to those manufacturers. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusFocusInfoMakernoteDirectory extends Directory +{ + public static final int TagFocusInfoVersion = 0x0000; + public static final int TagAutoFocus = 0x0209; + public static final int TagSceneDetect = 0x0210; + public static final int TagSceneArea = 0x0211; + public static final int TagSceneDetectData = 0x0212; + + public static final int TagZoomStepCount = 0x0300; + public static final int TagFocusStepCount = 0x0301; + public static final int TagFocusStepInfinity = 0x0303; + public static final int TagFocusStepNear = 0x0304; + public static final int TagFocusDistance = 0x0305; + public static final int TagAfPoint = 0x0308; + // 0x031a Continuous AF parameters? + public static final int TagAfInfo = 0x0328; // ifd + + public static final int TagExternalFlash = 0x1201; + public static final int TagExternalFlashGuideNumber = 0x1203; + public static final int TagExternalFlashBounce = 0x1204; + public static final int TagExternalFlashZoom = 0x1205; + public static final int TagInternalFlash = 0x1208; + public static final int TagManualFlash = 0x1209; + public static final int TagMacroLed = 0x120A; + + public static final int TagSensorTemperature = 0x1500; + + public static final int TagImageStabilization = 0x1600; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TagFocusInfoVersion, "Focus Info Version"); + _tagNameMap.put(TagAutoFocus, "Auto Focus"); + _tagNameMap.put(TagSceneDetect, "Scene Detect"); + _tagNameMap.put(TagSceneArea, "Scene Area"); + _tagNameMap.put(TagSceneDetectData, "Scene Detect Data"); + _tagNameMap.put(TagZoomStepCount, "Zoom Step Count"); + _tagNameMap.put(TagFocusStepCount, "Focus Step Count"); + _tagNameMap.put(TagFocusStepInfinity, "Focus Step Infinity"); + _tagNameMap.put(TagFocusStepNear, "Focus Step Near"); + _tagNameMap.put(TagFocusDistance, "Focus Distance"); + _tagNameMap.put(TagAfPoint, "AF Point"); + _tagNameMap.put(TagAfInfo, "AF Info"); + _tagNameMap.put(TagExternalFlash, "External Flash"); + _tagNameMap.put(TagExternalFlashGuideNumber, "External Flash Guide Number"); + _tagNameMap.put(TagExternalFlashBounce, "External Flash Bounce"); + _tagNameMap.put(TagExternalFlashZoom, "External Flash Zoom"); + _tagNameMap.put(TagInternalFlash, "Internal Flash"); + _tagNameMap.put(TagManualFlash, "Manual Flash"); + _tagNameMap.put(TagMacroLed, "Macro LED"); + _tagNameMap.put(TagSensorTemperature, "Sensor Temperature"); + _tagNameMap.put(TagImageStabilization, "Image Stabilization"); + } + + public OlympusFocusInfoMakernoteDirectory() + { + this.setDescriptor(new OlympusFocusInfoMakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Olympus Focus Info"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.java new file mode 100644 index 0000000..c2add9a --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDescriptor.java @@ -0,0 +1,240 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.makernotes.OlympusImageProcessingMakernoteDirectory.*; + +/** + * Provides human-readable String representations of tag values stored in a {@link OlympusImageProcessingMakernoteDirectory}. + * <p> + * Some Description functions converted from Exiftool version 10.33 created by Phil Harvey + * http://www.sno.phy.queensu.ca/~phil/exiftool/ + * lib\Image\ExifTool\Olympus.pm + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusImageProcessingMakernoteDescriptor extends TagDescriptor<OlympusImageProcessingMakernoteDirectory> +{ + public OlympusImageProcessingMakernoteDescriptor(@NotNull OlympusImageProcessingMakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagImageProcessingVersion: + return getImageProcessingVersionDescription(); + case TagColorMatrix: + return getColorMatrixDescription(); + case TagNoiseReduction2: + return getNoiseReduction2Description(); + case TagDistortionCorrection2: + return getDistortionCorrection2Description(); + case TagShadingCompensation2: + return getShadingCompensation2Description(); + case TagMultipleExposureMode: + return getMultipleExposureModeDescription(); + case TagAspectRatio: + return getAspectRatioDescription(); + case TagKeystoneCompensation: + return getKeystoneCompensationDescription(); + case TagKeystoneDirection: + return getKeystoneDirectionDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getImageProcessingVersionDescription() + { + return getVersionBytesDescription(TagImageProcessingVersion, 4); + } + + @Nullable + public String getColorMatrixDescription() + { + int[] obj = _directory.getIntArray(TagColorMatrix); + if (obj == null) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < obj.length; i++) { + if (i != 0) + sb.append(" "); + sb.append((short)obj[i]); + } + return sb.toString(); + } + + @Nullable + public String getNoiseReduction2Description() + { + Integer value = _directory.getInteger(TagNoiseReduction2); + if (value == null) + return null; + + if (value == 0) + return "(none)"; + + StringBuilder sb = new StringBuilder(); + short v = value.shortValue(); + + if (( v & 1) != 0) sb.append("Noise Reduction, "); + if (((v >> 1) & 1) != 0) sb.append("Noise Filter, "); + if (((v >> 2) & 1) != 0) sb.append("Noise Filter (ISO Boost), "); + + return sb.substring(0, sb.length() - 2); + } + + @Nullable + public String getDistortionCorrection2Description() + { + return getIndexedDescription(TagDistortionCorrection2, "Off", "On"); + } + + @Nullable + public String getShadingCompensation2Description() + { + return getIndexedDescription(TagShadingCompensation2, "Off", "On"); + } + + @Nullable + public String getMultipleExposureModeDescription() + { + int[] values = _directory.getIntArray(TagMultipleExposureMode); + if (values == null) + { + // check if it's only one value long also + Integer value = _directory.getInteger(TagMultipleExposureMode); + if(value == null) + return null; + + values = new int[1]; + values[0] = value; + } + + if (values.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + + switch ((short)values[0]) + { + case 0: + sb.append("Off"); + break; + case 2: + sb.append("On (2 frames)"); + break; + case 3: + sb.append("On (3 frames)"); + break; + default: + sb.append("Unknown (").append((short)values[0]).append(")"); + break; + } + + if (values.length > 1) + sb.append("; ").append((short)values[1]); + + return sb.toString(); + } + + @Nullable + public String getAspectRatioDescription() + { + byte[] values = _directory.getByteArray(TagAspectRatio); + if (values == null || values.length < 2) + return null; + + String join = String.format("%d %d", values[0], values[1]); + + String ret; + if(join.equals("1 1")) + ret = "4:3"; + else if(join.equals("1 4")) + ret = "1:1"; + else if(join.equals("2 1")) + ret = "3:2 (RAW)"; + else if(join.equals("2 2")) + ret = "3:2"; + else if(join.equals("3 1")) + ret = "16:9 (RAW)"; + else if(join.equals("3 3")) + ret = "16:9"; + else if(join.equals("4 1")) + ret = "1:1 (RAW)"; + else if(join.equals("4 4")) + ret = "6:6"; + else if(join.equals("5 5")) + ret = "5:4"; + else if(join.equals("6 6")) + ret = "7:6"; + else if(join.equals("7 7")) + ret = "6:5"; + else if(join.equals("8 8")) + ret = "7:5"; + else if(join.equals("9 1")) + ret = "3:4 (RAW)"; + else if(join.equals("9 9")) + ret = "3:4"; + else + ret = "Unknown (" + join + ")"; + + return ret; + } + + @Nullable + public String getKeystoneCompensationDescription() + { + byte[] values = _directory.getByteArray(TagKeystoneCompensation); + if (values == null || values.length < 2) + return null; + + String join = String.format("%d %d", values[0], values[1]); + + String ret; + if(join.equals("0 0")) + ret = "Off"; + else if(join.equals("0 1")) + ret = "On"; + else + ret = "Unknown (" + join + ")"; + + return ret; + } + + @Nullable + public String getKeystoneDirectionDescription() + { + return getIndexedDescription(TagKeystoneDirection, "Vertical", "Horizontal"); + } +} \ No newline at end of file diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java new file mode 100644 index 0000000..4452584 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusImageProcessingMakernoteDirectory.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * The Olympus image processing makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags + * that appear specific to those manufacturers. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusImageProcessingMakernoteDirectory extends Directory +{ + public static final int TagImageProcessingVersion = 0x0000; + public static final int TagWbRbLevels = 0x0100; + // 0x0101 - in-camera AutoWB unless it is all 0's or all 256's (ref IB) + public static final int TagWbRbLevels3000K = 0x0102; + public static final int TagWbRbLevels3300K = 0x0103; + public static final int TagWbRbLevels3600K = 0x0104; + public static final int TagWbRbLevels3900K = 0x0105; + public static final int TagWbRbLevels4000K = 0x0106; + public static final int TagWbRbLevels4300K = 0x0107; + public static final int TagWbRbLevels4500K = 0x0108; + public static final int TagWbRbLevels4800K = 0x0109; + public static final int TagWbRbLevels5300K = 0x010a; + public static final int TagWbRbLevels6000K = 0x010b; + public static final int TagWbRbLevels6600K = 0x010c; + public static final int TagWbRbLevels7500K = 0x010d; + public static final int TagWbRbLevelsCwB1 = 0x010e; + public static final int TagWbRbLevelsCwB2 = 0x010f; + public static final int TagWbRbLevelsCwB3 = 0x0110; + public static final int TagWbRbLevelsCwB4 = 0x0111; + public static final int TagWbGLevel3000K = 0x0113; + public static final int TagWbGLevel3300K = 0x0114; + public static final int TagWbGLevel3600K = 0x0115; + public static final int TagWbGLevel3900K = 0x0116; + public static final int TagWbGLevel4000K = 0x0117; + public static final int TagWbGLevel4300K = 0x0118; + public static final int TagWbGLevel4500K = 0x0119; + public static final int TagWbGLevel4800K = 0x011a; + public static final int TagWbGLevel5300K = 0x011b; + public static final int TagWbGLevel6000K = 0x011c; + public static final int TagWbGLevel6600K = 0x011d; + public static final int TagWbGLevel7500K = 0x011e; + public static final int TagWbGLevel = 0x011f; + // 0x0121 = WB preset for flash (about 6000K) (ref IB) + // 0x0125 = WB preset for underwater (ref IB) + + public static final int TagColorMatrix = 0x0200; + // color matrices (ref 11): + // 0x0201-0x020d are sRGB color matrices + // 0x020e-0x021a are Adobe RGB color matrices + // 0x021b-0x0227 are ProPhoto RGB color matrices + // 0x0228 and 0x0229 are ColorMatrix for E-330 + // 0x0250-0x0252 are sRGB color matrices + // 0x0253-0x0255 are Adobe RGB color matrices + // 0x0256-0x0258 are ProPhoto RGB color matrices + + public static final int TagEnhancer = 0x0300; + public static final int TagEnhancerValues = 0x0301; + public static final int TagCoringFilter = 0x0310; + public static final int TagCoringValues = 0x0311; + public static final int TagBlackLevel2 = 0x0600; + public static final int TagGainBase = 0x0610; + public static final int TagValidBits = 0x0611; + public static final int TagCropLeft = 0x0612; + public static final int TagCropTop = 0x0613; + public static final int TagCropWidth = 0x0614; + public static final int TagCropHeight = 0x0615; + public static final int TagUnknownBlock1 = 0x0635; + public static final int TagUnknownBlock2 = 0x0636; + + // 0x0800 LensDistortionParams, float[9] (ref 11) + // 0x0801 LensShadingParams, int16u[16] (ref 11) + public static final int TagSensorCalibration = 0x0805; + + public static final int TagNoiseReduction2 = 0x1010; + public static final int TagDistortionCorrection2 = 0x1011; + public static final int TagShadingCompensation2 = 0x1012; + public static final int TagMultipleExposureMode = 0x101c; + public static final int TagUnknownBlock3 = 0x1103; + public static final int TagUnknownBlock4 = 0x1104; + public static final int TagAspectRatio = 0x1112; + public static final int TagAspectFrame = 0x1113; + public static final int TagFacesDetected = 0x1200; + public static final int TagFaceDetectArea = 0x1201; + public static final int TagMaxFaces = 0x1202; + public static final int TagFaceDetectFrameSize = 0x1203; + public static final int TagFaceDetectFrameCrop = 0x1207; + public static final int TagCameraTemperature = 0x1306; + + public static final int TagKeystoneCompensation = 0x1900; + public static final int TagKeystoneDirection = 0x1901; + // 0x1905 - focal length (PH, E-M1) + public static final int TagKeystoneValue = 0x1906; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TagImageProcessingVersion, "Image Processing Version"); + _tagNameMap.put(TagWbRbLevels, "WB RB Levels"); + _tagNameMap.put(TagWbRbLevels3000K, "WB RB Levels 3000K"); + _tagNameMap.put(TagWbRbLevels3300K, "WB RB Levels 3300K"); + _tagNameMap.put(TagWbRbLevels3600K, "WB RB Levels 3600K"); + _tagNameMap.put(TagWbRbLevels3900K, "WB RB Levels 3900K"); + _tagNameMap.put(TagWbRbLevels4000K, "WB RB Levels 4000K"); + _tagNameMap.put(TagWbRbLevels4300K, "WB RB Levels 4300K"); + _tagNameMap.put(TagWbRbLevels4500K, "WB RB Levels 4500K"); + _tagNameMap.put(TagWbRbLevels4800K, "WB RB Levels 4800K"); + _tagNameMap.put(TagWbRbLevels5300K, "WB RB Levels 5300K"); + _tagNameMap.put(TagWbRbLevels6000K, "WB RB Levels 6000K"); + _tagNameMap.put(TagWbRbLevels6600K, "WB RB Levels 6600K"); + _tagNameMap.put(TagWbRbLevels7500K, "WB RB Levels 7500K"); + _tagNameMap.put(TagWbRbLevelsCwB1, "WB RB Levels CWB1"); + _tagNameMap.put(TagWbRbLevelsCwB2, "WB RB Levels CWB2"); + _tagNameMap.put(TagWbRbLevelsCwB3, "WB RB Levels CWB3"); + _tagNameMap.put(TagWbRbLevelsCwB4, "WB RB Levels CWB4"); + _tagNameMap.put(TagWbGLevel3000K, "WB G Level 3000K"); + _tagNameMap.put(TagWbGLevel3300K, "WB G Level 3300K"); + _tagNameMap.put(TagWbGLevel3600K, "WB G Level 3600K"); + _tagNameMap.put(TagWbGLevel3900K, "WB G Level 3900K"); + _tagNameMap.put(TagWbGLevel4000K, "WB G Level 4000K"); + _tagNameMap.put(TagWbGLevel4300K, "WB G Level 4300K"); + _tagNameMap.put(TagWbGLevel4500K, "WB G Level 4500K"); + _tagNameMap.put(TagWbGLevel4800K, "WB G Level 4800K"); + _tagNameMap.put(TagWbGLevel5300K, "WB G Level 5300K"); + _tagNameMap.put(TagWbGLevel6000K, "WB G Level 6000K"); + _tagNameMap.put(TagWbGLevel6600K, "WB G Level 6600K"); + _tagNameMap.put(TagWbGLevel7500K, "WB G Level 7500K"); + _tagNameMap.put(TagWbGLevel, "WB G Level"); + + _tagNameMap.put(TagColorMatrix, "Color Matrix"); + + _tagNameMap.put(TagEnhancer, "Enhancer"); + _tagNameMap.put(TagEnhancerValues, "Enhancer Values"); + _tagNameMap.put(TagCoringFilter, "Coring Filter"); + _tagNameMap.put(TagCoringValues, "Coring Values"); + _tagNameMap.put(TagBlackLevel2, "Black Level 2"); + _tagNameMap.put(TagGainBase, "Gain Base"); + _tagNameMap.put(TagValidBits, "Valid Bits"); + _tagNameMap.put(TagCropLeft, "Crop Left"); + _tagNameMap.put(TagCropTop, "Crop Top"); + _tagNameMap.put(TagCropWidth, "Crop Width"); + _tagNameMap.put(TagCropHeight, "Crop Height"); + _tagNameMap.put(TagUnknownBlock1, "Unknown Block 1"); + _tagNameMap.put(TagUnknownBlock2, "Unknown Block 2"); + + _tagNameMap.put(TagSensorCalibration, "Sensor Calibration"); + + _tagNameMap.put(TagNoiseReduction2, "Noise Reduction 2"); + _tagNameMap.put(TagDistortionCorrection2, "Distortion Correction 2"); + _tagNameMap.put(TagShadingCompensation2, "Shading Compensation 2"); + _tagNameMap.put(TagMultipleExposureMode, "Multiple Exposure Mode"); + _tagNameMap.put(TagUnknownBlock3, "Unknown Block 3"); + _tagNameMap.put(TagUnknownBlock4, "Unknown Block 4"); + _tagNameMap.put(TagAspectRatio, "Aspect Ratio"); + _tagNameMap.put(TagAspectFrame, "Aspect Frame"); + _tagNameMap.put(TagFacesDetected, "Faces Detected"); + _tagNameMap.put(TagFaceDetectArea, "Face Detect Area"); + _tagNameMap.put(TagMaxFaces, "Max Faces"); + _tagNameMap.put(TagFaceDetectFrameSize, "Face Detect Frame Size"); + _tagNameMap.put(TagFaceDetectFrameCrop, "Face Detect Frame Crop"); + _tagNameMap.put(TagCameraTemperature , "Camera Temperature"); + _tagNameMap.put(TagKeystoneCompensation, "Keystone Compensation"); + _tagNameMap.put(TagKeystoneDirection, "Keystone Direction"); + _tagNameMap.put(TagKeystoneValue, "Keystone Value"); + } + + public OlympusImageProcessingMakernoteDirectory() + { + this.setDescriptor(new OlympusImageProcessingMakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Olympus Image Processing"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java index f003fcb..41a313d 100644 --- a/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,11 +20,15 @@ */ package com.drew.metadata.exif.makernotes; +import com.drew.imaging.PhotographicConversions; +import com.drew.lang.Rational; +import com.drew.lang.DateUtil; import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; -import java.util.GregorianCalendar; +import java.math.RoundingMode; +import java.text.DecimalFormat; import static com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory.*; @@ -33,6 +37,7 @@ import static com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDirectory> { // TODO extend support for some offset-encoded byte[] tags: http://www.ozhiker.com/electronics/pjmt/jpeg_info/olympus_mn.html @@ -63,10 +68,22 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi return getMacroModeDescription(); case TAG_BW_MODE: return getBWModeDescription(); - case TAG_DIGI_ZOOM_RATIO: - return getDigiZoomRatioDescription(); + case TAG_DIGITAL_ZOOM: + return getDigitalZoomDescription(); + case TAG_FOCAL_PLANE_DIAGONAL: + return getFocalPlaneDiagonalDescription(); + case TAG_CAMERA_TYPE: + return getCameraTypeDescription(); case TAG_CAMERA_ID: return getCameraIdDescription(); + case TAG_ONE_TOUCH_WB: + return getOneTouchWbDescription(); + case TAG_SHUTTER_SPEED_VALUE: + return getShutterSpeedDescription(); + case TAG_ISO_VALUE: + return getIsoValueDescription(); + case TAG_APERTURE_VALUE: + return getApertureValueDescription(); case TAG_FLASH_MODE: return getFlashModeDescription(); case TAG_FOCUS_RANGE: @@ -75,6 +92,18 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi return getFocusModeDescription(); case TAG_SHARPNESS: return getSharpnessDescription(); + case TAG_COLOUR_MATRIX: + return getColorMatrixDescription(); + case TAG_WB_MODE: + return getWbModeDescription(); + case TAG_RED_BALANCE: + return getRedBalanceDescription(); + case TAG_BLUE_BALANCE: + return getBlueBalanceDescription(); + case TAG_CONTRAST: + return getContrastDescription(); + case TAG_PREVIEW_IMAGE_VALID: + return getPreviewImageValidDescription(); case CameraSettings.TAG_EXPOSURE_MODE: return getExposureModeDescription(); @@ -99,7 +128,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi case CameraSettings.TAG_MACRO_MODE: return getMacroModeCameraSettingDescription(); case CameraSettings.TAG_DIGITAL_ZOOM: - return getDigitalZoomDescription(); + return getDigitalZoomCameraSettingDescription(); case CameraSettings.TAG_EXPOSURE_COMPENSATION: return getExposureCompensationDescription(); case CameraSettings.TAG_BRACKET_STEP: @@ -114,7 +143,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi case CameraSettings.TAG_FOCUS_DISTANCE: return getFocusDistanceDescription(); case CameraSettings.TAG_FLASH_FIRED: - return getFlastFiredDescription(); + return getFlashFiredDescription(); case CameraSettings.TAG_DATE: return getDateDescription(); case CameraSettings.TAG_TIME: @@ -135,13 +164,13 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi case CameraSettings.TAG_SATURATION: return getSaturationDescription(); case CameraSettings.TAG_CONTRAST: - return getContrastDescription(); + return getContrastCameraSettingDescription(); case CameraSettings.TAG_SHARPNESS: return getSharpnessCameraSettingDescription(); case CameraSettings.TAG_SUBJECT_PROGRAM: return getSubjectProgramDescription(); case CameraSettings.TAG_FLASH_COMPENSATION: - return getFlastCompensationDescription(); + return getFlashCompensationDescription(); case CameraSettings.TAG_ISO_SETTING: return getIsoSettingDescription(); case CameraSettings.TAG_CAMERA_MODEL: @@ -257,7 +286,9 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi return null; double iso = Math.pow((value / 8d) - 1, 2) * 3.125; - return Double.toString(iso); + DecimalFormat format = new DecimalFormat("0.##"); + format.setRoundingMode(RoundingMode.HALF_UP); + return format.format(iso); } @Nullable @@ -273,7 +304,9 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi return null; double shutterSpeed = Math.pow((49-value) / 8d, 2); - return Double.toString(shutterSpeed) + " sec"; + DecimalFormat format = new DecimalFormat("0.###"); + format.setRoundingMode(RoundingMode.HALF_UP); + return format.format(shutterSpeed) + " sec"; } @Nullable @@ -288,7 +321,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi return null; double fStop = Math.pow((value/16d) - 0.5, 2); - return "F" + Double.toString(fStop); + return getFStopDescription(fStop); } @Nullable @@ -298,7 +331,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi } @Nullable - public String getDigitalZoomDescription() + public String getDigitalZoomCameraSettingDescription() { return getIndexedDescription(CameraSettings.TAG_DIGITAL_ZOOM, "Off", "Electronic magnification", "Digital zoom 2x"); } @@ -307,7 +340,8 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi public String getExposureCompensationDescription() { Long value = _directory.getLongObject(CameraSettings.TAG_EXPOSURE_COMPENSATION); - return value == null ? null : ((value / 3d) - 2) + " EV"; + DecimalFormat format = new DecimalFormat("0.##"); + return value == null ? null : format.format((value / 3d) - 2) + " EV"; } @Nullable @@ -340,7 +374,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi public String getFocalLengthDescription() { Long value = _directory.getLongObject(CameraSettings.TAG_FOCAL_LENGTH); - return value == null ? null : Double.toString(value/256d) + " mm"; + return value == null ? null : getFocalLengthDescription(value/256d); } @Nullable @@ -355,7 +389,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi } @Nullable - public String getFlastFiredDescription() + public String getFlashFiredDescription() { return getIndexedDescription(CameraSettings.TAG_FLASH_FIRED, "No", "Yes"); } @@ -369,10 +403,15 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi Long value = _directory.getLongObject(CameraSettings.TAG_DATE); if (value == null) return null; - long day = value & 0xFF; - long month = (value >> 16) & 0xFF; - long year = (value >> 8) & 0xFF; - return new GregorianCalendar((int)year + 1970, (int)month, (int)day).getTime().toString(); + + int day = (int) (value & 0xFF); + int month = (int) ((value >> 16) & 0xFF); + int year = (int) ((value >> 8) & 0xFF) + 1970; + + if (!DateUtil.isValidDate(year, month, day)) + return "Invalid date"; + + return String.format("%04d-%02d-%02d", year, month + 1, day); } @Nullable @@ -384,9 +423,13 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi Long value = _directory.getLongObject(CameraSettings.TAG_TIME); if (value == null) return null; - long hours = (value >> 8) & 0xFF; - long minutes = (value >> 16) & 0xFF; - long seconds = value & 0xFF; + + int hours = (int) ((value >> 8) & 0xFF); + int minutes = (int) ((value >> 16) & 0xFF); + int seconds = (int) (value & 0xFF); + + if (!DateUtil.isValidTime(hours, minutes, seconds)) + return "Invalid time"; return String.format("%02d:%02d:%02d", hours, minutes, seconds); } @@ -399,7 +442,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi if (value == null) return null; double fStop = Math.pow((value/16d) - 0.5, 2); - return "F" + fStop; + return getFStopDescription(fStop); } @Nullable @@ -423,21 +466,24 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi public String getWhiteBalanceRedDescription() { Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_RED); - return value == null ? null : Double.toString(value/256d); + DecimalFormat format = new DecimalFormat("0.##"); + return value == null ? null : format.format(value/256d); } @Nullable public String getWhiteBalanceGreenDescription() { Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_GREEN); - return value == null ? null : Double.toString(value/256d); + DecimalFormat format = new DecimalFormat("0.##"); + return value == null ? null : format.format(value/256d); } @Nullable public String getWhiteBalanceBlueDescription() { Long value = _directory.getLongObject(CameraSettings.TAG_WHITE_BALANCE_BLUE); - return value == null ? null : Double.toString(value/256d); + DecimalFormat format = new DecimalFormat("0.##"); + return value == null ? null : format.format(value / 256d); } @Nullable @@ -448,7 +494,7 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi } @Nullable - public String getContrastDescription() + public String getContrastCameraSettingDescription() { Long value = _directory.getLongObject(CameraSettings.TAG_CONTRAST); return value == null ? null : Long.toString(value-3); @@ -467,10 +513,11 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi } @Nullable - public String getFlastCompensationDescription() + public String getFlashCompensationDescription() { Long value = _directory.getLongObject(CameraSettings.TAG_FLASH_COMPENSATION); - return value == null ? null : ((value-6)/3d) + " EV"; + DecimalFormat format = new DecimalFormat("0.##"); + return value == null ? null : format.format((value-6)/3d) + " EV"; } @Nullable @@ -534,7 +581,8 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi public String getApexBrightnessDescription() { Long value = _directory.getLongObject(CameraSettings.TAG_APEX_BRIGHTNESS_VALUE); - return value == null ? null : Double.toString((value/8d)-6); + DecimalFormat format = new DecimalFormat("0.##"); + return value == null ? null : format.format((value/8d)-6); } @Nullable @@ -624,6 +672,93 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi return getIndexedDescription(TAG_SHARPNESS, "Normal", "Hard", "Soft"); } + @Nullable + public String getColorMatrixDescription() + { + int[] obj = _directory.getIntArray(TAG_COLOUR_MATRIX); + if (obj == null) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < obj.length; i++) { + sb.append((short)obj[i]); + if (i < obj.length - 1) + sb.append(" "); + } + return sb.length() == 0 ? null : sb.toString(); + } + + @Nullable + public String getWbModeDescription() + { + int[] obj = _directory.getIntArray(TAG_WB_MODE); + if (obj == null) + return null; + + String val = String.format("%d %d", obj[0], obj[1]); + + if(val.equals("1 0")) + return "Auto"; + else if(val.equals("1 2")) + return "Auto (2)"; + else if(val.equals("1 4")) + return "Auto (4)"; + else if(val.equals("2 2")) + return "3000 Kelvin"; + else if(val.equals("2 3")) + return "3700 Kelvin"; + else if(val.equals("2 4")) + return "4000 Kelvin"; + else if(val.equals("2 5")) + return "4500 Kelvin"; + else if(val.equals("2 6")) + return "5500 Kelvin"; + else if(val.equals("2 7")) + return "6500 Kelvin"; + else if(val.equals("2 8")) + return "7500 Kelvin"; + else if(val.equals("3 0")) + return "One-touch"; + else + return "Unknown " + val; + } + + @Nullable + public String getRedBalanceDescription() + { + int[] values = _directory.getIntArray(TAG_RED_BALANCE); + if (values == null) + return null; + + short value = (short)values[0]; + + return String.valueOf((double)value/256d); + } + + @Nullable + public String getBlueBalanceDescription() + { + int[] values = _directory.getIntArray(TAG_BLUE_BALANCE); + if (values == null) + return null; + + short value = (short)values[0]; + + return String.valueOf((double)value/256d); + } + + @Nullable + public String getContrastDescription() + { + return getIndexedDescription(TAG_CONTRAST, "High", "Normal", "Low"); + } + + @Nullable + public String getPreviewImageValidDescription() + { + return getIndexedDescription(TAG_PREVIEW_IMAGE_VALID, "No", "Yes"); + } + @Nullable public String getFocusModeDescription() { @@ -643,9 +778,36 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi } @Nullable - public String getDigiZoomRatioDescription() + public String getDigitalZoomDescription() { - return getIndexedDescription(TAG_DIGI_ZOOM_RATIO, "Normal", null, "Digital 2x Zoom"); + Rational value = _directory.getRational(TAG_DIGITAL_ZOOM); + if (value == null) + return null; + return value.toSimpleString(false); + } + + @Nullable + public String getFocalPlaneDiagonalDescription() + { + Rational value = _directory.getRational(TAG_FOCAL_PLANE_DIAGONAL); + if (value == null) + return null; + + DecimalFormat format = new DecimalFormat("0.###"); + return format.format(value.doubleValue()) + " mm"; + } + + @Nullable + public String getCameraTypeDescription() + { + String cameratype = _directory.getString(TAG_CAMERA_TYPE); + if(cameratype == null) + return null; + + if(OlympusMakernoteDirectory.OlympusCameraTypes.containsKey(cameratype)) + return OlympusMakernoteDirectory.OlympusCameraTypes.get(cameratype); + + return cameratype; } @Nullable @@ -657,6 +819,38 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi return new String(bytes); } + @Nullable + public String getOneTouchWbDescription() + { + return getIndexedDescription(TAG_ONE_TOUCH_WB, "Off", "On", "On (Preset)"); + } + + @Nullable + public String getShutterSpeedDescription() + { + return super.getShutterSpeedDescription(TAG_SHUTTER_SPEED_VALUE); + } + + @Nullable + public String getIsoValueDescription() + { + Rational value = _directory.getRational(TAG_ISO_VALUE); + if (value == null) + return null; + + return String.valueOf(Math.round(Math.pow(2, value.doubleValue() - 5) * 100)); + } + + @Nullable + public String getApertureValueDescription() + { + Double aperture = _directory.getDoubleObject(TAG_APERTURE_VALUE); + if (aperture == null) + return null; + double fStop = PhotographicConversions.apertureToFStop(aperture); + return getFStopDescription(fStop); + } + @Nullable public String getMacroModeDescription() { @@ -672,7 +866,56 @@ public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDi @Nullable public String getJpegQualityDescription() { - return getIndexedDescription(TAG_JPEG_QUALITY, + String cameratype = _directory.getString(TAG_CAMERA_TYPE); + + if(cameratype != null) + { + Integer value = _directory.getInteger(TAG_JPEG_QUALITY); + if(value == null) + return null; + + if((cameratype.startsWith("SX") && !cameratype.startsWith("SX151")) + || cameratype.startsWith("D4322")) + { + switch (value) + { + case 0: + return "Standard Quality (Low)"; + case 1: + return "High Quality (Normal)"; + case 2: + return "Super High Quality (Fine)"; + case 6: + return "RAW"; + default: + return "Unknown (" + value.toString() + ")"; + } + } + else + { + switch (value) + { + case 0: + return "Standard Quality (Low)"; + case 1: + return "High Quality (Normal)"; + case 2: + return "Super High Quality (Fine)"; + case 4: + return "RAW"; + case 5: + return "Medium-Fine"; + case 6: + return "Small-Fine"; + case 33: + return "Uncompressed"; + default: + return "Unknown (" + value.toString() + ")"; + } + } + } + else + return getIndexedDescription(TAG_JPEG_QUALITY, 1, "Standard Quality", "High Quality", diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java index dbfa316..2856a66 100644 --- a/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/OlympusMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class OlympusMakernoteDirectory extends Directory { /** Used by Konica / Minolta cameras. */ @@ -50,6 +51,8 @@ public class OlympusMakernoteDirectory extends Directory /** Length of thumbnail in bytes. Used by Konica / Minolta cameras. */ public static final int TAG_MINOLTA_THUMBNAIL_LENGTH = 0x0089; + public static final int TAG_THUMBNAIL_IMAGE = 0x0100; + /** * Used by Konica / Minolta cameras * 0 = Natural Colour @@ -82,6 +85,7 @@ public class OlympusMakernoteDirectory extends Directory */ public static final int TAG_IMAGE_QUALITY_2 = 0x0103; + public static final int TAG_BODY_FIRMWARE_VERSION = 0x0104; /** * Three values: @@ -113,10 +117,10 @@ public class OlympusMakernoteDirectory extends Directory public static final int TAG_BW_MODE = 0x0203; /** Zoom Factor (0 or 1 = normal) */ - public static final int TAG_DIGI_ZOOM_RATIO = 0x0204; + public static final int TAG_DIGITAL_ZOOM = 0x0204; public static final int TAG_FOCAL_PLANE_DIAGONAL = 0x0205; public static final int TAG_LENS_DISTORTION_PARAMETERS = 0x0206; - public static final int TAG_FIRMWARE_VERSION = 0x0207; + public static final int TAG_CAMERA_TYPE = 0x0207; public static final int TAG_PICT_INFO = 0x0208; public static final int TAG_CAMERA_ID = 0x0209; @@ -135,41 +139,99 @@ public class OlympusMakernoteDirectory extends Directory /** A string. Used by Epson cameras. */ public static final int TAG_ORIGINAL_MANUFACTURER_MODEL = 0x020D; + public static final int TAG_PREVIEW_IMAGE = 0x0280; + public static final int TAG_PRE_CAPTURE_FRAMES = 0x0300; + public static final int TAG_WHITE_BOARD = 0x0301; + public static final int TAG_ONE_TOUCH_WB = 0x0302; + public static final int TAG_WHITE_BALANCE_BRACKET = 0x0303; + public static final int TAG_WHITE_BALANCE_BIAS = 0x0304; + public static final int TAG_SCENE_MODE = 0x0403; + public static final int TAG_SERIAL_NUMBER_1 = 0x0404; + public static final int TAG_FIRMWARE = 0x0405; + /** * See the PIM specification here: * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html */ public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00; - public static final int TAG_DATA_DUMP = 0x0F00; + public static final int TAG_DATA_DUMP_1 = 0x0F00; + public static final int TAG_DATA_DUMP_2 = 0x0F01; public static final int TAG_SHUTTER_SPEED_VALUE = 0x1000; public static final int TAG_ISO_VALUE = 0x1001; public static final int TAG_APERTURE_VALUE = 0x1002; public static final int TAG_BRIGHTNESS_VALUE = 0x1003; public static final int TAG_FLASH_MODE = 0x1004; + public static final int TAG_FLASH_DEVICE = 0x1005; public static final int TAG_BRACKET = 0x1006; + public static final int TAG_SENSOR_TEMPERATURE = 0x1007; + public static final int TAG_LENS_TEMPERATURE = 0x1008; + public static final int TAG_LIGHT_CONDITION = 0x1009; public static final int TAG_FOCUS_RANGE = 0x100A; public static final int TAG_FOCUS_MODE = 0x100B; public static final int TAG_FOCUS_DISTANCE = 0x100C; public static final int TAG_ZOOM = 0x100D; public static final int TAG_MACRO_FOCUS = 0x100E; public static final int TAG_SHARPNESS = 0x100F; + public static final int TAG_FLASH_CHARGE_LEVEL = 0x1010; public static final int TAG_COLOUR_MATRIX = 0x1011; public static final int TAG_BLACK_LEVEL = 0x1012; - public static final int TAG_WHITE_BALANCE = 0x1015; - public static final int TAG_RED_BIAS = 0x1017; - public static final int TAG_BLUE_BIAS = 0x1018; - public static final int TAG_SERIAL_NUMBER = 0x101A; + public static final int TAG_COLOR_TEMPERATURE_BG = 0x1013; + public static final int TAG_COLOR_TEMPERATURE_RG = 0x1014; + public static final int TAG_WB_MODE = 0x1015; +// public static final int TAG_ = 0x1016; + public static final int TAG_RED_BALANCE = 0x1017; + public static final int TAG_BLUE_BALANCE = 0x1018; + public static final int TAG_COLOR_MATRIX_NUMBER = 0x1019; + public static final int TAG_SERIAL_NUMBER_2 = 0x101A; + + public static final int TAG_EXTERNAL_FLASH_AE1_0 = 0x101B; + public static final int TAG_EXTERNAL_FLASH_AE2_0 = 0x101C; + public static final int TAG_INTERNAL_FLASH_AE1_0 = 0x101D; + public static final int TAG_INTERNAL_FLASH_AE2_0 = 0x101E; + public static final int TAG_EXTERNAL_FLASH_AE1 = 0x101F; + public static final int TAG_EXTERNAL_FLASH_AE2 = 0x1020; + public static final int TAG_INTERNAL_FLASH_AE1 = 0x1021; + public static final int TAG_INTERNAL_FLASH_AE2 = 0x1022; + public static final int TAG_FLASH_BIAS = 0x1023; + public static final int TAG_INTERNAL_FLASH_TABLE = 0x1024; + public static final int TAG_EXTERNAL_FLASH_G_VALUE = 0x1025; + public static final int TAG_EXTERNAL_FLASH_BOUNCE = 0x1026; + public static final int TAG_EXTERNAL_FLASH_ZOOM = 0x1027; + public static final int TAG_EXTERNAL_FLASH_MODE = 0x1028; public static final int TAG_CONTRAST = 0x1029; public static final int TAG_SHARPNESS_FACTOR = 0x102A; public static final int TAG_COLOUR_CONTROL = 0x102B; public static final int TAG_VALID_BITS = 0x102C; public static final int TAG_CORING_FILTER = 0x102D; - public static final int TAG_FINAL_WIDTH = 0x102E; - public static final int TAG_FINAL_HEIGHT = 0x102F; + public static final int TAG_OLYMPUS_IMAGE_WIDTH = 0x102E; + public static final int TAG_OLYMPUS_IMAGE_HEIGHT = 0x102F; + public static final int TAG_SCENE_DETECT = 0x1030; + public static final int TAG_SCENE_AREA = 0x1031; +// public static final int TAG_ = 0x1032; + public static final int TAG_SCENE_DETECT_DATA = 0x1033; public static final int TAG_COMPRESSION_RATIO = 0x1034; + public static final int TAG_PREVIEW_IMAGE_VALID = 0x1035; + public static final int TAG_PREVIEW_IMAGE_START = 0x1036; + public static final int TAG_PREVIEW_IMAGE_LENGTH = 0x1037; + public static final int TAG_AF_RESULT = 0x1038; + public static final int TAG_CCD_SCAN_MODE = 0x1039; + public static final int TAG_NOISE_REDUCTION = 0x103A; + public static final int TAG_INFINITY_LENS_STEP = 0x103B; + public static final int TAG_NEAR_LENS_STEP = 0x103C; + public static final int TAG_LIGHT_VALUE_CENTER = 0x103D; + public static final int TAG_LIGHT_VALUE_PERIPHERY = 0x103E; + public static final int TAG_FIELD_COUNT = 0x103F; + public static final int TAG_EQUIPMENT = 0x2010; + public static final int TAG_CAMERA_SETTINGS = 0x2020; + public static final int TAG_RAW_DEVELOPMENT = 0x2030; + public static final int TAG_RAW_DEVELOPMENT_2 = 0x2031; + public static final int TAG_IMAGE_PROCESSING = 0x2040; + public static final int TAG_FOCUS_INFO = 0x2050; + public static final int TAG_RAW_INFO = 0x3000; + public static final int TAG_MAIN_INFO = 0x4000; public final static class CameraSettings { @@ -190,7 +252,7 @@ public class OlympusMakernoteDirectory extends Directory public static final int TAG_DIGITAL_ZOOM = OFFSET + 13; public static final int TAG_EXPOSURE_COMPENSATION = OFFSET + 14; public static final int TAG_BRACKET_STEP = OFFSET + 15; - + // 16 missing public static final int TAG_INTERVAL_LENGTH = OFFSET + 17; public static final int TAG_INTERVAL_NUMBER = OFFSET + 18; public static final int TAG_FOCAL_LENGTH = OFFSET + 19; @@ -199,7 +261,7 @@ public class OlympusMakernoteDirectory extends Directory public static final int TAG_DATE = OFFSET + 22; public static final int TAG_TIME = OFFSET + 23; public static final int TAG_MAX_APERTURE_AT_FOCAL_LENGTH = OFFSET + 24; - + // 25, 26 missing public static final int TAG_FILE_NUMBER_MEMORY = OFFSET + 27; public static final int TAG_LAST_FILE_NUMBER = OFFSET + 28; public static final int TAG_WHITE_BALANCE_RED = OFFSET + 29; @@ -231,17 +293,6 @@ public class OlympusMakernoteDirectory extends Directory protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); static { - _tagNameMap.put(TAG_SPECIAL_MODE, "Special Mode"); - _tagNameMap.put(TAG_JPEG_QUALITY, "JPEG Quality"); - _tagNameMap.put(TAG_MACRO_MODE, "Macro"); - _tagNameMap.put(TAG_BW_MODE, "BW Mode"); - _tagNameMap.put(TAG_DIGI_ZOOM_RATIO, "DigiZoom Ratio"); - _tagNameMap.put(TAG_FOCAL_PLANE_DIAGONAL, "Focal Plane Diagonal"); - _tagNameMap.put(TAG_LENS_DISTORTION_PARAMETERS, "Lens Distortion Parameters"); - _tagNameMap.put(TAG_FIRMWARE_VERSION, "Firmware Version"); - _tagNameMap.put(TAG_PICT_INFO, "Pict Info"); - _tagNameMap.put(TAG_CAMERA_ID, "Camera Id"); - _tagNameMap.put(TAG_DATA_DUMP, "Data Dump"); _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version"); _tagNameMap.put(TAG_CAMERA_SETTINGS_1, "Camera Settings"); _tagNameMap.put(TAG_CAMERA_SETTINGS_2, "Camera Settings"); @@ -249,41 +300,106 @@ public class OlympusMakernoteDirectory extends Directory _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_OFFSET_1, "Thumbnail Offset"); _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_OFFSET_2, "Thumbnail Offset"); _tagNameMap.put(TAG_MINOLTA_THUMBNAIL_LENGTH, "Thumbnail Length"); + _tagNameMap.put(TAG_THUMBNAIL_IMAGE, "Thumbnail Image"); _tagNameMap.put(TAG_COLOUR_MODE, "Colour Mode"); _tagNameMap.put(TAG_IMAGE_QUALITY_1, "Image Quality"); _tagNameMap.put(TAG_IMAGE_QUALITY_2, "Image Quality"); - _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height"); + _tagNameMap.put(TAG_BODY_FIRMWARE_VERSION, "Body Firmware Version"); + _tagNameMap.put(TAG_SPECIAL_MODE, "Special Mode"); + _tagNameMap.put(TAG_JPEG_QUALITY, "JPEG Quality"); + _tagNameMap.put(TAG_MACRO_MODE, "Macro"); + _tagNameMap.put(TAG_BW_MODE, "BW Mode"); + _tagNameMap.put(TAG_DIGITAL_ZOOM, "Digital Zoom"); + _tagNameMap.put(TAG_FOCAL_PLANE_DIAGONAL, "Focal Plane Diagonal"); + _tagNameMap.put(TAG_LENS_DISTORTION_PARAMETERS, "Lens Distortion Parameters"); + _tagNameMap.put(TAG_CAMERA_TYPE, "Camera Type"); + _tagNameMap.put(TAG_PICT_INFO, "Pict Info"); + _tagNameMap.put(TAG_CAMERA_ID, "Camera Id"); _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width"); + _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height"); _tagNameMap.put(TAG_ORIGINAL_MANUFACTURER_MODEL, "Original Manufacturer Model"); + _tagNameMap.put(TAG_PREVIEW_IMAGE, "Preview Image"); + _tagNameMap.put(TAG_PRE_CAPTURE_FRAMES, "Pre Capture Frames"); + _tagNameMap.put(TAG_WHITE_BOARD, "White Board"); + _tagNameMap.put(TAG_ONE_TOUCH_WB, "One Touch WB"); + _tagNameMap.put(TAG_WHITE_BALANCE_BRACKET, "White Balance Bracket"); + _tagNameMap.put(TAG_WHITE_BALANCE_BIAS, "White Balance Bias"); + _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode"); + _tagNameMap.put(TAG_SERIAL_NUMBER_1, "Serial Number"); + _tagNameMap.put(TAG_FIRMWARE, "Firmware"); _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info"); - + _tagNameMap.put(TAG_DATA_DUMP_1, "Data Dump"); + _tagNameMap.put(TAG_DATA_DUMP_2, "Data Dump 2"); _tagNameMap.put(TAG_SHUTTER_SPEED_VALUE, "Shutter Speed Value"); _tagNameMap.put(TAG_ISO_VALUE, "ISO Value"); _tagNameMap.put(TAG_APERTURE_VALUE, "Aperture Value"); _tagNameMap.put(TAG_BRIGHTNESS_VALUE, "Brightness Value"); _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode"); + _tagNameMap.put(TAG_FLASH_DEVICE, "Flash Device"); _tagNameMap.put(TAG_BRACKET, "Bracket"); + _tagNameMap.put(TAG_SENSOR_TEMPERATURE, "Sensor Temperature"); + _tagNameMap.put(TAG_LENS_TEMPERATURE, "Lens Temperature"); + _tagNameMap.put(TAG_LIGHT_CONDITION, "Light Condition"); _tagNameMap.put(TAG_FOCUS_RANGE, "Focus Range"); _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode"); _tagNameMap.put(TAG_FOCUS_DISTANCE, "Focus Distance"); _tagNameMap.put(TAG_ZOOM, "Zoom"); _tagNameMap.put(TAG_MACRO_FOCUS, "Macro Focus"); _tagNameMap.put(TAG_SHARPNESS, "Sharpness"); + _tagNameMap.put(TAG_FLASH_CHARGE_LEVEL, "Flash Charge Level"); _tagNameMap.put(TAG_COLOUR_MATRIX, "Colour Matrix"); _tagNameMap.put(TAG_BLACK_LEVEL, "Black Level"); - _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance"); - _tagNameMap.put(TAG_RED_BIAS, "Red Bias"); - _tagNameMap.put(TAG_BLUE_BIAS, "Blue Bias"); - _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number"); + _tagNameMap.put(TAG_COLOR_TEMPERATURE_BG, "Color Temperature BG"); + _tagNameMap.put(TAG_COLOR_TEMPERATURE_RG, "Color Temperature RG"); + _tagNameMap.put(TAG_WB_MODE, "White Balance Mode"); + _tagNameMap.put(TAG_RED_BALANCE, "Red Balance"); + _tagNameMap.put(TAG_BLUE_BALANCE, "Blue Balance"); + _tagNameMap.put(TAG_COLOR_MATRIX_NUMBER, "Color Matrix Number"); + _tagNameMap.put(TAG_SERIAL_NUMBER_2, "Serial Number"); + _tagNameMap.put(TAG_EXTERNAL_FLASH_AE1_0, "External Flash AE1 0"); + _tagNameMap.put(TAG_EXTERNAL_FLASH_AE2_0, "External Flash AE2 0"); + _tagNameMap.put(TAG_INTERNAL_FLASH_AE1_0, "Internal Flash AE1 0"); + _tagNameMap.put(TAG_INTERNAL_FLASH_AE2_0, "Internal Flash AE2 0"); + _tagNameMap.put(TAG_EXTERNAL_FLASH_AE1, "External Flash AE1"); + _tagNameMap.put(TAG_EXTERNAL_FLASH_AE2, "External Flash AE2"); + _tagNameMap.put(TAG_INTERNAL_FLASH_AE1, "Internal Flash AE1"); + _tagNameMap.put(TAG_INTERNAL_FLASH_AE2, "Internal Flash AE2"); _tagNameMap.put(TAG_FLASH_BIAS, "Flash Bias"); + _tagNameMap.put(TAG_INTERNAL_FLASH_TABLE, "Internal Flash Table"); + _tagNameMap.put(TAG_EXTERNAL_FLASH_G_VALUE, "External Flash G Value"); + _tagNameMap.put(TAG_EXTERNAL_FLASH_BOUNCE, "External Flash Bounce"); + _tagNameMap.put(TAG_EXTERNAL_FLASH_ZOOM, "External Flash Zoom"); + _tagNameMap.put(TAG_EXTERNAL_FLASH_MODE, "External Flash Mode"); _tagNameMap.put(TAG_CONTRAST, "Contrast"); _tagNameMap.put(TAG_SHARPNESS_FACTOR, "Sharpness Factor"); _tagNameMap.put(TAG_COLOUR_CONTROL, "Colour Control"); _tagNameMap.put(TAG_VALID_BITS, "Valid Bits"); _tagNameMap.put(TAG_CORING_FILTER, "Coring Filter"); - _tagNameMap.put(TAG_FINAL_WIDTH, "Final Width"); - _tagNameMap.put(TAG_FINAL_HEIGHT, "Final Height"); + _tagNameMap.put(TAG_OLYMPUS_IMAGE_WIDTH, "Olympus Image Width"); + _tagNameMap.put(TAG_OLYMPUS_IMAGE_HEIGHT, "Olympus Image Height"); + _tagNameMap.put(TAG_SCENE_DETECT, "Scene Detect"); + _tagNameMap.put(TAG_SCENE_AREA, "Scene Area"); + _tagNameMap.put(TAG_SCENE_DETECT_DATA, "Scene Detect Data"); _tagNameMap.put(TAG_COMPRESSION_RATIO, "Compression Ratio"); + _tagNameMap.put(TAG_PREVIEW_IMAGE_VALID, "Preview Image Valid"); + _tagNameMap.put(TAG_PREVIEW_IMAGE_START, "Preview Image Start"); + _tagNameMap.put(TAG_PREVIEW_IMAGE_LENGTH, "Preview Image Length"); + _tagNameMap.put(TAG_AF_RESULT, "AF Result"); + _tagNameMap.put(TAG_CCD_SCAN_MODE, "CCD Scan Mode"); + _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction"); + _tagNameMap.put(TAG_INFINITY_LENS_STEP, "Infinity Lens Step"); + _tagNameMap.put(TAG_NEAR_LENS_STEP, "Near Lens Step"); + _tagNameMap.put(TAG_LIGHT_VALUE_CENTER, "Light Value Center"); + _tagNameMap.put(TAG_LIGHT_VALUE_PERIPHERY, "Light Value Periphery"); + _tagNameMap.put(TAG_FIELD_COUNT, "Field Count"); + _tagNameMap.put(TAG_EQUIPMENT, "Equipment"); + _tagNameMap.put(TAG_CAMERA_SETTINGS, "Camera Settings"); + _tagNameMap.put(TAG_RAW_DEVELOPMENT, "Raw Development"); + _tagNameMap.put(TAG_RAW_DEVELOPMENT_2, "Raw Development 2"); + _tagNameMap.put(TAG_IMAGE_PROCESSING, "Image Processing"); + _tagNameMap.put(TAG_FOCUS_INFO, "Focus Info"); + _tagNameMap.put(TAG_RAW_INFO, "Raw Info"); + _tagNameMap.put(TAG_MAIN_INFO, "Main Info"); _tagNameMap.put(CameraSettings.TAG_EXPOSURE_MODE, "Exposure Mode"); _tagNameMap.put(CameraSettings.TAG_FLASH_MODE, "Flash Mode"); @@ -388,4 +504,334 @@ public class OlympusMakernoteDirectory extends Directory { return _tagNameMap; } + + // <summary> + // These values are currently decoded only for Olympus models. Models with + // Olympus-style maker notes from other brands such as Acer, BenQ, Hitachi, HP, + // Premier, Konica-Minolta, Maginon, Ricoh, Rollei, SeaLife, Sony, Supra, + // Vivitar are not listed. + // </summary> + // <remarks> + // Converted from Exiftool version 10.33 created by Phil Harvey + // http://www.sno.phy.queensu.ca/~phil/exiftool/ + // lib\Image\ExifTool\Olympus.pm + // </remarks> + public static final HashMap<String, String> OlympusCameraTypes = new HashMap<String, String>(); + + static { + OlympusCameraTypes.put("D4028", "X-2,C-50Z"); + OlympusCameraTypes.put("D4029", "E-20,E-20N,E-20P"); + OlympusCameraTypes.put("D4034", "C720UZ"); + OlympusCameraTypes.put("D4040", "E-1"); + OlympusCameraTypes.put("D4041", "E-300"); + OlympusCameraTypes.put("D4083", "C2Z,D520Z,C220Z"); + OlympusCameraTypes.put("D4106", "u20D,S400D,u400D"); + OlympusCameraTypes.put("D4120", "X-1"); + OlympusCameraTypes.put("D4122", "u10D,S300D,u300D"); + OlympusCameraTypes.put("D4125", "AZ-1"); + OlympusCameraTypes.put("D4141", "C150,D390"); + OlympusCameraTypes.put("D4193", "C-5000Z"); + OlympusCameraTypes.put("D4194", "X-3,C-60Z"); + OlympusCameraTypes.put("D4199", "u30D,S410D,u410D"); + OlympusCameraTypes.put("D4205", "X450,D535Z,C370Z"); + OlympusCameraTypes.put("D4210", "C160,D395"); + OlympusCameraTypes.put("D4211", "C725UZ"); + OlympusCameraTypes.put("D4213", "FerrariMODEL2003"); + OlympusCameraTypes.put("D4216", "u15D"); + OlympusCameraTypes.put("D4217", "u25D"); + OlympusCameraTypes.put("D4220", "u-miniD,Stylus V"); + OlympusCameraTypes.put("D4221", "u40D,S500,uD500"); + OlympusCameraTypes.put("D4231", "FerrariMODEL2004"); + OlympusCameraTypes.put("D4240", "X500,D590Z,C470Z"); + OlympusCameraTypes.put("D4244", "uD800,S800"); + OlympusCameraTypes.put("D4256", "u720SW,S720SW"); + OlympusCameraTypes.put("D4261", "X600,D630,FE5500"); + OlympusCameraTypes.put("D4262", "uD600,S600"); + OlympusCameraTypes.put("D4301", "u810/S810"); // (yes, "/". Olympus is not consistent in the notation) + OlympusCameraTypes.put("D4302", "u710,S710"); + OlympusCameraTypes.put("D4303", "u700,S700"); + OlympusCameraTypes.put("D4304", "FE100,X710"); + OlympusCameraTypes.put("D4305", "FE110,X705"); + OlympusCameraTypes.put("D4310", "FE-130,X-720"); + OlympusCameraTypes.put("D4311", "FE-140,X-725"); + OlympusCameraTypes.put("D4312", "FE150,X730"); + OlympusCameraTypes.put("D4313", "FE160,X735"); + OlympusCameraTypes.put("D4314", "u740,S740"); + OlympusCameraTypes.put("D4315", "u750,S750"); + OlympusCameraTypes.put("D4316", "u730/S730"); + OlympusCameraTypes.put("D4317", "FE115,X715"); + OlympusCameraTypes.put("D4321", "SP550UZ"); + OlympusCameraTypes.put("D4322", "SP510UZ"); + OlympusCameraTypes.put("D4324", "FE170,X760"); + OlympusCameraTypes.put("D4326", "FE200"); + OlympusCameraTypes.put("D4327", "FE190/X750"); // (also SX876) + OlympusCameraTypes.put("D4328", "u760,S760"); + OlympusCameraTypes.put("D4330", "FE180/X745"); // (also SX875) + OlympusCameraTypes.put("D4331", "u1000/S1000"); + OlympusCameraTypes.put("D4332", "u770SW,S770SW"); + OlympusCameraTypes.put("D4333", "FE240/X795"); + OlympusCameraTypes.put("D4334", "FE210,X775"); + OlympusCameraTypes.put("D4336", "FE230/X790"); + OlympusCameraTypes.put("D4337", "FE220,X785"); + OlympusCameraTypes.put("D4338", "u725SW,S725SW"); + OlympusCameraTypes.put("D4339", "FE250/X800"); + OlympusCameraTypes.put("D4341", "u780,S780"); + OlympusCameraTypes.put("D4343", "u790SW,S790SW"); + OlympusCameraTypes.put("D4344", "u1020,S1020"); + OlympusCameraTypes.put("D4346", "FE15,X10"); + OlympusCameraTypes.put("D4348", "FE280,X820,C520"); + OlympusCameraTypes.put("D4349", "FE300,X830"); + OlympusCameraTypes.put("D4350", "u820,S820"); + OlympusCameraTypes.put("D4351", "u1200,S1200"); + OlympusCameraTypes.put("D4352", "FE270,X815,C510"); + OlympusCameraTypes.put("D4353", "u795SW,S795SW"); + OlympusCameraTypes.put("D4354", "u1030SW,S1030SW"); + OlympusCameraTypes.put("D4355", "SP560UZ"); + OlympusCameraTypes.put("D4356", "u1010,S1010"); + OlympusCameraTypes.put("D4357", "u830,S830"); + OlympusCameraTypes.put("D4359", "u840,S840"); + OlympusCameraTypes.put("D4360", "FE350WIDE,X865"); + OlympusCameraTypes.put("D4361", "u850SW,S850SW"); + OlympusCameraTypes.put("D4362", "FE340,X855,C560"); + OlympusCameraTypes.put("D4363", "FE320,X835,C540"); + OlympusCameraTypes.put("D4364", "SP570UZ"); + OlympusCameraTypes.put("D4366", "FE330,X845,C550"); + OlympusCameraTypes.put("D4368", "FE310,X840,C530"); + OlympusCameraTypes.put("D4370", "u1050SW,S1050SW"); + OlympusCameraTypes.put("D4371", "u1060,S1060"); + OlympusCameraTypes.put("D4372", "FE370,X880,C575"); + OlympusCameraTypes.put("D4374", "SP565UZ"); + OlympusCameraTypes.put("D4377", "u1040,S1040"); + OlympusCameraTypes.put("D4378", "FE360,X875,C570"); + OlympusCameraTypes.put("D4379", "FE20,X15,C25"); + OlympusCameraTypes.put("D4380", "uT6000,ST6000"); + OlympusCameraTypes.put("D4381", "uT8000,ST8000"); + OlympusCameraTypes.put("D4382", "u9000,S9000"); + OlympusCameraTypes.put("D4384", "SP590UZ"); + OlympusCameraTypes.put("D4385", "FE3010,X895"); + OlympusCameraTypes.put("D4386", "FE3000,X890"); + OlympusCameraTypes.put("D4387", "FE35,X30"); + OlympusCameraTypes.put("D4388", "u550WP,S550WP"); + OlympusCameraTypes.put("D4390", "FE5000,X905"); + OlympusCameraTypes.put("D4391", "u5000"); + OlympusCameraTypes.put("D4392", "u7000,S7000"); + OlympusCameraTypes.put("D4396", "FE5010,X915"); + OlympusCameraTypes.put("D4397", "FE25,X20"); + OlympusCameraTypes.put("D4398", "FE45,X40"); + OlympusCameraTypes.put("D4401", "XZ-1"); + OlympusCameraTypes.put("D4402", "uT6010,ST6010"); + OlympusCameraTypes.put("D4406", "u7010,S7010 / u7020,S7020"); + OlympusCameraTypes.put("D4407", "FE4010,X930"); + OlympusCameraTypes.put("D4408", "X560WP"); + OlympusCameraTypes.put("D4409", "FE26,X21"); + OlympusCameraTypes.put("D4410", "FE4000,X920,X925"); + OlympusCameraTypes.put("D4411", "FE46,X41,X42"); + OlympusCameraTypes.put("D4412", "FE5020,X935"); + OlympusCameraTypes.put("D4413", "uTough-3000"); + OlympusCameraTypes.put("D4414", "StylusTough-6020"); + OlympusCameraTypes.put("D4415", "StylusTough-8010"); + OlympusCameraTypes.put("D4417", "u5010,S5010"); + OlympusCameraTypes.put("D4418", "u7040,S7040"); + OlympusCameraTypes.put("D4419", "u9010,S9010"); + OlympusCameraTypes.put("D4423", "FE4040"); + OlympusCameraTypes.put("D4424", "FE47,X43"); + OlympusCameraTypes.put("D4426", "FE4030,X950"); + OlympusCameraTypes.put("D4428", "FE5030,X965,X960"); + OlympusCameraTypes.put("D4430", "u7030,S7030"); + OlympusCameraTypes.put("D4432", "SP600UZ"); + OlympusCameraTypes.put("D4434", "SP800UZ"); + OlympusCameraTypes.put("D4439", "FE4020,X940"); + OlympusCameraTypes.put("D4442", "FE5035"); + OlympusCameraTypes.put("D4448", "FE4050,X970"); + OlympusCameraTypes.put("D4450", "FE5050,X985"); + OlympusCameraTypes.put("D4454", "u-7050"); + OlympusCameraTypes.put("D4464", "T10,X27"); + OlympusCameraTypes.put("D4470", "FE5040,X980"); + OlympusCameraTypes.put("D4472", "TG-310"); + OlympusCameraTypes.put("D4474", "TG-610"); + OlympusCameraTypes.put("D4476", "TG-810"); + OlympusCameraTypes.put("D4478", "VG145,VG140,D715"); + OlympusCameraTypes.put("D4479", "VG130,D710"); + OlympusCameraTypes.put("D4480", "VG120,D705"); + OlympusCameraTypes.put("D4482", "VR310,D720"); + OlympusCameraTypes.put("D4484", "VR320,D725"); + OlympusCameraTypes.put("D4486", "VR330,D730"); + OlympusCameraTypes.put("D4488", "VG110,D700"); + OlympusCameraTypes.put("D4490", "SP-610UZ"); + OlympusCameraTypes.put("D4492", "SZ-10"); + OlympusCameraTypes.put("D4494", "SZ-20"); + OlympusCameraTypes.put("D4496", "SZ-30MR"); + OlympusCameraTypes.put("D4498", "SP-810UZ"); + OlympusCameraTypes.put("D4500", "SZ-11"); + OlympusCameraTypes.put("D4504", "TG-615"); + OlympusCameraTypes.put("D4508", "TG-620"); + OlympusCameraTypes.put("D4510", "TG-820"); + OlympusCameraTypes.put("D4512", "TG-1"); + OlympusCameraTypes.put("D4516", "SH-21"); + OlympusCameraTypes.put("D4519", "SZ-14"); + OlympusCameraTypes.put("D4520", "SZ-31MR"); + OlympusCameraTypes.put("D4521", "SH-25MR"); + OlympusCameraTypes.put("D4523", "SP-720UZ"); + OlympusCameraTypes.put("D4529", "VG170"); + OlympusCameraTypes.put("D4531", "XZ-2"); + OlympusCameraTypes.put("D4535", "SP-620UZ"); + OlympusCameraTypes.put("D4536", "TG-320"); + OlympusCameraTypes.put("D4537", "VR340,D750"); + OlympusCameraTypes.put("D4538", "VG160,X990,D745"); + OlympusCameraTypes.put("D4541", "SZ-12"); + OlympusCameraTypes.put("D4545", "VH410"); + OlympusCameraTypes.put("D4546", "XZ-10"); //IB + OlympusCameraTypes.put("D4547", "TG-2"); + OlympusCameraTypes.put("D4548", "TG-830"); + OlympusCameraTypes.put("D4549", "TG-630"); + OlympusCameraTypes.put("D4550", "SH-50"); + OlympusCameraTypes.put("D4553", "SZ-16,DZ-105"); + OlympusCameraTypes.put("D4562", "SP-820UZ"); + OlympusCameraTypes.put("D4566", "SZ-15"); + OlympusCameraTypes.put("D4572", "STYLUS1"); + OlympusCameraTypes.put("D4574", "TG-3"); + OlympusCameraTypes.put("D4575", "TG-850"); + OlympusCameraTypes.put("D4579", "SP-100EE"); + OlympusCameraTypes.put("D4580", "SH-60"); + OlympusCameraTypes.put("D4581", "SH-1"); + OlympusCameraTypes.put("D4582", "TG-835"); + OlympusCameraTypes.put("D4585", "SH-2 / SH-3"); + OlympusCameraTypes.put("D4586", "TG-4"); + OlympusCameraTypes.put("D4587", "TG-860"); + OlympusCameraTypes.put("D4591", "TG-870"); + OlympusCameraTypes.put("D4809", "C2500L"); + OlympusCameraTypes.put("D4842", "E-10"); + OlympusCameraTypes.put("D4856", "C-1"); + OlympusCameraTypes.put("D4857", "C-1Z,D-150Z"); + OlympusCameraTypes.put("DCHC", "D500L"); + OlympusCameraTypes.put("DCHT", "D600L / D620L"); + OlympusCameraTypes.put("K0055", "AIR-A01"); + OlympusCameraTypes.put("S0003", "E-330"); + OlympusCameraTypes.put("S0004", "E-500"); + OlympusCameraTypes.put("S0009", "E-400"); + OlympusCameraTypes.put("S0010", "E-510"); + OlympusCameraTypes.put("S0011", "E-3"); + OlympusCameraTypes.put("S0013", "E-410"); + OlympusCameraTypes.put("S0016", "E-420"); + OlympusCameraTypes.put("S0017", "E-30"); + OlympusCameraTypes.put("S0018", "E-520"); + OlympusCameraTypes.put("S0019", "E-P1"); + OlympusCameraTypes.put("S0023", "E-620"); + OlympusCameraTypes.put("S0026", "E-P2"); + OlympusCameraTypes.put("S0027", "E-PL1"); + OlympusCameraTypes.put("S0029", "E-450"); + OlympusCameraTypes.put("S0030", "E-600"); + OlympusCameraTypes.put("S0032", "E-P3"); + OlympusCameraTypes.put("S0033", "E-5"); + OlympusCameraTypes.put("S0034", "E-PL2"); + OlympusCameraTypes.put("S0036", "E-M5"); + OlympusCameraTypes.put("S0038", "E-PL3"); + OlympusCameraTypes.put("S0039", "E-PM1"); + OlympusCameraTypes.put("S0040", "E-PL1s"); + OlympusCameraTypes.put("S0042", "E-PL5"); + OlympusCameraTypes.put("S0043", "E-PM2"); + OlympusCameraTypes.put("S0044", "E-P5"); + OlympusCameraTypes.put("S0045", "E-PL6"); + OlympusCameraTypes.put("S0046", "E-PL7"); //IB + OlympusCameraTypes.put("S0047", "E-M1"); + OlympusCameraTypes.put("S0051", "E-M10"); + OlympusCameraTypes.put("S0052", "E-M5MarkII"); //IB + OlympusCameraTypes.put("S0059", "E-M10MarkII"); + OlympusCameraTypes.put("S0061", "PEN-F"); //forum7005 + OlympusCameraTypes.put("S0065", "E-PL8"); + OlympusCameraTypes.put("S0067", "E-M1MarkII"); + OlympusCameraTypes.put("SR45", "D220"); + OlympusCameraTypes.put("SR55", "D320L"); + OlympusCameraTypes.put("SR83", "D340L"); + OlympusCameraTypes.put("SR85", "C830L,D340R"); + OlympusCameraTypes.put("SR852", "C860L,D360L"); + OlympusCameraTypes.put("SR872", "C900Z,D400Z"); + OlympusCameraTypes.put("SR874", "C960Z,D460Z"); + OlympusCameraTypes.put("SR951", "C2000Z"); + OlympusCameraTypes.put("SR952", "C21"); + OlympusCameraTypes.put("SR953", "C21T.commu"); + OlympusCameraTypes.put("SR954", "C2020Z"); + OlympusCameraTypes.put("SR955", "C990Z,D490Z"); + OlympusCameraTypes.put("SR956", "C211Z"); + OlympusCameraTypes.put("SR959", "C990ZS,D490Z"); + OlympusCameraTypes.put("SR95A", "C2100UZ"); + OlympusCameraTypes.put("SR971", "C100,D370"); + OlympusCameraTypes.put("SR973", "C2,D230"); + OlympusCameraTypes.put("SX151", "E100RS"); + OlympusCameraTypes.put("SX351", "C3000Z / C3030Z"); + OlympusCameraTypes.put("SX354", "C3040Z"); + OlympusCameraTypes.put("SX355", "C2040Z"); + OlympusCameraTypes.put("SX357", "C700UZ"); + OlympusCameraTypes.put("SX358", "C200Z,D510Z"); + OlympusCameraTypes.put("SX374", "C3100Z,C3020Z"); + OlympusCameraTypes.put("SX552", "C4040Z"); + OlympusCameraTypes.put("SX553", "C40Z,D40Z"); + OlympusCameraTypes.put("SX556", "C730UZ"); + OlympusCameraTypes.put("SX558", "C5050Z"); + OlympusCameraTypes.put("SX571", "C120,D380"); + OlympusCameraTypes.put("SX574", "C300Z,D550Z"); + OlympusCameraTypes.put("SX575", "C4100Z,C4000Z"); + OlympusCameraTypes.put("SX751", "X200,D560Z,C350Z"); + OlympusCameraTypes.put("SX752", "X300,D565Z,C450Z"); + OlympusCameraTypes.put("SX753", "C750UZ"); + OlympusCameraTypes.put("SX754", "C740UZ"); + OlympusCameraTypes.put("SX755", "C755UZ"); + OlympusCameraTypes.put("SX756", "C5060WZ"); + OlympusCameraTypes.put("SX757", "C8080WZ"); + OlympusCameraTypes.put("SX758", "X350,D575Z,C360Z"); + OlympusCameraTypes.put("SX759", "X400,D580Z,C460Z"); + OlympusCameraTypes.put("SX75A", "AZ-2ZOOM"); + OlympusCameraTypes.put("SX75B", "D595Z,C500Z"); + OlympusCameraTypes.put("SX75C", "X550,D545Z,C480Z"); + OlympusCameraTypes.put("SX75D", "IR-300"); + OlympusCameraTypes.put("SX75F", "C55Z,C5500Z"); + OlympusCameraTypes.put("SX75G", "C170,D425"); + OlympusCameraTypes.put("SX75J", "C180,D435"); + OlympusCameraTypes.put("SX771", "C760UZ"); + OlympusCameraTypes.put("SX772", "C770UZ"); + OlympusCameraTypes.put("SX773", "C745UZ"); + OlympusCameraTypes.put("SX774", "X250,D560Z,C350Z"); + OlympusCameraTypes.put("SX775", "X100,D540Z,C310Z"); + OlympusCameraTypes.put("SX776", "C460ZdelSol"); + OlympusCameraTypes.put("SX777", "C765UZ"); + OlympusCameraTypes.put("SX77A", "D555Z,C315Z"); + OlympusCameraTypes.put("SX851", "C7070WZ"); + OlympusCameraTypes.put("SX852", "C70Z,C7000Z"); + OlympusCameraTypes.put("SX853", "SP500UZ"); + OlympusCameraTypes.put("SX854", "SP310"); + OlympusCameraTypes.put("SX855", "SP350"); + OlympusCameraTypes.put("SX873", "SP320"); + OlympusCameraTypes.put("SX875", "FE180/X745"); // (also D4330) + OlympusCameraTypes.put("SX876", "FE190/X750"); // (also D4327) + + // other brands + // 4MP9Q3", "Camera 4MP-9Q3' + // 4MP9T2", "BenQ DC C420 / Camera 4MP-9T2' + // 5MP9Q3", "Camera 5MP-9Q3" }, + // 5MP9X9", "Camera 5MP-9X9" }, + // '5MP-9T'=> 'Camera 5MP-9T3" }, + // '5MP-9Y'=> 'Camera 5MP-9Y2" }, + // '6MP-9U'=> 'Camera 6MP-9U9" }, + // 7MP9Q3", "Camera 7MP-9Q3" }, + // '8MP-9U'=> 'Camera 8MP-9U4" }, + // CE5330", "Acer CE-5330" }, + // 'CP-853'=> 'Acer CP-8531" }, + // CS5531", "Acer CS5531" }, + // DC500 ", "SeaLife DC500" }, + // DC7370", "Camera 7MP-9GA" }, + // DC7371", "Camera 7MP-9GM" }, + // DC7371", "Hitachi HDC-751E" }, + // DC7375", "Hitachi HDC-763E / Rollei RCP-7330X / Ricoh Caplio RR770 / Vivitar ViviCam 7330" }, + // 'DC E63'=> 'BenQ DC E63+" }, + // 'DC P86'=> 'BenQ DC P860" }, + // DS5340", "Maginon Performic S5 / Premier 5MP-9M7" }, + // DS5341", "BenQ E53+ / Supra TCM X50 / Maginon X50 / Premier 5MP-9P8" }, + // DS5346", "Premier 5MP-9Q2" }, + // E500 ", "Konica Minolta DiMAGE E500" }, + // MAGINO", "Maginon X60" }, + // Mz60 ", "HP Photosmart Mz60" }, + // Q3DIGI", "Camera 5MP-9Q3" }, + // SLIMLI", "Supra Slimline X6" }, + // V8300s", "Vivitar V8300s" }, + } } diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java new file mode 100644 index 0000000..90743fa --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDescriptor.java @@ -0,0 +1,230 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import java.util.HashMap; + +import static com.drew.metadata.exif.makernotes.OlympusRawDevelopment2MakernoteDirectory.*; + +/** + * Provides human-readable String representations of tag values stored in a {@link OlympusRawDevelopment2MakernoteDirectory}. + * <p> + * Some Description functions converted from Exiftool version 10.10 created by Phil Harvey + * http://www.sno.phy.queensu.ca/~phil/exiftool/ + * lib\Image\ExifTool\Olympus.pm + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusRawDevelopment2MakernoteDescriptor extends TagDescriptor<OlympusRawDevelopment2MakernoteDirectory> +{ + public OlympusRawDevelopment2MakernoteDescriptor(@NotNull OlympusRawDevelopment2MakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagRawDevVersion: + return getRawDevVersionDescription(); + case TagRawDevExposureBiasValue: + return getRawDevExposureBiasValueDescription(); + case TagRawDevColorSpace: + return getRawDevColorSpaceDescription(); + case TagRawDevNoiseReduction: + return getRawDevNoiseReductionDescription(); + case TagRawDevEngine: + return getRawDevEngineDescription(); + case TagRawDevPictureMode: + return getRawDevPictureModeDescription(); + case TagRawDevPmBwFilter: + return getRawDevPmBwFilterDescription(); + case TagRawDevPmPictureTone: + return getRawDevPmPictureToneDescription(); + case TagRawDevArtFilter: + return getRawDevArtFilterDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getRawDevVersionDescription() + { + return getVersionBytesDescription(TagRawDevVersion, 4); + } + + @Nullable + public String getRawDevExposureBiasValueDescription() + { + return getIndexedDescription(TagRawDevExposureBiasValue, + 1, "Color Temperature", "Gray Point"); + } + + @Nullable + public String getRawDevColorSpaceDescription() + { + return getIndexedDescription(TagRawDevColorSpace, + "sRGB", "Adobe RGB", "Pro Photo RGB"); + } + + @Nullable + public String getRawDevNoiseReductionDescription() + { + Integer value = _directory.getInteger(TagRawDevNoiseReduction); + if (value == null) + return null; + + if (value == 0) + return "(none)"; + + StringBuilder sb = new StringBuilder(); + int v = value; + + if ((v & 1) != 0) sb.append("Noise Reduction, "); + if (((v >> 1) & 1) != 0) sb.append("Noise Filter, "); + if (((v >> 2) & 1) != 0) sb.append("Noise Filter (ISO Boost), "); + + return sb.substring(0, sb.length() - 2); + } + + @Nullable + public String getRawDevEngineDescription() + { + return getIndexedDescription(TagRawDevEngine, + "High Speed", "High Function", "Advanced High Speed", "Advanced High Function"); + } + + @Nullable + public String getRawDevPictureModeDescription() + { + Integer value = _directory.getInteger(TagRawDevPictureMode); + if (value == null) + return null; + + switch (value) + { + case 1: + return "Vivid"; + case 2: + return "Natural"; + case 3: + return "Muted"; + case 256: + return "Monotone"; + case 512: + return "Sepia"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getRawDevPmBwFilterDescription() + { + return getIndexedDescription(TagRawDevPmBwFilter, + "Neutral", "Yellow", "Orange", "Red", "Green"); + } + + @Nullable + public String getRawDevPmPictureToneDescription() + { + return getIndexedDescription(TagRawDevPmPictureTone, + "Neutral", "Sepia", "Blue", "Purple", "Green"); + } + + @Nullable + public String getRawDevArtFilterDescription() + { + return getFilterDescription(TagRawDevArtFilter); + } + + @Nullable + public String getFilterDescription(int tag) + { + int[] values = _directory.getIntArray(tag); + if (values == null || values.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + if (i == 0) + sb.append(_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]"); + else + sb.append(values[i]).append("; "); + sb.append("; "); + } + + return sb.substring(0, sb.length() - 2); + } + + // RawDevArtFilter values + private static final HashMap<Integer, String> _filters = new HashMap<Integer, String>(); + + static { + _filters.put(0, "Off"); + _filters.put(1, "Soft Focus"); + _filters.put(2, "Pop Art"); + _filters.put(3, "Pale & Light Color"); + _filters.put(4, "Light Tone"); + _filters.put(5, "Pin Hole"); + _filters.put(6, "Grainy Film"); + _filters.put(9, "Diorama"); + _filters.put(10, "Cross Process"); + _filters.put(12, "Fish Eye"); + _filters.put(13, "Drawing"); + _filters.put(14, "Gentle Sepia"); + _filters.put(15, "Pale & Light Color II"); + _filters.put(16, "Pop Art II"); + _filters.put(17, "Pin Hole II"); + _filters.put(18, "Pin Hole III"); + _filters.put(19, "Grainy Film II"); + _filters.put(20, "Dramatic Tone"); + _filters.put(21, "Punk"); + _filters.put(22, "Soft Focus 2"); + _filters.put(23, "Sparkle"); + _filters.put(24, "Watercolor"); + _filters.put(25, "Key Line"); + _filters.put(26, "Key Line II"); + _filters.put(27, "Miniature"); + _filters.put(28, "Reflection"); + _filters.put(29, "Fragmented"); + _filters.put(31, "Cross Process II"); + _filters.put(32, "Dramatic Tone II"); + _filters.put(33, "Watercolor I"); + _filters.put(34, "Watercolor II"); + _filters.put(35, "Diorama II"); + _filters.put(36, "Vintage"); + _filters.put(37, "Vintage II"); + _filters.put(38, "Vintage III"); + _filters.put(39, "Partial Color"); + _filters.put(40, "Partial Color II"); + _filters.put(41, "Partial Color III"); + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDirectory.java new file mode 100644 index 0000000..e25b8b1 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopment2MakernoteDirectory.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * The Olympus raw development 2 makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags + * that appear specific to those manufacturers. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusRawDevelopment2MakernoteDirectory extends Directory +{ + public static final int TagRawDevVersion = 0x0000; + public static final int TagRawDevExposureBiasValue = 0x0100; + public static final int TagRawDevWhiteBalance = 0x0101; + public static final int TagRawDevWhiteBalanceValue = 0x0102; + public static final int TagRawDevWbFineAdjustment = 0x0103; + public static final int TagRawDevGrayPoint = 0x0104; + public static final int TagRawDevContrastValue = 0x0105; + public static final int TagRawDevSharpnessValue = 0x0106; + public static final int TagRawDevSaturationEmphasis = 0x0107; + public static final int TagRawDevMemoryColorEmphasis = 0x0108; + public static final int TagRawDevColorSpace = 0x0109; + public static final int TagRawDevNoiseReduction = 0x010a; + public static final int TagRawDevEngine = 0x010b; + public static final int TagRawDevPictureMode = 0x010c; + public static final int TagRawDevPmSaturation = 0x010d; + public static final int TagRawDevPmContrast = 0x010e; + public static final int TagRawDevPmSharpness = 0x010f; + public static final int TagRawDevPmBwFilter = 0x0110; + public static final int TagRawDevPmPictureTone = 0x0111; + public static final int TagRawDevGradation = 0x0112; + public static final int TagRawDevSaturation3 = 0x0113; + public static final int TagRawDevAutoGradation = 0x0119; + public static final int TagRawDevPmNoiseFilter = 0x0120; + public static final int TagRawDevArtFilter = 0x0121; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TagRawDevVersion, "Raw Dev Version"); + _tagNameMap.put(TagRawDevExposureBiasValue, "Raw Dev Exposure Bias Value"); + _tagNameMap.put(TagRawDevWhiteBalance, "Raw Dev White Balance"); + _tagNameMap.put(TagRawDevWhiteBalanceValue, "Raw Dev White Balance Value"); + _tagNameMap.put(TagRawDevWbFineAdjustment, "Raw Dev WB Fine Adjustment"); + _tagNameMap.put(TagRawDevGrayPoint, "Raw Dev Gray Point"); + _tagNameMap.put(TagRawDevContrastValue, "Raw Dev Contrast Value"); + _tagNameMap.put(TagRawDevSharpnessValue, "Raw Dev Sharpness Value"); + _tagNameMap.put(TagRawDevSaturationEmphasis, "Raw Dev Saturation Emphasis"); + _tagNameMap.put(TagRawDevMemoryColorEmphasis, "Raw Dev Memory Color Emphasis"); + _tagNameMap.put(TagRawDevColorSpace, "Raw Dev Color Space"); + _tagNameMap.put(TagRawDevNoiseReduction, "Raw Dev Noise Reduction"); + _tagNameMap.put(TagRawDevEngine, "Raw Dev Engine"); + _tagNameMap.put(TagRawDevPictureMode, "Raw Dev Picture Mode"); + _tagNameMap.put(TagRawDevPmSaturation, "Raw Dev PM Saturation"); + _tagNameMap.put(TagRawDevPmContrast, "Raw Dev PM Contrast"); + _tagNameMap.put(TagRawDevPmSharpness, "Raw Dev PM Sharpness"); + _tagNameMap.put(TagRawDevPmBwFilter, "Raw Dev PM BW Filter"); + _tagNameMap.put(TagRawDevPmPictureTone, "Raw Dev PM Picture Tone"); + _tagNameMap.put(TagRawDevGradation, "Raw Dev Gradation"); + _tagNameMap.put(TagRawDevSaturation3, "Raw Dev Saturation 3"); + _tagNameMap.put(TagRawDevAutoGradation, "Raw Dev Auto Gradation"); + _tagNameMap.put(TagRawDevPmNoiseFilter, "Raw Dev PM Noise Filter"); + _tagNameMap.put(TagRawDevArtFilter, "Raw Dev Art Filter"); + } + + public OlympusRawDevelopment2MakernoteDirectory() + { + this.setDescriptor(new OlympusRawDevelopment2MakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Olympus Raw Development 2"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java new file mode 100644 index 0000000..d7c88c4 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDescriptor.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.makernotes.OlympusRawDevelopmentMakernoteDirectory.*; + +/** + * Provides human-readable String representations of tag values stored in a {@link OlympusRawDevelopmentMakernoteDirectory}. + * <p> + * Some Description functions converted from Exiftool version 10.10 created by Phil Harvey + * http://www.sno.phy.queensu.ca/~phil/exiftool/ + * lib\Image\ExifTool\Olympus.pm + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusRawDevelopmentMakernoteDescriptor extends TagDescriptor<OlympusRawDevelopmentMakernoteDirectory> +{ + public OlympusRawDevelopmentMakernoteDescriptor(@NotNull OlympusRawDevelopmentMakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagRawDevVersion: + return getRawDevVersionDescription(); + case TagRawDevColorSpace: + return getRawDevColorSpaceDescription(); + case TagRawDevEngine: + return getRawDevEngineDescription(); + case TagRawDevNoiseReduction: + return getRawDevNoiseReductionDescription(); + case TagRawDevEditStatus: + return getRawDevEditStatusDescription(); + case TagRawDevSettings: + return getRawDevSettingsDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getRawDevVersionDescription() + { + return getVersionBytesDescription(TagRawDevVersion, 4); + } + + @Nullable + public String getRawDevColorSpaceDescription() + { + return getIndexedDescription(TagRawDevColorSpace, + "sRGB", "Adobe RGB", "Pro Photo RGB"); + } + + @Nullable + public String getRawDevEngineDescription() + { + return getIndexedDescription(TagRawDevEngine, + "High Speed", "High Function", "Advanced High Speed", "Advanced High Function"); + } + + @Nullable + public String getRawDevNoiseReductionDescription() + { + Integer value = _directory.getInteger(TagRawDevNoiseReduction); + if (value == null) + return null; + + if (value == 0) + return "(none)"; + + StringBuilder sb = new StringBuilder(); + int v = value; + + if ((v & 1) != 0) sb.append("Noise Reduction, "); + if (((v >> 1) & 1) != 0) sb.append("Noise Filter, "); + if (((v >> 2) & 1) != 0) sb.append("Noise Filter (ISO Boost), "); + + return sb.substring(0, sb.length() - 2); + } + + @Nullable + public String getRawDevEditStatusDescription() + { + Integer value = _directory.getInteger(TagRawDevEditStatus); + if (value == null) + return null; + + switch (value) + { + case 0: + return "Original"; + case 1: + return "Edited (Landscape)"; + case 6: + case 8: + return "Edited (Portrait)"; + default: + return "Unknown (" + value + ")"; + } + } + + @Nullable + public String getRawDevSettingsDescription() + { + Integer value = _directory.getInteger(TagRawDevSettings); + if (value == null) + return null; + + if (value == 0) + return "(none)"; + + StringBuilder sb = new StringBuilder(); + int v = value; + + if ((v & 1) != 0) sb.append("WB Color Temp, "); + if (((v >> 1) & 1) != 0) sb.append("WB Gray Point, "); + if (((v >> 2) & 1) != 0) sb.append("Saturation, "); + if (((v >> 3) & 1) != 0) sb.append("Contrast, "); + if (((v >> 4) & 1) != 0) sb.append("Sharpness, "); + if (((v >> 5) & 1) != 0) sb.append("Color Space, "); + if (((v >> 6) & 1) != 0) sb.append("High Function, "); + if (((v >> 7) & 1) != 0) sb.append("Noise Reduction, "); + + return sb.substring(0, sb.length() - 2); + } + +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java new file mode 100644 index 0000000..13ec7e5 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawDevelopmentMakernoteDirectory.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * The Olympus raw development makernote is used by many manufacturers (Epson, Konica, Minolta and Agfa...), and as such contains some tags + * that appear specific to those manufacturers. + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusRawDevelopmentMakernoteDirectory extends Directory +{ + public static final int TagRawDevVersion = 0x0000; + public static final int TagRawDevExposureBiasValue = 0x0100; + public static final int TagRawDevWhiteBalanceValue = 0x0101; + public static final int TagRawDevWbFineAdjustment = 0x0102; + public static final int TagRawDevGrayPoint = 0x0103; + public static final int TagRawDevSaturationEmphasis = 0x0104; + public static final int TagRawDevMemoryColorEmphasis = 0x0105; + public static final int TagRawDevContrastValue = 0x0106; + public static final int TagRawDevSharpnessValue = 0x0107; + public static final int TagRawDevColorSpace = 0x0108; + public static final int TagRawDevEngine = 0x0109; + public static final int TagRawDevNoiseReduction = 0x010a; + public static final int TagRawDevEditStatus = 0x010b; + public static final int TagRawDevSettings = 0x010c; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TagRawDevVersion, "Raw Dev Version"); + _tagNameMap.put(TagRawDevExposureBiasValue, "Raw Dev Exposure Bias Value"); + _tagNameMap.put(TagRawDevWhiteBalanceValue, "Raw Dev White Balance Value"); + _tagNameMap.put(TagRawDevWbFineAdjustment, "Raw Dev WB Fine Adjustment"); + _tagNameMap.put(TagRawDevGrayPoint, "Raw Dev Gray Point"); + _tagNameMap.put(TagRawDevSaturationEmphasis, "Raw Dev Saturation Emphasis"); + _tagNameMap.put(TagRawDevMemoryColorEmphasis, "Raw Dev Memory Color Emphasis"); + _tagNameMap.put(TagRawDevContrastValue, "Raw Dev Contrast Value"); + _tagNameMap.put(TagRawDevSharpnessValue, "Raw Dev Sharpness Value"); + _tagNameMap.put(TagRawDevColorSpace, "Raw Dev Color Space"); + _tagNameMap.put(TagRawDevEngine, "Raw Dev Engine"); + _tagNameMap.put(TagRawDevNoiseReduction, "Raw Dev Noise Reduction"); + _tagNameMap.put(TagRawDevEditStatus, "Raw Dev Edit Status"); + _tagNameMap.put(TagRawDevSettings, "Raw Dev Settings"); + } + + public OlympusRawDevelopmentMakernoteDirectory() + { + this.setDescriptor(new OlympusRawDevelopmentMakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Olympus Raw Development"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java new file mode 100644 index 0000000..aac65ae --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDescriptor.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.Rational; +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.makernotes.OlympusRawInfoMakernoteDirectory.*; + +/** + * Provides human-readable String representations of tag values stored in a {@link OlympusRawInfoMakernoteDirectory}. + * <p> + * Some Description functions converted from Exiftool version 10.33 created by Phil Harvey + * http://www.sno.phy.queensu.ca/~phil/exiftool/ + * lib\Image\ExifTool\Olympus.pm + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusRawInfoMakernoteDescriptor extends TagDescriptor<OlympusRawInfoMakernoteDirectory> +{ + public OlympusRawInfoMakernoteDescriptor(@NotNull OlympusRawInfoMakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagRawInfoVersion: + return getVersionBytesDescription(TagRawInfoVersion, 4); + case TagColorMatrix2: + return getColorMatrix2Description(); + case TagYCbCrCoefficients: + return getYCbCrCoefficientsDescription(); + case TagLightSource: + return getOlympusLightSourceDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getColorMatrix2Description() + { + int[] values = _directory.getIntArray(TagColorMatrix2); + if (values == null) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < values.length; i++) { + sb.append((short)values[i]); + if (i < values.length - 1) + sb.append(" "); + } + return sb.length() == 0 ? null : sb.toString(); + } + + @Nullable + public String getYCbCrCoefficientsDescription() + { + int[] values = _directory.getIntArray(TagYCbCrCoefficients); + if (values == null) + return null; + + Rational[] ret = new Rational[values.length / 2]; + for(int i = 0; i < values.length / 2; i++) + { + ret[i] = new Rational((short)values[2*i], (short)values[2*i + 1]); + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < ret.length; i++) { + sb.append(ret[i].doubleValue()); + if (i < ret.length - 1) + sb.append(" "); + } + return sb.length() == 0 ? null : sb.toString(); + } + + @Nullable + public String getOlympusLightSourceDescription() + { + Integer value = _directory.getInteger(TagLightSource); + if (value == null) + return null; + + switch (value.shortValue()) + { + case 0: + return "Unknown"; + case 16: + return "Shade"; + case 17: + return "Cloudy"; + case 18: + return "Fine Weather"; + case 20: + return "Tungsten (Incandescent)"; + case 22: + return "Evening Sunlight"; + case 33: + return "Daylight Fluorescent"; + case 34: + return "Day White Fluorescent"; + case 35: + return "Cool White Fluorescent"; + case 36: + return "White Fluorescent"; + case 256: + return "One Touch White Balance"; + case 512: + return "Custom 1-4"; + default: + return "Unknown (" + value + ")"; + } + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java new file mode 100644 index 0000000..629cf60 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/OlympusRawInfoMakernoteDirectory.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2015 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * These tags are found only in ORF images of some models (eg. C8080WZ) + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class OlympusRawInfoMakernoteDirectory extends Directory +{ + public static final int TagRawInfoVersion = 0x0000; + public static final int TagWbRbLevelsUsed = 0x0100; + public static final int TagWbRbLevelsAuto = 0x0110; + public static final int TagWbRbLevelsShade = 0x0120; + public static final int TagWbRbLevelsCloudy = 0x0121; + public static final int TagWbRbLevelsFineWeather = 0x0122; + public static final int TagWbRbLevelsTungsten = 0x0123; + public static final int TagWbRbLevelsEveningSunlight = 0x0124; + public static final int TagWbRbLevelsDaylightFluor = 0x0130; + public static final int TagWbRbLevelsDayWhiteFluor = 0x0131; + public static final int TagWbRbLevelsCoolWhiteFluor = 0x0132; + public static final int TagWbRbLevelsWhiteFluorescent = 0x0133; + + public static final int TagColorMatrix2 = 0x0200; + public static final int TagCoringFilter = 0x0310; + public static final int TagCoringValues = 0x0311; + public static final int TagBlackLevel2 = 0x0600; + public static final int TagYCbCrCoefficients = 0x0601; + public static final int TagValidPixelDepth = 0x0611; + public static final int TagCropLeft = 0x0612; + public static final int TagCropTop = 0x0613; + public static final int TagCropWidth = 0x0614; + public static final int TagCropHeight = 0x0615; + + public static final int TagLightSource = 0x1000; + + //the following 5 tags all have 3 values: val, min, max + public static final int TagWhiteBalanceComp = 0x1001; + public static final int TagSaturationSetting = 0x1010; + public static final int TagHueSetting = 0x1011; + public static final int TagContrastSetting = 0x1012; + public static final int TagSharpnessSetting = 0x1013; + + // settings written by Camedia Master 4.x + public static final int TagCmExposureCompensation = 0x2000; + public static final int TagCmWhiteBalance = 0x2001; + public static final int TagCmWhiteBalanceComp = 0x2002; + public static final int TagCmWhiteBalanceGrayPoint = 0x2010; + public static final int TagCmSaturation = 0x2020; + public static final int TagCmHue = 0x2021; + public static final int TagCmContrast = 0x2022; + public static final int TagCmSharpness = 0x2023; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TagRawInfoVersion, "Raw Info Version"); + _tagNameMap.put(TagWbRbLevelsUsed, "WB RB Levels Used"); + _tagNameMap.put(TagWbRbLevelsAuto, "WB RB Levels Auto"); + _tagNameMap.put(TagWbRbLevelsShade, "WB RB Levels Shade"); + _tagNameMap.put(TagWbRbLevelsCloudy, "WB RB Levels Cloudy"); + _tagNameMap.put(TagWbRbLevelsFineWeather, "WB RB Levels Fine Weather"); + _tagNameMap.put(TagWbRbLevelsTungsten, "WB RB Levels Tungsten"); + _tagNameMap.put(TagWbRbLevelsEveningSunlight, "WB RB Levels Evening Sunlight"); + _tagNameMap.put(TagWbRbLevelsDaylightFluor, "WB RB Levels Daylight Fluor"); + _tagNameMap.put(TagWbRbLevelsDayWhiteFluor, "WB RB Levels Day White Fluor"); + _tagNameMap.put(TagWbRbLevelsCoolWhiteFluor, "WB RB Levels Cool White Fluor"); + _tagNameMap.put(TagWbRbLevelsWhiteFluorescent, "WB RB Levels White Fluorescent"); + _tagNameMap.put(TagColorMatrix2, "Color Matrix 2"); + _tagNameMap.put(TagCoringFilter, "Coring Filter"); + _tagNameMap.put(TagCoringValues, "Coring Values"); + _tagNameMap.put(TagBlackLevel2, "Black Level 2"); + _tagNameMap.put(TagYCbCrCoefficients, "YCbCrCoefficients"); + _tagNameMap.put(TagValidPixelDepth, "Valid Pixel Depth"); + _tagNameMap.put(TagCropLeft, "Crop Left"); + _tagNameMap.put(TagCropTop, "Crop Top"); + _tagNameMap.put(TagCropWidth, "Crop Width"); + _tagNameMap.put(TagCropHeight, "Crop Height"); + _tagNameMap.put(TagLightSource, "Light Source"); + + _tagNameMap.put(TagWhiteBalanceComp, "White Balance Comp"); + _tagNameMap.put(TagSaturationSetting, "Saturation Setting"); + _tagNameMap.put(TagHueSetting, "Hue Setting"); + _tagNameMap.put(TagContrastSetting, "Contrast Setting"); + _tagNameMap.put(TagSharpnessSetting, "Sharpness Setting"); + + _tagNameMap.put(TagCmExposureCompensation, "CM Exposure Compensation"); + _tagNameMap.put(TagCmWhiteBalance, "CM White Balance"); + _tagNameMap.put(TagCmWhiteBalanceComp, "CM White Balance Comp"); + _tagNameMap.put(TagCmWhiteBalanceGrayPoint, "CM White Balance Gray Point"); + _tagNameMap.put(TagCmSaturation, "CM Saturation"); + _tagNameMap.put(TagCmHue, "CM Hue"); + _tagNameMap.put(TagCmContrast, "CM Contrast"); + _tagNameMap.put(TagCmSharpness, "CM Sharpness"); + } + + public OlympusRawInfoMakernoteDirectory() + { + this.setDescriptor(new OlympusRawInfoMakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Olympus Raw Info"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java index 71ca518..d5e4943 100644 --- a/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ package com.drew.metadata.exif.makernotes; import com.drew.lang.ByteArrayReader; +import com.drew.lang.Charsets; import com.drew.lang.RandomAccessReader; import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; @@ -28,6 +29,7 @@ import com.drew.metadata.Age; import com.drew.metadata.Face; import com.drew.metadata.TagDescriptor; +import java.text.DecimalFormat; import java.io.IOException; import static com.drew.metadata.exif.makernotes.PanasonicMakernoteDirectory.*; @@ -44,6 +46,7 @@ import static com.drew.metadata.exif.makernotes.PanasonicMakernoteDirectory.*; * @author Drew Noakes https://drewnoakes.com * @author Philipp Sandhaus */ +@SuppressWarnings("WeakerAccess") public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakernoteDirectory> { public PanasonicMakernoteDescriptor(@NotNull PanasonicMakernoteDirectory directory) @@ -108,8 +111,8 @@ public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakerno return getDetectedFacesDescription(); case TAG_TRANSFORM: return getTransformDescription(); - case TAG_TRANSFORM_1: - return getTransform1Description(); + case TAG_TRANSFORM_1: + return getTransform1Description(); case TAG_INTELLIGENT_EXPOSURE: return getIntelligentExposureDescription(); case TAG_FLASH_WARNING: @@ -126,20 +129,18 @@ public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakerno return getIntelligentResolutionDescription(); case TAG_FACE_RECOGNITION_INFO: return getRecognizedFacesDescription(); - case TAG_PRINT_IMAGE_MATCHING_INFO: - return getPrintImageMatchingInfoDescription(); case TAG_SCENE_MODE: return getSceneModeDescription(); case TAG_FLASH_FIRED: return getFlashFiredDescription(); case TAG_TEXT_STAMP: - return getTextStampDescription(); - case TAG_TEXT_STAMP_1: - return getTextStamp1Description(); - case TAG_TEXT_STAMP_2: - return getTextStamp2Description(); - case TAG_TEXT_STAMP_3: - return getTextStamp3Description(); + return getTextStampDescription(); + case TAG_TEXT_STAMP_1: + return getTextStamp1Description(); + case TAG_TEXT_STAMP_2: + return getTextStamp2Description(); + case TAG_TEXT_STAMP_3: + return getTextStamp3Description(); case TAG_MAKERNOTE_VERSION: return getMakernoteVersionDescription(); case TAG_EXIF_VERSION: @@ -147,26 +148,61 @@ public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakerno case TAG_INTERNAL_SERIAL_NUMBER: return getInternalSerialNumberDescription(); case TAG_TITLE: - return getTitleDescription(); - case TAG_BABY_NAME: - return getBabyNameDescription(); - case TAG_LOCATION: - return getLocationDescription(); - case TAG_BABY_AGE: - return getBabyAgeDescription(); - case TAG_BABY_AGE_1: - return getBabyAge1Description(); - default: + return getTitleDescription(); + case TAG_BRACKET_SETTINGS: + return getBracketSettingsDescription(); + case TAG_FLASH_CURTAIN: + return getFlashCurtainDescription(); + case TAG_LONG_EXPOSURE_NOISE_REDUCTION: + return getLongExposureNoiseReductionDescription(); + case TAG_BABY_NAME: + return getBabyNameDescription(); + case TAG_LOCATION: + return getLocationDescription(); + + case TAG_LENS_FIRMWARE_VERSION: + return getLensFirmwareVersionDescription(); + case TAG_INTELLIGENT_D_RANGE: + return getIntelligentDRangeDescription(); + case TAG_CLEAR_RETOUCH: + return getClearRetouchDescription(); + case TAG_PHOTO_STYLE: + return getPhotoStyleDescription(); + case TAG_SHADING_COMPENSATION: + return getShadingCompensationDescription(); + + case TAG_ACCELEROMETER_Z: + return getAccelerometerZDescription(); + case TAG_ACCELEROMETER_X: + return getAccelerometerXDescription(); + case TAG_ACCELEROMETER_Y: + return getAccelerometerYDescription(); + case TAG_CAMERA_ORIENTATION: + return getCameraOrientationDescription(); + case TAG_ROLL_ANGLE: + return getRollAngleDescription(); + case TAG_PITCH_ANGLE: + return getPitchAngleDescription(); + case TAG_SWEEP_PANORAMA_DIRECTION: + return getSweepPanoramaDirectionDescription(); + case TAG_TIMER_RECORDING: + return getTimerRecordingDescription(); + case TAG_HDR: + return getHDRDescription(); + case TAG_SHUTTER_TYPE: + return getShutterTypeDescription(); + case TAG_TOUCH_AE: + return getTouchAeDescription(); + + case TAG_BABY_AGE: + return getBabyAgeDescription(); + case TAG_BABY_AGE_1: + return getBabyAge1Description(); + default: return super.getDescription(tagType); } } - @Nullable - public String getPrintImageMatchingInfoDescription() - { - return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO); - } - @Nullable public String getTextStampDescription() { @@ -277,46 +313,241 @@ public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakerno "No", "Yes (Flash required but disabled)"); } + @Nullable + private static String trim(@Nullable String s) + { + return s == null ? null : s.trim(); + } + @Nullable public String getCountryDescription() { - return getAsciiStringFromBytes(TAG_COUNTRY); + return trim(getStringFromBytes(TAG_COUNTRY, Charsets.UTF_8)); } @Nullable public String getStateDescription() { - return getAsciiStringFromBytes(TAG_STATE); + return trim(getStringFromBytes(TAG_STATE, Charsets.UTF_8)); } @Nullable public String getCityDescription() { - return getAsciiStringFromBytes(TAG_CITY); + return trim(getStringFromBytes(TAG_CITY, Charsets.UTF_8)); } @Nullable public String getLandmarkDescription() { - return getAsciiStringFromBytes(TAG_LANDMARK); + return trim(getStringFromBytes(TAG_LANDMARK, Charsets.UTF_8)); } - @Nullable + @Nullable public String getTitleDescription() { - return getAsciiStringFromBytes(TAG_TITLE); + return trim(getStringFromBytes(TAG_TITLE, Charsets.UTF_8)); } - @Nullable + @Nullable + public String getBracketSettingsDescription() + { + return getIndexedDescription(TAG_BRACKET_SETTINGS, + "No Bracket", "3 Images, Sequence 0/-/+", "3 Images, Sequence -/0/+", "5 Images, Sequence 0/-/+", + "5 Images, Sequence -/0/+", "7 Images, Sequence 0/-/+", "7 Images, Sequence -/0/+"); + } + + @Nullable + public String getFlashCurtainDescription() + { + return getIndexedDescription(TAG_FLASH_CURTAIN, + "n/a", "1st", "2nd"); + } + + @Nullable + public String getLongExposureNoiseReductionDescription() + { + return getIndexedDescription(TAG_LONG_EXPOSURE_NOISE_REDUCTION, 1, + "Off", "On"); + } + + @Nullable + public String getLensFirmwareVersionDescription() + { + // lens version has 4 parts separated by periods + byte[] bytes = _directory.getByteArray(TAG_LENS_FIRMWARE_VERSION); + if (bytes == null) + return null; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + sb.append(bytes[i]); + if (i < bytes.length - 1) + sb.append("."); + } + return sb.toString(); + //return string.Join(".", bytes.Select(b => b.ToString()).ToArray()); + } + + @Nullable + public String getIntelligentDRangeDescription() + { + return getIndexedDescription(TAG_INTELLIGENT_D_RANGE, + "Off", "Low", "Standard", "High"); + } + + @Nullable + public String getClearRetouchDescription() + { + return getIndexedDescription(TAG_CLEAR_RETOUCH, + "Off", "On"); + + } + + @Nullable + public String getPhotoStyleDescription() + { + return getIndexedDescription(TAG_PHOTO_STYLE, + "Auto", "Standard or Custom", "Vivid", "Natural", "Monochrome", "Scenery", "Portrait"); + } + + @Nullable + public String getShadingCompensationDescription() + { + return getIndexedDescription(TAG_SHADING_COMPENSATION, + "Off", "On"); + } + + @Nullable + public String getAccelerometerZDescription() + { + Integer value = _directory.getInteger(TAG_ACCELEROMETER_Z); + if (value == null) + return null; + + // positive is acceleration upwards + return String.valueOf(value.shortValue()); + } + + @Nullable + public String getAccelerometerXDescription() + { + Integer value = _directory.getInteger(TAG_ACCELEROMETER_X); + if (value == null) + return null; + + // positive is acceleration to the left + return String.valueOf(value.shortValue()); + } + + @Nullable + public String getAccelerometerYDescription() + { + Integer value = _directory.getInteger(TAG_ACCELEROMETER_Y); + if (value == null) + return null; + + // positive is acceleration backwards + return String.valueOf(value.shortValue()); + } + + @Nullable + public String getCameraOrientationDescription() + { + return getIndexedDescription(TAG_CAMERA_ORIENTATION, + "Normal", "Rotate CW", "Rotate 180", "Rotate CCW", "Tilt Upwards", "Tile Downwards"); + } + + @Nullable + public String getRollAngleDescription() + { + Integer value = _directory.getInteger(TAG_ROLL_ANGLE); + if (value == null) + return null; + + DecimalFormat format = new DecimalFormat("0.#"); + // converted to degrees of clockwise camera rotation + return format.format(value.shortValue() / 10.0); + } + + @Nullable + public String getPitchAngleDescription() + { + Integer value = _directory.getInteger(TAG_PITCH_ANGLE); + if (value == null) + return null; + + DecimalFormat format = new DecimalFormat("0.#"); + // converted to degrees of upward camera tilt + return format.format(-value.shortValue() / 10.0); + } + + @Nullable + public String getSweepPanoramaDirectionDescription() + { + return getIndexedDescription(TAG_SWEEP_PANORAMA_DIRECTION, + "Off", "Left to Right", "Right to Left", "Top to Bottom", "Bottom to Top"); + } + + @Nullable + public String getTimerRecordingDescription() + { + return getIndexedDescription(TAG_TIMER_RECORDING, + "Off", "Time Lapse", "Stop-motion Animation"); + } + + @Nullable + public String getHDRDescription() + { + Integer value = _directory.getInteger(TAG_HDR); + if (value == null) + return null; + + switch (value) + { + case 0: + return "Off"; + case 100: + return "1 EV"; + case 200: + return "2 EV"; + case 300: + return "3 EV"; + case 32868: + return "1 EV (Auto)"; + case 32968: + return "2 EV (Auto)"; + case 33068: + return "3 EV (Auto)"; + default: + return String.format("Unknown (%d)", value); + } + } + + @Nullable + public String getShutterTypeDescription() + { + return getIndexedDescription(TAG_SHUTTER_TYPE, + "Mechanical", "Electronic", "Hybrid"); + } + + @Nullable + public String getTouchAeDescription() + { + return getIndexedDescription(TAG_TOUCH_AE, + "Off", "On"); + } + + @Nullable public String getBabyNameDescription() { - return getAsciiStringFromBytes(TAG_BABY_NAME); + return trim(getStringFromBytes(TAG_BABY_NAME, Charsets.UTF_8)); } @Nullable public String getLocationDescription() { - return getAsciiStringFromBytes(TAG_LOCATION); + return trim(getStringFromBytes(TAG_LOCATION, Charsets.UTF_8)); } @Nullable diff --git a/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java index a934cec..b2b6c25 100644 --- a/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/PanasonicMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,8 +34,10 @@ import java.util.HashMap; /** * Describes tags specific to Panasonic and Leica cameras. * - * @author Drew Noakes https://drewnoakes.com, Philipp Sandhaus + * @author Drew Noakes https://drewnoakes.com + * @author Philipp Sandhaus */ +@SuppressWarnings("WeakerAccess") public class PanasonicMakernoteDirectory extends Directory { @@ -352,16 +354,23 @@ public class PanasonicMakernoteDirectory extends Directory public static final int TAG_SHARPNESS = 0x0041; public static final int TAG_FILM_MODE = 0x0042; + public static final int TAG_COLOR_TEMP_KELVIN = 0x0044; + public static final int TAG_BRACKET_SETTINGS = 0x0045; + /** - * WB adjust AB. Positive is a shift toward blue. - */ - public static final int TAG_WB_ADJUST_AB = 0x0046; + * WB adjust AB. Positive is a shift toward blue. + */ + public static final int TAG_WB_ADJUST_AB = 0x0046; /** - * WB adjust GM. Positive is a shift toward green. - */ - public static final int TAG_WB_ADJUST_GM = 0x0047; + * WB adjust GM. Positive is a shift toward green. + */ + public static final int TAG_WB_ADJUST_GM = 0x0047; + public static final int TAG_FLASH_CURTAIN = 0x0048; + public static final int TAG_LONG_EXPOSURE_NOISE_REDUCTION = 0x0049; + public static final int TAG_PANASONIC_IMAGE_WIDTH = 0x004b; + public static final int TAG_PANASONIC_IMAGE_HEIGHT = 0x004c; public static final int TAG_AF_POINT_POSITION = 0x004d; @@ -382,6 +391,7 @@ public class PanasonicMakernoteDirectory extends Directory public static final int TAG_LENS_TYPE = 0x0051; public static final int TAG_LENS_SERIAL_NUMBER = 0x0052; public static final int TAG_ACCESSORY_TYPE = 0x0053; + public static final int TAG_ACCESSORY_SERIAL_NUMBER = 0x0054; /** * (decoded as two 16-bit signed integers) @@ -401,10 +411,35 @@ public class PanasonicMakernoteDirectory extends Directory */ public static final int TAG_INTELLIGENT_EXPOSURE = 0x005d; - /** - * Info at http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html - */ - public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00; + public static final int TAG_LENS_FIRMWARE_VERSION = 0x0060; + public static final int TAG_BURST_SPEED = 0x0077; + public static final int TAG_INTELLIGENT_D_RANGE = 0x0079; + public static final int TAG_CLEAR_RETOUCH = 0x007c; + public static final int TAG_CITY2 = 0x0080; + public static final int TAG_PHOTO_STYLE = 0x0089; + public static final int TAG_SHADING_COMPENSATION = 0x008a; + + public static final int TAG_ACCELEROMETER_Z = 0x008c; + public static final int TAG_ACCELEROMETER_X = 0x008d; + public static final int TAG_ACCELEROMETER_Y = 0x008e; + public static final int TAG_CAMERA_ORIENTATION = 0x008f; + public static final int TAG_ROLL_ANGLE = 0x0090; + public static final int TAG_PITCH_ANGLE = 0x0091; + public static final int TAG_SWEEP_PANORAMA_DIRECTION = 0x0093; + public static final int TAG_SWEEP_PANORAMA_FIELD_OF_VIEW = 0x0094; + public static final int TAG_TIMER_RECORDING = 0x0096; + + public static final int TAG_INTERNAL_ND_FILTER = 0x009d; + public static final int TAG_HDR = 0x009e; + public static final int TAG_SHUTTER_TYPE = 0x009f; + + public static final int TAG_CLEAR_RETOUCH_VALUE = 0x00a3; + public static final int TAG_TOUCH_AE = 0x00ab; + + /** + * Info at http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html + */ + public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00; /** * Byte Indexes: <br> @@ -432,9 +467,9 @@ public class PanasonicMakernoteDirectory extends Directory public static final int TAG_FLASH_WARNING = 0x0062; public static final int TAG_RECOGNIZED_FACE_FLAGS = 0x0063; public static final int TAG_TITLE = 0x0065; - public static final int TAG_BABY_NAME = 0x0066; - public static final int TAG_LOCATION = 0x0067; - public static final int TAG_COUNTRY = 0x0069; + public static final int TAG_BABY_NAME = 0x0066; + public static final int TAG_LOCATION = 0x0067; + public static final int TAG_COUNTRY = 0x0069; public static final int TAG_STATE = 0x006b; public static final int TAG_CITY = 0x006d; public static final int TAG_LANDMARK = 0x006f; @@ -453,8 +488,8 @@ public class PanasonicMakernoteDirectory extends Directory public static final int TAG_WB_BLUE_LEVEL = 0x8006; public static final int TAG_FLASH_FIRED = 0x8007; public static final int TAG_TEXT_STAMP_2 = 0x8008; - public static final int TAG_TEXT_STAMP_3 = 0x8009; - public static final int TAG_BABY_AGE_1 = 0x8010; + public static final int TAG_TEXT_STAMP_3 = 0x8009; + public static final int TAG_BABY_AGE_1 = 0x8010; /** * (decoded as two 16-bit signed integers) @@ -504,43 +539,76 @@ public class PanasonicMakernoteDirectory extends Directory _tagNameMap.put(TAG_WORLD_TIME_LOCATION, "World Time Location"); _tagNameMap.put(TAG_TEXT_STAMP, "Text Stamp"); _tagNameMap.put(TAG_PROGRAM_ISO, "Program ISO"); - _tagNameMap.put(TAG_ADVANCED_SCENE_MODE, "Advanced Scene Mode"); + _tagNameMap.put(TAG_ADVANCED_SCENE_MODE, "Advanced Scene Mode"); _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info"); _tagNameMap.put(TAG_FACES_DETECTED, "Number of Detected Faces"); _tagNameMap.put(TAG_SATURATION, "Saturation"); _tagNameMap.put(TAG_SHARPNESS, "Sharpness"); _tagNameMap.put(TAG_FILM_MODE, "Film Mode"); + _tagNameMap.put(TAG_COLOR_TEMP_KELVIN, "Color Temp Kelvin"); + _tagNameMap.put(TAG_BRACKET_SETTINGS, "Bracket Settings"); _tagNameMap.put(TAG_WB_ADJUST_AB, "White Balance Adjust (AB)"); - _tagNameMap.put(TAG_WB_ADJUST_GM, "White Balance Adjust (GM)"); - _tagNameMap.put(TAG_AF_POINT_POSITION, "Af Point Position"); + _tagNameMap.put(TAG_WB_ADJUST_GM, "White Balance Adjust (GM)"); + + _tagNameMap.put(TAG_FLASH_CURTAIN, "Flash Curtain"); + _tagNameMap.put(TAG_LONG_EXPOSURE_NOISE_REDUCTION, "Long Exposure Noise Reduction"); + _tagNameMap.put(TAG_PANASONIC_IMAGE_WIDTH, "Panasonic Image Width"); + _tagNameMap.put(TAG_PANASONIC_IMAGE_HEIGHT, "Panasonic Image Height"); + + _tagNameMap.put(TAG_AF_POINT_POSITION, "Af Point Position"); _tagNameMap.put(TAG_FACE_DETECTION_INFO, "Face Detection Info"); _tagNameMap.put(TAG_LENS_TYPE, "Lens Type"); _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number"); _tagNameMap.put(TAG_ACCESSORY_TYPE, "Accessory Type"); + _tagNameMap.put(TAG_ACCESSORY_SERIAL_NUMBER, "Accessory Serial Number"); _tagNameMap.put(TAG_TRANSFORM, "Transform"); _tagNameMap.put(TAG_INTELLIGENT_EXPOSURE, "Intelligent Exposure"); + _tagNameMap.put(TAG_LENS_FIRMWARE_VERSION, "Lens Firmware Version"); _tagNameMap.put(TAG_FACE_RECOGNITION_INFO, "Face Recognition Info"); _tagNameMap.put(TAG_FLASH_WARNING, "Flash Warning"); _tagNameMap.put(TAG_RECOGNIZED_FACE_FLAGS, "Recognized Face Flags"); - _tagNameMap.put(TAG_TITLE, "Title"); - _tagNameMap.put(TAG_BABY_NAME, "Baby Name"); - _tagNameMap.put(TAG_LOCATION, "Location"); - _tagNameMap.put(TAG_COUNTRY, "Country"); + _tagNameMap.put(TAG_TITLE, "Title"); + _tagNameMap.put(TAG_BABY_NAME, "Baby Name"); + _tagNameMap.put(TAG_LOCATION, "Location"); + _tagNameMap.put(TAG_COUNTRY, "Country"); _tagNameMap.put(TAG_STATE, "State"); _tagNameMap.put(TAG_CITY, "City"); _tagNameMap.put(TAG_LANDMARK, "Landmark"); _tagNameMap.put(TAG_INTELLIGENT_RESOLUTION, "Intelligent Resolution"); + _tagNameMap.put(TAG_BURST_SPEED, "Burst Speed"); + _tagNameMap.put(TAG_INTELLIGENT_D_RANGE, "Intelligent D-Range"); + _tagNameMap.put(TAG_CLEAR_RETOUCH, "Clear Retouch"); + _tagNameMap.put(TAG_CITY2, "City 2"); + _tagNameMap.put(TAG_PHOTO_STYLE, "Photo Style"); + _tagNameMap.put(TAG_SHADING_COMPENSATION, "Shading Compensation"); + + _tagNameMap.put(TAG_ACCELEROMETER_Z, "Accelerometer Z"); + _tagNameMap.put(TAG_ACCELEROMETER_X, "Accelerometer X"); + _tagNameMap.put(TAG_ACCELEROMETER_Y, "Accelerometer Y"); + _tagNameMap.put(TAG_CAMERA_ORIENTATION, "Camera Orientation"); + _tagNameMap.put(TAG_ROLL_ANGLE, "Roll Angle"); + _tagNameMap.put(TAG_PITCH_ANGLE, "Pitch Angle"); + _tagNameMap.put(TAG_SWEEP_PANORAMA_DIRECTION, "Sweep Panorama Direction"); + _tagNameMap.put(TAG_SWEEP_PANORAMA_FIELD_OF_VIEW, "Sweep Panorama Field Of View"); + _tagNameMap.put(TAG_TIMER_RECORDING, "Timer Recording"); + + _tagNameMap.put(TAG_INTERNAL_ND_FILTER, "Internal ND Filter"); + _tagNameMap.put(TAG_HDR, "HDR"); + _tagNameMap.put(TAG_SHUTTER_TYPE, "Shutter Type"); + _tagNameMap.put(TAG_CLEAR_RETOUCH_VALUE, "Clear Retouch Value"); + _tagNameMap.put(TAG_TOUCH_AE, "Touch AE"); + _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version"); _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode"); _tagNameMap.put(TAG_WB_RED_LEVEL, "White Balance (Red)"); _tagNameMap.put(TAG_WB_GREEN_LEVEL, "White Balance (Green)"); _tagNameMap.put(TAG_WB_BLUE_LEVEL, "White Balance (Blue)"); _tagNameMap.put(TAG_FLASH_FIRED, "Flash Fired"); - _tagNameMap.put(TAG_TEXT_STAMP_1, "Text Stamp 1"); - _tagNameMap.put(TAG_TEXT_STAMP_2, "Text Stamp 2"); - _tagNameMap.put(TAG_TEXT_STAMP_3, "Text Stamp 3"); - _tagNameMap.put(TAG_BABY_AGE_1, "Baby Age 1"); - _tagNameMap.put(TAG_TRANSFORM_1, "Transform 1"); + _tagNameMap.put(TAG_TEXT_STAMP_1, "Text Stamp 1"); + _tagNameMap.put(TAG_TEXT_STAMP_2, "Text Stamp 2"); + _tagNameMap.put(TAG_TEXT_STAMP_3, "Text Stamp 3"); + _tagNameMap.put(TAG_BABY_AGE_1, "Baby Age 1"); + _tagNameMap.put(TAG_TRANSFORM_1, "Transform 1"); } public PanasonicMakernoteDirectory() diff --git a/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java index 30889e1..40ae317 100644 --- a/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import static com.drew.metadata.exif.makernotes.PentaxMakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class PentaxMakernoteDescriptor extends TagDescriptor<PentaxMakernoteDirectory> { public PentaxMakernoteDescriptor(@NotNull PentaxMakernoteDirectory directory) diff --git a/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java index ee38d55..8915c62 100644 --- a/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/PentaxMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class PentaxMakernoteDirectory extends Directory { /** diff --git a/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java new file mode 100644 index 0000000..97f0190 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDescriptor.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.StringValue; +import com.drew.metadata.TagDescriptor; + +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import static com.drew.metadata.exif.makernotes.ReconyxHyperFireMakernoteDirectory.*; + +/** + * Provides human-readable string representations of tag values stored in a {@link ReconyxHyperFireMakernoteDirectory}. + * + * @author Todd West http://cascadescarnivoreproject.blogspot.com + */ +@SuppressWarnings("WeakerAccess") +public class ReconyxHyperFireMakernoteDescriptor extends TagDescriptor<ReconyxHyperFireMakernoteDirectory> +{ + public ReconyxHyperFireMakernoteDescriptor(@NotNull ReconyxHyperFireMakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TAG_MAKERNOTE_VERSION: + return String.format("%d", _directory.getInteger(tagType)); + case TAG_FIRMWARE_VERSION: + return _directory.getString(tagType); + case TAG_TRIGGER_MODE: + return _directory.getString(tagType); + case TAG_SEQUENCE: + int[] sequence = _directory.getIntArray(tagType); + if (sequence == null) + return null; + return String.format("%d/%d", sequence[0], sequence[1]); + case TAG_EVENT_NUMBER: + return String.format("%d", _directory.getInteger(tagType)); + case TAG_MOTION_SENSITIVITY: + return String.format("%d", _directory.getInteger(tagType)); + case TAG_BATTERY_VOLTAGE: + Double value = _directory.getDoubleObject(tagType); + DecimalFormat formatter = new DecimalFormat("0.000"); + return value == null ? null : formatter.format(value); + case TAG_DATE_TIME_ORIGINAL: + String date = _directory.getString(tagType); + try { + DateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); + return parser.format(parser.parse(date)); + } catch (ParseException e) { + return null; + } + case TAG_MOON_PHASE: + return getIndexedDescription(tagType, "New", "Waxing Crescent", "First Quarter", "Waxing Gibbous", "Full", "Waning Gibbous", "Last Quarter", "Waning Crescent"); + case TAG_AMBIENT_TEMPERATURE_FAHRENHEIT: + case TAG_AMBIENT_TEMPERATURE: + return String.format("%d", _directory.getInteger(tagType)); + case TAG_SERIAL_NUMBER: + // default is UTF_16LE + StringValue svalue = _directory.getStringValue(tagType); + if(svalue == null) + return null; + return svalue.toString(); + case TAG_CONTRAST: + case TAG_BRIGHTNESS: + case TAG_SHARPNESS: + case TAG_SATURATION: + return String.format("%d", _directory.getInteger(tagType)); + case TAG_INFRARED_ILLUMINATOR: + return getIndexedDescription(tagType, "Off", "On"); + case TAG_USER_LABEL: + return _directory.getString(tagType); + default: + return super.getDescription(tagType); + } + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java new file mode 100644 index 0000000..74dbb2e --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/ReconyxHyperFireMakernoteDirectory.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * Describes tags specific to Reconyx HyperFire cameras. + * + * Reconyx uses a fixed makernote block. Tag values are the byte index of the tag within the makernote. + * @author Todd West http://cascadescarnivoreproject.blogspot.com + */ +@SuppressWarnings("WeakerAccess") +public class ReconyxHyperFireMakernoteDirectory extends Directory +{ + /** + * Version number used for identifying makernotes from Reconyx HyperFire cameras. + */ + public static final int MAKERNOTE_VERSION = 61697; + + public static final int TAG_MAKERNOTE_VERSION = 0; + public static final int TAG_FIRMWARE_VERSION = 2; + public static final int TAG_TRIGGER_MODE = 12; + public static final int TAG_SEQUENCE = 14; + public static final int TAG_EVENT_NUMBER = 18; + public static final int TAG_DATE_TIME_ORIGINAL = 22; + public static final int TAG_MOON_PHASE = 36; + public static final int TAG_AMBIENT_TEMPERATURE_FAHRENHEIT = 38; + public static final int TAG_AMBIENT_TEMPERATURE = 40; + public static final int TAG_SERIAL_NUMBER = 42; + public static final int TAG_CONTRAST = 72; + public static final int TAG_BRIGHTNESS = 74; + public static final int TAG_SHARPNESS = 76; + public static final int TAG_SATURATION = 78; + public static final int TAG_INFRARED_ILLUMINATOR = 80; + public static final int TAG_MOTION_SENSITIVITY = 82; + public static final int TAG_BATTERY_VOLTAGE = 84; + public static final int TAG_USER_LABEL = 86; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version"); + _tagNameMap.put(TAG_FIRMWARE_VERSION, "Firmware Version"); + _tagNameMap.put(TAG_TRIGGER_MODE, "Trigger Mode"); + _tagNameMap.put(TAG_SEQUENCE, "Sequence"); + _tagNameMap.put(TAG_EVENT_NUMBER, "Event Number"); + _tagNameMap.put(TAG_DATE_TIME_ORIGINAL, "Date/Time Original"); + _tagNameMap.put(TAG_MOON_PHASE, "Moon Phase"); + _tagNameMap.put(TAG_AMBIENT_TEMPERATURE_FAHRENHEIT, "Ambient Temperature Fahrenheit"); + _tagNameMap.put(TAG_AMBIENT_TEMPERATURE, "Ambient Temperature"); + _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number"); + _tagNameMap.put(TAG_CONTRAST, "Contrast"); + _tagNameMap.put(TAG_BRIGHTNESS, "Brightness"); + _tagNameMap.put(TAG_SHARPNESS, "Sharpness"); + _tagNameMap.put(TAG_SATURATION, "Saturation"); + _tagNameMap.put(TAG_INFRARED_ILLUMINATOR, "Infrared Illuminator"); + _tagNameMap.put(TAG_MOTION_SENSITIVITY, "Motion Sensitivity"); + _tagNameMap.put(TAG_BATTERY_VOLTAGE, "Battery Voltage"); + _tagNameMap.put(TAG_USER_LABEL, "User Label"); + } + + public ReconyxHyperFireMakernoteDirectory() + { + this.setDescriptor(new ReconyxHyperFireMakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Reconyx HyperFire Makernote"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java new file mode 100644 index 0000000..86d4e70 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDescriptor.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.StringValue; +import com.drew.metadata.TagDescriptor; + +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import static com.drew.metadata.exif.makernotes.ReconyxUltraFireMakernoteDirectory.*; + +/** + * Provides human-readable string representations of tag values stored in a {@link ReconyxUltraFireMakernoteDirectory}. + * + * @author Todd West http://cascadescarnivoreproject.blogspot.com + */ +@SuppressWarnings("WeakerAccess") +public class ReconyxUltraFireMakernoteDescriptor extends TagDescriptor<ReconyxUltraFireMakernoteDirectory> +{ + public ReconyxUltraFireMakernoteDescriptor(@NotNull ReconyxUltraFireMakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TAG_LABEL: + return _directory.getString(tagType); + case TAG_MAKERNOTE_ID: + return String.format("0x%08X", _directory.getInteger(tagType)); + case TAG_MAKERNOTE_SIZE: + return String.format("%d", _directory.getInteger(tagType)); + case TAG_MAKERNOTE_PUBLIC_ID: + return String.format("0x%08X", _directory.getInteger(tagType)); + case TAG_MAKERNOTE_PUBLIC_SIZE: + return String.format("%d", _directory.getInteger(tagType)); + case TAG_CAMERA_VERSION: + case TAG_UIB_VERSION: + case TAG_BTL_VERSION: + case TAG_PEX_VERSION: + case TAG_EVENT_TYPE: + return _directory.getString(tagType); + case TAG_SEQUENCE: + int[] sequence = _directory.getIntArray(tagType); + if (sequence == null) + return null; + return String.format("%d/%d", sequence[0], sequence[1]); + case TAG_EVENT_NUMBER: + return String.format("%d", _directory.getInteger(tagType)); + case TAG_DATE_TIME_ORIGINAL: + String date = _directory.getString(tagType); + try { + DateFormat parser = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); + return parser.format(parser.parse(date)); + } catch (ParseException e) { + return null; + } + /*case TAG_DAY_OF_WEEK: + return getIndexedDescription(tagType, CultureInfo.CurrentCulture.DateTimeFormat.DayNames);*/ + case TAG_MOON_PHASE: + return getIndexedDescription(tagType, "New", "Waxing Crescent", "First Quarter", "Waxing Gibbous", "Full", "Waning Gibbous", "Last Quarter", "Waning Crescent"); + case TAG_AMBIENT_TEMPERATURE_FAHRENHEIT: + case TAG_AMBIENT_TEMPERATURE: + return String.format("%d", _directory.getInteger(tagType)); + case TAG_FLASH: + return getIndexedDescription(tagType, "Off", "On"); + case TAG_BATTERY_VOLTAGE: + Double value = _directory.getDoubleObject(tagType); + DecimalFormat formatter = new DecimalFormat("0.000"); + return value == null ? null : formatter.format(value); + case TAG_SERIAL_NUMBER: + // default is UTF_8 + StringValue svalue = _directory.getStringValue(tagType); + if(svalue == null) + return null; + return svalue.toString(); + case TAG_USER_LABEL: + return _directory.getString(tagType); + default: + return super.getDescription(tagType); + } + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java new file mode 100644 index 0000000..d518ba7 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/ReconyxUltraFireMakernoteDirectory.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * Describes tags specific to Reconyx UltraFire cameras. + * + * Reconyx uses a fixed makernote block. Tag values are the byte index of the tag within the makernote. + * @author Todd West http://cascadescarnivoreproject.blogspot.com + */ +@SuppressWarnings("WeakerAccess") +public class ReconyxUltraFireMakernoteDirectory extends Directory +{ + /** + * Version number used for identifying makernotes from Reconyx UltraFire cameras. + */ + public static final int MAKERNOTE_ID = 0x00010000; + + /** + * Version number used for identifying the public portion of makernotes from Reconyx UltraFire cameras. + */ + public static final int MAKERNOTE_PUBLIC_ID = 0x07f10001; + + public static final int TAG_LABEL = 0; + public static final int TAG_MAKERNOTE_ID = 10; + public static final int TAG_MAKERNOTE_SIZE = 14; + public static final int TAG_MAKERNOTE_PUBLIC_ID = 18; + public static final int TAG_MAKERNOTE_PUBLIC_SIZE = 22; + public static final int TAG_CAMERA_VERSION = 24; + public static final int TAG_UIB_VERSION = 31; + public static final int TAG_BTL_VERSION = 38; + public static final int TAG_PEX_VERSION = 45; + public static final int TAG_EVENT_TYPE = 52; + public static final int TAG_SEQUENCE = 53; + public static final int TAG_EVENT_NUMBER = 55; + public static final int TAG_DATE_TIME_ORIGINAL = 59; + public static final int TAG_DAY_OF_WEEK = 66; + public static final int TAG_MOON_PHASE = 67; + public static final int TAG_AMBIENT_TEMPERATURE_FAHRENHEIT = 68; + public static final int TAG_AMBIENT_TEMPERATURE = 70; + public static final int TAG_FLASH = 72; + public static final int TAG_BATTERY_VOLTAGE = 73; + public static final int TAG_SERIAL_NUMBER = 75; + public static final int TAG_USER_LABEL = 80; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TAG_LABEL, "Makernote Label"); + _tagNameMap.put(TAG_MAKERNOTE_ID, "Makernote ID"); + _tagNameMap.put(TAG_MAKERNOTE_SIZE, "Makernote Size"); + _tagNameMap.put(TAG_MAKERNOTE_PUBLIC_ID, "Makernote Public ID"); + _tagNameMap.put(TAG_MAKERNOTE_PUBLIC_SIZE, "Makernote Public Size"); + _tagNameMap.put(TAG_CAMERA_VERSION, "Camera Version"); + _tagNameMap.put(TAG_UIB_VERSION, "Uib Version"); + _tagNameMap.put(TAG_BTL_VERSION, "Btl Version"); + _tagNameMap.put(TAG_PEX_VERSION, "Pex Version"); + _tagNameMap.put(TAG_EVENT_TYPE, "Event Type"); + _tagNameMap.put(TAG_SEQUENCE, "Sequence"); + _tagNameMap.put(TAG_EVENT_NUMBER, "Event Number"); + _tagNameMap.put(TAG_DATE_TIME_ORIGINAL, "Date/Time Original"); + _tagNameMap.put(TAG_DAY_OF_WEEK, "Day of Week"); + _tagNameMap.put(TAG_MOON_PHASE, "Moon Phase"); + _tagNameMap.put(TAG_AMBIENT_TEMPERATURE_FAHRENHEIT, "Ambient Temperature Fahrenheit"); + _tagNameMap.put(TAG_AMBIENT_TEMPERATURE, "Ambient Temperature"); + _tagNameMap.put(TAG_FLASH, "Flash"); + _tagNameMap.put(TAG_BATTERY_VOLTAGE, "Battery Voltage"); + _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number"); + _tagNameMap.put(TAG_USER_LABEL, "User Label"); + } + + public ReconyxUltraFireMakernoteDirectory() + { + this.setDescriptor(new ReconyxUltraFireMakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Reconyx UltraFire Makernote"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java index 46001db..ace1cbc 100644 --- a/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,13 +25,14 @@ import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; /** - * Provides human-readable string representations of tag values stored in a {@link RicohMakernoteDescriptor}. + * Provides human-readable string representations of tag values stored in a {@link RicohMakernoteDirectory}. * <p> * Some information about this makernote taken from here: * http://www.ozhiker.com/electronics/pjmt/jpeg_info/ricoh_mn.html * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class RicohMakernoteDescriptor extends TagDescriptor<RicohMakernoteDirectory> { public RicohMakernoteDescriptor(@NotNull RicohMakernoteDirectory directory) @@ -44,20 +45,12 @@ public class RicohMakernoteDescriptor extends TagDescriptor<RicohMakernoteDirect public String getDescription(int tagType) { switch (tagType) { -// case TAG_PRINT_IMAGE_MATCHING_INFO: -// return getPrintImageMatchingInfoDescription(); // case TAG_PROPRIETARY_THUMBNAIL: // return getProprietaryThumbnailDataDescription(); default: return super.getDescription(tagType); } } - -// @Nullable -// public String getPrintImageMatchingInfoDescription() -// { -// return getByteLengthDescription(TAG_PRINT_IMAGE_MATCHING_INFO); -// } // // @Nullable // public String getProprietaryThumbnailDataDescription() diff --git a/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java index fdb1e90..da36a58 100644 --- a/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/RicohMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class RicohMakernoteDirectory extends Directory { public static final int TAG_MAKERNOTE_DATA_TYPE = 0x0001; diff --git a/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java new file mode 100644 index 0000000..dc747ed --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDescriptor.java @@ -0,0 +1,213 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.exif.makernotes.SamsungType2MakernoteDirectory.*; + +/** + * Provides human-readable string representations of tag values stored in a {@link SamsungType2MakernoteDirectory}. + * <p> + * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Samsung.html + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class SamsungType2MakernoteDescriptor extends TagDescriptor<SamsungType2MakernoteDirectory> +{ + public SamsungType2MakernoteDescriptor(@NotNull SamsungType2MakernoteDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TagMakerNoteVersion: + return getMakernoteVersionDescription(); + case TagDeviceType: + return getDeviceTypeDescription(); + case TagSamsungModelId: + return getSamsungModelIdDescription(); + + case TagCameraTemperature: + return getCameraTemperatureDescription(); + + case TagFaceDetect: + return getFaceDetectDescription(); + case TagFaceRecognition: + return getFaceRecognitionDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getMakernoteVersionDescription() + { + return getVersionBytesDescription(TagMakerNoteVersion, 2); + } + + @Nullable + public String getDeviceTypeDescription() + { + Integer value = _directory.getInteger(TagDeviceType); + if (value == null) + return null; + + switch (value) + { + case 0x1000: + return "Compact Digital Camera"; + case 0x2000: + return "High-end NX Camera"; + case 0x3000: + return "HXM Video Camera"; + case 0x12000: + return "Cell Phone"; + case 0x300000: + return "SMX Video Camera"; + default: + return String.format("Unknown (%d)", value); + } + } + + @Nullable + public String getSamsungModelIdDescription() + { + Integer value = _directory.getInteger(TagSamsungModelId); + if (value == null) + return null; + + switch (value) + { + case 0x100101c: + return "NX10"; + /*case 0x1001226: + return "HMX-S10BP";*/ + case 0x1001226: + return "HMX-S15BP"; + case 0x1001233: + return "HMX-Q10"; + /*case 0x1001234: + return "HMX-H300";*/ + case 0x1001234: + return "HMX-H304"; + case 0x100130c: + return "NX100"; + case 0x1001327: + return "NX11"; + case 0x170104e: + return "ES70, ES71 / VLUU ES70, ES71 / SL600"; + case 0x1701052: + return "ES73 / VLUU ES73 / SL605"; + case 0x1701300: + return "ES28 / VLUU ES28"; + case 0x1701303: + return "ES74,ES75,ES78 / VLUU ES75,ES78"; + case 0x2001046: + return "PL150 / VLUU PL150 / TL210 / PL151"; + case 0x2001311: + return "PL120,PL121 / VLUU PL120,PL121"; + case 0x2001315: + return "PL170,PL171 / VLUUPL170,PL171"; + case 0x200131e: + return "PL210, PL211 / VLUU PL210, PL211"; + case 0x2701317: + return "PL20,PL21 / VLUU PL20,PL21"; + case 0x2a0001b: + return "WP10 / VLUU WP10 / AQ100"; + case 0x3000000: + return "Various Models (0x3000000)"; + case 0x3a00018: + return "Various Models (0x3a00018)"; + case 0x400101f: + return "ST1000 / ST1100 / VLUU ST1000 / CL65"; + case 0x4001022: + return "ST550 / VLUU ST550 / TL225"; + case 0x4001025: + return "Various Models (0x4001025)"; + case 0x400103e: + return "VLUU ST5500, ST5500, CL80"; + case 0x4001041: + return "VLUU ST5000, ST5000, TL240"; + case 0x4001043: + return "ST70 / VLUU ST70 / ST71"; + case 0x400130a: + return "Various Models (0x400130a)"; + case 0x400130e: + return "ST90,ST91 / VLUU ST90,ST91"; + case 0x4001313: + return "VLUU ST95, ST95"; + case 0x4a00015: + return "VLUU ST60"; + case 0x4a0135b: + return "ST30, ST65 / VLUU ST65 / ST67"; + case 0x5000000: + return "Various Models (0x5000000)"; + case 0x5001038: + return "Various Models (0x5001038)"; + case 0x500103a: + return "WB650 / VLUU WB650 / WB660"; + case 0x500103c: + return "WB600 / VLUU WB600 / WB610"; + case 0x500133e: + return "WB150 / WB150F / WB152 / WB152F / WB151"; + case 0x5a0000f: + return "WB5000 / HZ25W"; + case 0x6001036: + return "EX1"; + case 0x700131c: + return "VLUU SH100, SH100"; + case 0x27127002: + return "SMX - C20N"; + default: + return String.format("Unknown (%d)", value); + } + } + + @Nullable + private String getCameraTemperatureDescription() + { + return getFormattedInt(TagCameraTemperature, "%d C"); + } + + @Nullable + public String getFaceDetectDescription() + { + return getIndexedDescription(TagFaceDetect, + "Off", "On"); + } + + @Nullable + public String getFaceRecognitionDescription() + { + return getIndexedDescription(TagFaceRecognition, + "Off", "On"); + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java new file mode 100644 index 0000000..5af173c --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/SamsungType2MakernoteDirectory.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.exif.makernotes; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * Describes tags specific certain 'newer' Samsung cameras. + * <p> + * Tag reference from: http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Samsung.html + * + * @author Kevin Mott https://github.com/kwhopper + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class SamsungType2MakernoteDirectory extends Directory +{ + // This list is incomplete + public static final int TagMakerNoteVersion = 0x001; + public static final int TagDeviceType = 0x0002; + public static final int TagSamsungModelId = 0x0003; + + public static final int TagCameraTemperature = 0x0043; + + public static final int TagFaceDetect = 0x0100; + public static final int TagFaceRecognition = 0x0120; + public static final int TagFaceName = 0x0123; + + // following tags found only in SRW images + public static final int TagFirmwareName = 0xa001; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TagMakerNoteVersion, "Maker Note Version"); + _tagNameMap.put(TagDeviceType, "Device Type"); + _tagNameMap.put(TagSamsungModelId, "Model Id"); + + _tagNameMap.put(TagCameraTemperature, "Camera Temperature"); + + _tagNameMap.put(TagFaceDetect, "Face Detect"); + _tagNameMap.put(TagFaceRecognition, "Face Recognition"); + _tagNameMap.put(TagFaceName, "Face Name"); + _tagNameMap.put(TagFirmwareName, "Firmware Name"); + } + + public SamsungType2MakernoteDirectory() + { + this.setDescriptor(new SamsungType2MakernoteDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Samsung Makernote"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java index 22db4fe..3367fa1 100644 --- a/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import static com.drew.metadata.exif.makernotes.SanyoMakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class SanyoMakernoteDescriptor extends TagDescriptor<SanyoMakernoteDirectory> { public SanyoMakernoteDescriptor(@NotNull SanyoMakernoteDirectory directory) diff --git a/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java index 26500c6..e22d1fb 100644 --- a/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/SanyoMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class SanyoMakernoteDirectory extends Directory { public static final int TAG_MAKERNOTE_OFFSET = 0x00ff; @@ -61,7 +62,7 @@ public class SanyoMakernoteDirectory extends Directory public static final int TAG_SEQUENCE_SHOT_INTERVAL = 0x0224; public static final int TAG_FLASH_MODE = 0x0225; - public static final int TAG_PRINT_IM = 0x0e00; + public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00; public static final int TAG_DATA_DUMP = 0x0f00; @@ -98,7 +99,7 @@ public class SanyoMakernoteDirectory extends Directory _tagNameMap.put(TAG_SEQUENCE_SHOT_INTERVAL, "Sequence Shot Interval"); _tagNameMap.put(TAG_FLASH_MODE, "Flash Mode"); - _tagNameMap.put(TAG_PRINT_IM, "Print IM"); + _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print IM"); _tagNameMap.put(TAG_DATA_DUMP, "Data Dump"); } diff --git a/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java index 56442f1..3111378 100644 --- a/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import static com.drew.metadata.exif.makernotes.SigmaMakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class SigmaMakernoteDescriptor extends TagDescriptor<SigmaMakernoteDirectory> { public SigmaMakernoteDescriptor(@NotNull SigmaMakernoteDirectory directory) diff --git a/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java index 75afedf..c62b002 100644 --- a/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/SigmaMakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class SigmaMakernoteDirectory extends Directory { public static final int TAG_SERIAL_NUMBER = 0x2; diff --git a/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java index 8ae873f..12e383b 100644 --- a/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import static com.drew.metadata.exif.makernotes.SonyType1MakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class SonyType1MakernoteDescriptor extends TagDescriptor<SonyType1MakernoteDirectory> { public SonyType1MakernoteDescriptor(@NotNull SonyType1MakernoteDirectory directory) @@ -432,19 +433,43 @@ public class SonyType1MakernoteDescriptor extends TagDescriptor<SonyType1Makerno @Nullable public String getVignettingCorrectionDescription() { - return getIndexedDescription(TAG_VIGNETTING_CORRECTION, "Off", null, "Auto"); + Integer value = _directory.getInteger(TAG_VIGNETTING_CORRECTION); + if (value == null) + return null; + switch (value) { + case 0: return "Off"; + case 2: return "Auto"; + case 0xffffffff: return "N/A"; + default: return String.format("Unknown (%d)", value); + } } @Nullable public String getLateralChromaticAberrationDescription() { - return getIndexedDescription(TAG_LATERAL_CHROMATIC_ABERRATION, "Off", null, "Auto"); + Integer value = _directory.getInteger(TAG_LATERAL_CHROMATIC_ABERRATION); + if (value == null) + return null; + switch (value) { + case 0: return "Off"; + case 2: return "Auto"; + case 0xffffffff: return "N/A"; + default: return String.format("Unknown (%d)", value); + } } @Nullable public String getDistortionCorrectionDescription() { - return getIndexedDescription(TAG_DISTORTION_CORRECTION, "Off", null, "Auto"); + Integer value = _directory.getInteger(TAG_DISTORTION_CORRECTION); + if (value == null) + return null; + switch (value) { + case 0: return "Off"; + case 2: return "Auto"; + case 0xffffffff: return "N/A"; + default: return String.format("Unknown (%d)", value); + } } @Nullable diff --git a/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java index ef5ce0d..2a23ffb 100644 --- a/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/SonyType1MakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class SonyType1MakernoteDirectory extends Directory { public static final int TAG_CAMERA_INFO = 0x0010; @@ -140,7 +141,7 @@ public class SonyType1MakernoteDirectory extends Directory _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance"); _tagNameMap.put(TAG_EXTRA_INFO, "Extra Info"); - _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching Info"); + _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info"); _tagNameMap.put(TAG_MULTI_BURST_MODE, "Multi Burst Mode"); _tagNameMap.put(TAG_MULTI_BURST_IMAGE_WIDTH, "Multi Burst Image Width"); diff --git a/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java b/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java index 8175dec..c08f33a 100644 --- a/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java +++ b/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import static com.drew.metadata.exif.makernotes.SonyType6MakernoteDirectory.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class SonyType6MakernoteDescriptor extends TagDescriptor<SonyType6MakernoteDirectory> { public SonyType6MakernoteDescriptor(@NotNull SonyType6MakernoteDirectory directory) diff --git a/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java b/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java index a68d81e..90690a1 100644 --- a/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java +++ b/Source/com/drew/metadata/exif/makernotes/SonyType6MakernoteDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class SonyType6MakernoteDirectory extends Directory { public static final int TAG_MAKERNOTE_THUMB_OFFSET = 0x0513; diff --git a/Source/com/drew/metadata/exif/makernotes/package-info.java b/Source/com/drew/metadata/exif/makernotes/package-info.java new file mode 100644 index 0000000..a3bc025 --- /dev/null +++ b/Source/com/drew/metadata/exif/makernotes/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains {@link com.drew.metadata.Directory} and {@link com.drew.metadata.TagDescriptor} classes related to the + * modelling of manufacturer-specific makernotes. + */ +package com.drew.metadata.exif.makernotes; diff --git a/Source/com/drew/metadata/exif/makernotes/package.html b/Source/com/drew/metadata/exif/makernotes/package.html deleted file mode 100644 index 7115326..0000000 --- a/Source/com/drew/metadata/exif/makernotes/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains {@link com.drew.metadata.Directory} and {@link com.drew.metadata.TagDescriptor} classes related to the modelling of manufacturer-specific makernotes. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/metadata/exif/package-info.java b/Source/com/drew/metadata/exif/package-info.java new file mode 100644 index 0000000..707b8d6 --- /dev/null +++ b/Source/com/drew/metadata/exif/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of Exif metadata and camera manufacturer-specific makernotes. + */ +package com.drew.metadata.exif; diff --git a/Source/com/drew/metadata/exif/package.html b/Source/com/drew/metadata/exif/package.html deleted file mode 100644 index 0ec6f41..0000000 --- a/Source/com/drew/metadata/exif/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of Exif metadata and camera manufacturer-specific makernotes. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/metadata/file/FileMetadataDescriptor.java b/Source/com/drew/metadata/file/FileMetadataDescriptor.java new file mode 100644 index 0000000..601ea7d --- /dev/null +++ b/Source/com/drew/metadata/file/FileMetadataDescriptor.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.file; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.file.FileMetadataDirectory.*; + +/** + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class FileMetadataDescriptor extends TagDescriptor<FileMetadataDirectory> +{ + public FileMetadataDescriptor(@NotNull FileMetadataDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TAG_FILE_SIZE: + return getFileSizeDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + private String getFileSizeDescription() + { + Long size = _directory.getLongObject(TAG_FILE_SIZE); + + if (size == null) + return null; + + return Long.toString(size) + " bytes"; + } +} + diff --git a/Source/com/drew/metadata/file/FileMetadataDirectory.java b/Source/com/drew/metadata/file/FileMetadataDirectory.java new file mode 100644 index 0000000..f01d832 --- /dev/null +++ b/Source/com/drew/metadata/file/FileMetadataDirectory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.file; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class FileMetadataDirectory extends Directory +{ + public static final int TAG_FILE_NAME = 1; + public static final int TAG_FILE_SIZE = 2; + public static final int TAG_FILE_MODIFIED_DATE = 3; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TAG_FILE_NAME, "File Name"); + _tagNameMap.put(TAG_FILE_SIZE, "File Size"); + _tagNameMap.put(TAG_FILE_MODIFIED_DATE, "File Modified Date"); + } + + public FileMetadataDirectory() + { + this.setDescriptor(new FileMetadataDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "File"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/file/FileMetadataReader.java b/Source/com/drew/metadata/file/FileMetadataReader.java new file mode 100644 index 0000000..8cb5135 --- /dev/null +++ b/Source/com/drew/metadata/file/FileMetadataReader.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.file; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; + +import java.io.File; +import java.io.IOException; +import java.util.Date; + +public class FileMetadataReader +{ + public void read(@NotNull File file, @NotNull Metadata metadata) throws IOException + { + if (!file.isFile()) + throw new IOException("File object must reference a file"); + if (!file.exists()) + throw new IOException("File does not exist"); + if (!file.canRead()) + throw new IOException("File is not readable"); + + FileMetadataDirectory directory = new FileMetadataDirectory(); + + directory.setString(FileMetadataDirectory.TAG_FILE_NAME, file.getName()); + directory.setLong(FileMetadataDirectory.TAG_FILE_SIZE, file.length()); + directory.setDate(FileMetadataDirectory.TAG_FILE_MODIFIED_DATE, new Date(file.lastModified())); + + metadata.addDirectory(directory); + } +} diff --git a/Source/com/drew/metadata/file/package-info.java b/Source/com/drew/metadata/file/package-info.java new file mode 100644 index 0000000..d8c0b47 --- /dev/null +++ b/Source/com/drew/metadata/file/package-info.java @@ -0,0 +1,6 @@ +/** + * Contains classes for the extraction and modelling of file system metadata. + * + * @since 2.8.0 + */ +package com.drew.metadata.file; diff --git a/Source/com/drew/metadata/gif/GifAnimationDescriptor.java b/Source/com/drew/metadata/gif/GifAnimationDescriptor.java new file mode 100644 index 0000000..69315ec --- /dev/null +++ b/Source/com/drew/metadata/gif/GifAnimationDescriptor.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.gif.GifAnimationDirectory.*; + +/** + * @author Drew Noakes https://drewnoakes.com + * @author Kevin Mott https://github.com/kwhopper + */ +@SuppressWarnings("WeakerAccess") +public class GifAnimationDescriptor extends TagDescriptor<GifAnimationDirectory> +{ + public GifAnimationDescriptor(@NotNull GifAnimationDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TAG_ITERATION_COUNT: + return getIterationCountDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getIterationCountDescription() + { + Integer count = _directory.getInteger(TAG_ITERATION_COUNT); + if (count == null) + return null; + + return count == 0 ? "Infinite" : count == 1 ? "Once" : count == 2 ? "Twice" : count.toString() + " times"; + } +} diff --git a/Source/com/drew/metadata/gif/GifAnimationDirectory.java b/Source/com/drew/metadata/gif/GifAnimationDirectory.java new file mode 100644 index 0000000..7e3734c --- /dev/null +++ b/Source/com/drew/metadata/gif/GifAnimationDirectory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * @author Drew Noakes https://drewnoakes.com + * @author Kevin Mott https://github.com/kwhopper + */ +@SuppressWarnings("WeakerAccess") +public class GifAnimationDirectory extends Directory +{ + public static final int TAG_ITERATION_COUNT = 1; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TAG_ITERATION_COUNT, "Iteration Count"); + } + + public GifAnimationDirectory() + { + this.setDescriptor(new GifAnimationDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "GIF Animation"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/gif/GifCommentDescriptor.java b/Source/com/drew/metadata/gif/GifCommentDescriptor.java new file mode 100644 index 0000000..06f2028 --- /dev/null +++ b/Source/com/drew/metadata/gif/GifCommentDescriptor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.TagDescriptor; + +/** + * @author Drew Noakes https://drewnoakes.com + * @author Kevin Mott https://github.com/kwhopper + */ +@SuppressWarnings("WeakerAccess") +public class GifCommentDescriptor extends TagDescriptor<GifCommentDirectory> +{ + public GifCommentDescriptor(@NotNull GifCommentDirectory directory) + { + super(directory); + } +} diff --git a/Source/com/drew/metadata/gif/GifCommentDirectory.java b/Source/com/drew/metadata/gif/GifCommentDirectory.java new file mode 100644 index 0000000..eaa4b3e --- /dev/null +++ b/Source/com/drew/metadata/gif/GifCommentDirectory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.StringValue; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * @author Drew Noakes https://drewnoakes.com + * @author Kevin Mott https://github.com/kwhopper + */ +@SuppressWarnings("WeakerAccess") +public class GifCommentDirectory extends Directory +{ + public static final int TAG_COMMENT = 1; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TAG_COMMENT, "Comment"); + } + + public GifCommentDirectory(StringValue comment) + { + this.setDescriptor(new GifCommentDescriptor(this)); + setStringValue(TAG_COMMENT, comment); + } + + @Override + @NotNull + public String getName() + { + return "GIF Comment"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/gif/GifControlDescriptor.java b/Source/com/drew/metadata/gif/GifControlDescriptor.java new file mode 100644 index 0000000..8e4cf15 --- /dev/null +++ b/Source/com/drew/metadata/gif/GifControlDescriptor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.TagDescriptor; + +/** + * @author Drew Noakes https://drewnoakes.com + * @author Kevin Mott https://github.com/kwhopper + */ +@SuppressWarnings("WeakerAccess") +public class GifControlDescriptor extends TagDescriptor<GifControlDirectory> +{ + public GifControlDescriptor(@NotNull GifControlDirectory directory) + { + super(directory); + } +} diff --git a/Source/com/drew/metadata/gif/GifControlDirectory.java b/Source/com/drew/metadata/gif/GifControlDirectory.java new file mode 100644 index 0000000..5b98f5c --- /dev/null +++ b/Source/com/drew/metadata/gif/GifControlDirectory.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * @author Drew Noakes https://drewnoakes.com + * @author Kevin Mott https://github.com/kwhopper + */ +@SuppressWarnings("WeakerAccess") +public class GifControlDirectory extends Directory +{ + public static final int TAG_DELAY = 1; + public static final int TAG_DISPOSAL_METHOD = 2; + public static final int TAG_USER_INPUT_FLAG = 3; + public static final int TAG_TRANSPARENT_COLOR_FLAG = 4; + public static final int TAG_TRANSPARENT_COLOR_INDEX = 5; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TAG_DELAY, "Delay"); + _tagNameMap.put(TAG_DISPOSAL_METHOD, "Disposal Method"); + _tagNameMap.put(TAG_USER_INPUT_FLAG, "User Input Flag"); + _tagNameMap.put(TAG_TRANSPARENT_COLOR_FLAG, "Transparent Color Flag"); + _tagNameMap.put(TAG_TRANSPARENT_COLOR_INDEX, "Transparent Color Index"); + } + + public GifControlDirectory() + { + this.setDescriptor(new GifControlDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "GIF Control"; + } + + /** + * @return The {@link DisposalMethod}. + */ + @NotNull + public DisposalMethod getDisposalMethod() { + return (DisposalMethod) getObject(TAG_DISPOSAL_METHOD); + } + + /** + * @return Whether the GIF has transparency. + */ + public boolean isTransparent() { + Boolean transparent = getBooleanObject(TAG_TRANSPARENT_COLOR_FLAG); + return transparent != null ? transparent.booleanValue() : false; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } + + /** + * Disposal method indicates the way in which the graphic is to be treated + * after being displayed. + */ + public enum DisposalMethod { + NOT_SPECIFIED, + DO_NOT_DISPOSE, + RESTORE_TO_BACKGROUND_COLOR, + RESTORE_TO_PREVIOUS, + TO_BE_DEFINED, + INVALID; + + public static DisposalMethod typeOf(int value) { + switch (value) { + case 0: return NOT_SPECIFIED; + case 1: return DO_NOT_DISPOSE; + case 2: return RESTORE_TO_BACKGROUND_COLOR; + case 3: return RESTORE_TO_PREVIOUS; + case 4: + case 5: + case 6: + case 7: return TO_BE_DEFINED; + default: return INVALID; + } + } + + @Override + public String toString() { + switch (this) { + case DO_NOT_DISPOSE: + return "Don't Dispose"; + case INVALID: + return "Invalid value"; + case NOT_SPECIFIED: + return "Not Specified"; + case RESTORE_TO_BACKGROUND_COLOR: + return "Restore to Background Color"; + case RESTORE_TO_PREVIOUS: + return "Restore to Previous"; + case TO_BE_DEFINED: + return "To Be Defined"; + default: + return super.toString(); + } + } + } +} diff --git a/Source/com/drew/metadata/gif/GifHeaderDescriptor.java b/Source/com/drew/metadata/gif/GifHeaderDescriptor.java index b32abbe..c5fb5c0 100644 --- a/Source/com/drew/metadata/gif/GifHeaderDescriptor.java +++ b/Source/com/drew/metadata/gif/GifHeaderDescriptor.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; import com.drew.lang.annotations.NotNull; @@ -6,6 +26,7 @@ import com.drew.metadata.TagDescriptor; /** * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class GifHeaderDescriptor extends TagDescriptor<GifHeaderDirectory> { public GifHeaderDescriptor(@NotNull GifHeaderDirectory directory) diff --git a/Source/com/drew/metadata/gif/GifHeaderDirectory.java b/Source/com/drew/metadata/gif/GifHeaderDirectory.java index b0e436e..ec4bc80 100644 --- a/Source/com/drew/metadata/gif/GifHeaderDirectory.java +++ b/Source/com/drew/metadata/gif/GifHeaderDirectory.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; import com.drew.lang.annotations.NotNull; @@ -8,6 +28,7 @@ import java.util.HashMap; /** * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class GifHeaderDirectory extends Directory { public static final int TAG_GIF_FORMAT_VERSION = 1; @@ -17,7 +38,12 @@ public class GifHeaderDirectory extends Directory public static final int TAG_IS_COLOR_TABLE_SORTED = 5; public static final int TAG_BITS_PER_PIXEL = 6; public static final int TAG_HAS_GLOBAL_COLOR_TABLE = 7; + /** + * @deprecated use {@link #TAG_BACKGROUND_COLOR_INDEX} instead. + */ + @Deprecated public static final int TAG_TRANSPARENT_COLOR_INDEX = 8; + public static final int TAG_BACKGROUND_COLOR_INDEX = 8; public static final int TAG_PIXEL_ASPECT_RATIO = 9; @NotNull @@ -31,7 +57,7 @@ public class GifHeaderDirectory extends Directory _tagNameMap.put(TAG_IS_COLOR_TABLE_SORTED, "Is Color Table Sorted"); _tagNameMap.put(TAG_BITS_PER_PIXEL, "Bits per Pixel"); _tagNameMap.put(TAG_HAS_GLOBAL_COLOR_TABLE, "Has Global Color Table"); - _tagNameMap.put(TAG_TRANSPARENT_COLOR_INDEX, "Transparent Color Index"); + _tagNameMap.put(TAG_BACKGROUND_COLOR_INDEX, "Background Color Index"); _tagNameMap.put(TAG_PIXEL_ASPECT_RATIO, "Pixel Aspect Ratio"); } diff --git a/Source/com/drew/metadata/gif/GifImageDescriptor.java b/Source/com/drew/metadata/gif/GifImageDescriptor.java new file mode 100644 index 0000000..7fff4bf --- /dev/null +++ b/Source/com/drew/metadata/gif/GifImageDescriptor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.TagDescriptor; + +/** + * @author Drew Noakes https://drewnoakes.com + * @author Kevin Mott https://github.com/kwhopper + */ +@SuppressWarnings("WeakerAccess") +public class GifImageDescriptor extends TagDescriptor<GifImageDirectory> +{ + public GifImageDescriptor(@NotNull GifImageDirectory directory) + { + super(directory); + } +} diff --git a/Source/com/drew/metadata/gif/GifImageDirectory.java b/Source/com/drew/metadata/gif/GifImageDirectory.java new file mode 100644 index 0000000..bffa2ff --- /dev/null +++ b/Source/com/drew/metadata/gif/GifImageDirectory.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * @author Drew Noakes https://drewnoakes.com + * @author Kevin Mott https://github.com/kwhopper + */ +@SuppressWarnings("WeakerAccess") +public class GifImageDirectory extends Directory +{ + public static final int TAG_LEFT = 1; + public static final int TAG_TOP = 2; + public static final int TAG_WIDTH = 3; + public static final int TAG_HEIGHT = 4; + public static final int TAG_HAS_LOCAL_COLOUR_TABLE = 5; + public static final int TAG_IS_INTERLACED = 6; + public static final int TAG_IS_COLOR_TABLE_SORTED = 7; + public static final int TAG_LOCAL_COLOUR_TABLE_BITS_PER_PIXEL = 8; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TAG_LEFT, "Left"); + _tagNameMap.put(TAG_TOP, "Top"); + _tagNameMap.put(TAG_WIDTH, "Width"); + _tagNameMap.put(TAG_HEIGHT, "Height"); + _tagNameMap.put(TAG_HAS_LOCAL_COLOUR_TABLE, "Has Local Colour Table"); + _tagNameMap.put(TAG_IS_INTERLACED, "Is Interlaced"); + _tagNameMap.put(TAG_IS_COLOR_TABLE_SORTED, "Is Local Colour Table Sorted"); + _tagNameMap.put(TAG_LOCAL_COLOUR_TABLE_BITS_PER_PIXEL, "Local Colour Table Bits Per Pixel"); + } + + public GifImageDirectory() + { + this.setDescriptor(new GifImageDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "GIF Image"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/gif/GifReader.java b/Source/com/drew/metadata/gif/GifReader.java index e313bdb..5ff4ad7 100644 --- a/Source/com/drew/metadata/gif/GifReader.java +++ b/Source/com/drew/metadata/gif/GifReader.java @@ -1,13 +1,54 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.gif; +import com.drew.lang.ByteArrayReader; +import com.drew.lang.Charsets; import com.drew.lang.SequentialReader; import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; + +import com.drew.metadata.Directory; +import com.drew.metadata.ErrorDirectory; import com.drew.metadata.Metadata; +import com.drew.metadata.StringValue; +import com.drew.metadata.gif.GifControlDirectory.DisposalMethod; +import com.drew.metadata.icc.IccReader; +import com.drew.metadata.xmp.XmpReader; +import java.io.ByteArrayOutputStream; import java.io.IOException; /** + * Reader of GIF encoded data. + * + * Resources: + * <ul> + * <li>https://wiki.whatwg.org/wiki/GIF</li> + * <li>https://www.w3.org/Graphics/GIF/spec-gif89a.txt</li> + * <li>http://web.archive.org/web/20100929230301/http://www.etsimo.uniovi.es/gifanim/gif87a.txt</li> + * </ul> + * * @author Drew Noakes https://drewnoakes.com + * @author Kevin Mott https://github.com/kwhopper */ public class GifReader { @@ -16,8 +57,75 @@ public class GifReader public void extract(@NotNull final SequentialReader reader, final @NotNull Metadata metadata) { - final GifHeaderDirectory directory = metadata.getOrCreateDirectory(GifHeaderDirectory.class); + reader.setMotorolaByteOrder(false); + + GifHeaderDirectory header; + try { + header = readGifHeader(reader); + metadata.addDirectory(header); + } catch (IOException ex) { + metadata.addDirectory(new ErrorDirectory("IOException processing GIF data")); + return; + } + + if(header.hasErrors()) + return; + + try { + // Skip over any global colour table + Integer globalColorTableSize = header.getInteger(GifHeaderDirectory.TAG_COLOR_TABLE_SIZE); + if (globalColorTableSize != null) + { + // Colour table has R/G/B byte triplets + reader.skip(3 * globalColorTableSize); + } + + // After the header comes a sequence of blocks + while (true) + { + byte marker; + try { + marker = reader.getInt8(); + } catch (IOException ex) { + return; + } + switch (marker) + { + case (byte)'!': // 0x21 + { + readGifExtensionBlock(reader, metadata); + break; + } + case (byte)',': // 0x2c + { + metadata.addDirectory(readImageBlock(reader)); + + // skip image data blocks + skipBlocks(reader); + break; + } + case (byte)';': // 0x3b + { + // terminator + return; + } + default: + { + // Anything other than these types is unexpected. + // GIF87a spec says to keep reading until a separator is found. + // GIF89a spec says file is corrupt. + return; + } + } + } + } catch (IOException e) { + metadata.addDirectory(new ErrorDirectory("IOException processing GIF data")); + } + } + + private static GifHeaderDirectory readGifHeader(@NotNull final SequentialReader reader) throws IOException + { // FILE HEADER // // 3 - signature: "GIF" @@ -35,55 +143,254 @@ public class GifReader // 1 - background color index // 1 - pixel aspect ratio - reader.setMotorolaByteOrder(false); + GifHeaderDirectory headerDirectory = new GifHeaderDirectory(); - try { - String signature = reader.getString(3); + String signature = reader.getString(3); - if (!signature.equals("GIF")) - { - directory.addError("Invalid GIF file signature"); - return; - } + if (!signature.equals("GIF")) + { + headerDirectory.addError("Invalid GIF file signature"); + return headerDirectory; + } - String version = reader.getString(3); + String version = reader.getString(3); - if (!version.equals(GIF_87A_VERSION_IDENTIFIER) && !version.equals(GIF_89A_VERSION_IDENTIFIER)) { - directory.addError("Unexpected GIF version"); - return; - } + if (!version.equals(GIF_87A_VERSION_IDENTIFIER) && !version.equals(GIF_89A_VERSION_IDENTIFIER)) { + headerDirectory.addError("Unexpected GIF version"); + return headerDirectory; + } - directory.setString(GifHeaderDirectory.TAG_GIF_FORMAT_VERSION, version); - directory.setInt(GifHeaderDirectory.TAG_IMAGE_WIDTH, reader.getUInt16()); - directory.setInt(GifHeaderDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16()); + headerDirectory.setString(GifHeaderDirectory.TAG_GIF_FORMAT_VERSION, version); - short flags = reader.getUInt8(); + // LOGICAL SCREEN DESCRIPTOR - // First three bits = (BPP - 1) - int colorTableSize = 1 << ((flags & 7) + 1); - directory.setInt(GifHeaderDirectory.TAG_COLOR_TABLE_SIZE, colorTableSize); + headerDirectory.setInt(GifHeaderDirectory.TAG_IMAGE_WIDTH, reader.getUInt16()); + headerDirectory.setInt(GifHeaderDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16()); - if (version.equals(GIF_89A_VERSION_IDENTIFIER)) { - boolean isColorTableSorted = (flags & 8) != 0; - directory.setBoolean(GifHeaderDirectory.TAG_IS_COLOR_TABLE_SORTED, isColorTableSorted); - } + short flags = reader.getUInt8(); - int bitsPerPixel = ((flags & 0x70) >> 4) + 1; - directory.setInt(GifHeaderDirectory.TAG_BITS_PER_PIXEL, bitsPerPixel); + // First three bits = (BPP - 1) + int colorTableSize = 1 << ((flags & 7) + 1); + int bitsPerPixel = ((flags & 0x70) >> 4) + 1; + boolean hasGlobalColorTable = (flags & 0xf) != 0; - boolean hasGlobalColorTable = (flags & 0xf) != 0; - directory.setBoolean(GifHeaderDirectory.TAG_HAS_GLOBAL_COLOR_TABLE, hasGlobalColorTable); + headerDirectory.setInt(GifHeaderDirectory.TAG_COLOR_TABLE_SIZE, colorTableSize); - directory.setInt(GifHeaderDirectory.TAG_TRANSPARENT_COLOR_INDEX, reader.getUInt8()); + if (version.equals(GIF_89A_VERSION_IDENTIFIER)) { + boolean isColorTableSorted = (flags & 8) != 0; + headerDirectory.setBoolean(GifHeaderDirectory.TAG_IS_COLOR_TABLE_SORTED, isColorTableSorted); + } - int aspectRatioByte = reader.getUInt8(); - if (aspectRatioByte != 0) { - float pixelAspectRatio = (float)((aspectRatioByte + 15d) / 64d); - directory.setFloat(GifHeaderDirectory.TAG_PIXEL_ASPECT_RATIO, pixelAspectRatio); - } + headerDirectory.setInt(GifHeaderDirectory.TAG_BITS_PER_PIXEL, bitsPerPixel); + headerDirectory.setBoolean(GifHeaderDirectory.TAG_HAS_GLOBAL_COLOR_TABLE, hasGlobalColorTable); - } catch (IOException e) { - directory.addError("Unable to read BMP header"); + headerDirectory.setInt(GifHeaderDirectory.TAG_BACKGROUND_COLOR_INDEX, reader.getUInt8()); + + int aspectRatioByte = reader.getUInt8(); + if (aspectRatioByte != 0) { + float pixelAspectRatio = (float)((aspectRatioByte + 15d) / 64d); + headerDirectory.setFloat(GifHeaderDirectory.TAG_PIXEL_ASPECT_RATIO, pixelAspectRatio); + } + + return headerDirectory; + } + + private static void readGifExtensionBlock(SequentialReader reader, Metadata metadata) throws IOException + { + byte extensionLabel = reader.getInt8(); + short blockSizeBytes = reader.getUInt8(); + long blockStartPos = reader.getPosition(); + + switch (extensionLabel) + { + case (byte) 0x01: + Directory plainTextBlock = readPlainTextBlock(reader, blockSizeBytes); + if (plainTextBlock != null) + metadata.addDirectory(plainTextBlock); + break; + case (byte) 0xf9: + metadata.addDirectory(readControlBlock(reader, blockSizeBytes)); + break; + case (byte) 0xfe: + metadata.addDirectory(readCommentBlock(reader, blockSizeBytes)); + break; + case (byte) 0xff: + readApplicationExtensionBlock(reader, blockSizeBytes, metadata); + break; + default: + metadata.addDirectory(new ErrorDirectory(String.format("Unsupported GIF extension block with type 0x%02X.", extensionLabel))); + break; + } + + long skipCount = blockStartPos + blockSizeBytes - reader.getPosition(); + if (skipCount > 0) + reader.skip(skipCount); + } + + @Nullable + private static Directory readPlainTextBlock(SequentialReader reader, int blockSizeBytes) throws IOException + { + // It seems this extension is deprecated. If somebody finds an image with this in it, could implement here. + // Just skip the entire block for now. + + if (blockSizeBytes != 12) + return new ErrorDirectory(String.format("Invalid GIF plain text block size. Expected 12, got %d.", blockSizeBytes)); + + // skip 'blockSizeBytes' bytes + reader.skip(12); + + // keep reading and skipping until a 0 byte is reached + skipBlocks(reader); + + return null; + } + + private static GifCommentDirectory readCommentBlock(SequentialReader reader, int blockSizeBytes) throws IOException + { + byte[] buffer = gatherBytes(reader, blockSizeBytes); + return new GifCommentDirectory(new StringValue(buffer, Charsets.ASCII)); + } + + private static void readApplicationExtensionBlock(SequentialReader reader, int blockSizeBytes, Metadata metadata) throws IOException + { + if (blockSizeBytes != 11) + { + metadata.addDirectory(new ErrorDirectory(String.format("Invalid GIF application extension block size. Expected 11, got %d.", blockSizeBytes))); + return; + } + + String extensionType = reader.getString(blockSizeBytes, Charsets.UTF_8); + + if (extensionType.equals("XMP DataXMP")) + { + // XMP data extension + byte[] xmpBytes = gatherBytes(reader); + new XmpReader().extract(xmpBytes, 0, xmpBytes.length - 257, metadata, null); + } + else if (extensionType.equals("ICCRGBG1012")) + { + // ICC profile extension + byte[] iccBytes = gatherBytes(reader, reader.getByte()); + if (iccBytes.length != 0) + new IccReader().extract(new ByteArrayReader(iccBytes), metadata); + } + else if (extensionType.equals("NETSCAPE2.0")) + { + reader.skip(2); + // Netscape's animated GIF extension + // Iteration count (0 means infinite) + int iterationCount = reader.getUInt16(); + // Skip terminator + reader.skip(1); + GifAnimationDirectory animationDirectory = new GifAnimationDirectory(); + animationDirectory.setInt(GifAnimationDirectory.TAG_ITERATION_COUNT, iterationCount); + metadata.addDirectory(animationDirectory); + } + else + { + skipBlocks(reader); + } + } + + private static GifControlDirectory readControlBlock(SequentialReader reader, int blockSizeBytes) throws IOException + { + if (blockSizeBytes < 4) + blockSizeBytes = 4; + + GifControlDirectory directory = new GifControlDirectory(); + + short packedFields = reader.getUInt8(); + directory.setObject(GifControlDirectory.TAG_DISPOSAL_METHOD, DisposalMethod.typeOf((packedFields >> 2) & 7)); + directory.setBoolean(GifControlDirectory.TAG_USER_INPUT_FLAG, (packedFields & 2) >> 1 == 1 ? true : false); + directory.setBoolean(GifControlDirectory.TAG_TRANSPARENT_COLOR_FLAG, (packedFields & 1) == 1 ? true : false); + directory.setInt(GifControlDirectory.TAG_DELAY, reader.getUInt16()); + directory.setInt(GifControlDirectory.TAG_TRANSPARENT_COLOR_INDEX, reader.getUInt8()); + + // skip 0x0 block terminator + reader.skip(1); + + return directory; + } + + private static GifImageDirectory readImageBlock(SequentialReader reader) throws IOException + { + GifImageDirectory imageDirectory = new GifImageDirectory(); + + imageDirectory.setInt(GifImageDirectory.TAG_LEFT, reader.getUInt16()); + imageDirectory.setInt(GifImageDirectory.TAG_TOP, reader.getUInt16()); + imageDirectory.setInt(GifImageDirectory.TAG_WIDTH, reader.getUInt16()); + imageDirectory.setInt(GifImageDirectory.TAG_HEIGHT, reader.getUInt16()); + + byte flags = reader.getByte(); + boolean hasColorTable = (flags & 0x7) != 0; + boolean isInterlaced = (flags & 0x40) != 0; + boolean isColorTableSorted = (flags & 0x20) != 0; + + imageDirectory.setBoolean(GifImageDirectory.TAG_HAS_LOCAL_COLOUR_TABLE, hasColorTable); + imageDirectory.setBoolean(GifImageDirectory.TAG_IS_INTERLACED, isInterlaced); + + if (hasColorTable) + { + imageDirectory.setBoolean(GifImageDirectory.TAG_IS_COLOR_TABLE_SORTED, isColorTableSorted); + + int bitsPerPixel = (flags & 0x7) + 1; + imageDirectory.setInt(GifImageDirectory.TAG_LOCAL_COLOUR_TABLE_BITS_PER_PIXEL, bitsPerPixel); + + // skip color table + reader.skip(3 * (2 << (flags & 0x7))); + } + + // skip "LZW Minimum Code Size" byte + reader.getByte(); + + return imageDirectory; + } + + private static byte[] gatherBytes(SequentialReader reader) throws IOException + { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[257]; + + while (true) + { + byte b = reader.getByte(); + if (b == 0) + return bytes.toByteArray(); + + int bInt = b & 0xFF; + + buffer[0] = b; + reader.getBytes(buffer, 1, bInt); + bytes.write(buffer, 0, bInt + 1); + } + } + + private static byte[] gatherBytes(SequentialReader reader, int firstLength) throws IOException + { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + int length = firstLength; + + while (length > 0) + { + buffer.write(reader.getBytes(length), 0, length); + + length = reader.getByte(); + } + + return buffer.toByteArray(); + } + + private static void skipBlocks(SequentialReader reader) throws IOException + { + while (true) + { + short length = reader.getUInt8(); + + if (length == 0) + return; + + reader.skip(length); } } } diff --git a/Source/com/drew/metadata/gif/package-info.java b/Source/com/drew/metadata/gif/package-info.java new file mode 100644 index 0000000..b28eb9e --- /dev/null +++ b/Source/com/drew/metadata/gif/package-info.java @@ -0,0 +1,6 @@ +/** + * Contains classes for the extraction and modelling of GIF file metadata. + * + * @since 2.7.0 + */ +package com.drew.metadata.gif; diff --git a/Source/com/drew/metadata/gif/package.html b/Source/com/drew/metadata/gif/package.html deleted file mode 100644 index 5915c52..0000000 --- a/Source/com/drew/metadata/gif/package.html +++ /dev/null @@ -1,34 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of GIF file metadata. - -<!-- Put @see and @since tags down here. --> -@since 2.7.0 - -</body> -</html> diff --git a/Source/com/drew/metadata/icc/IccDescriptor.java b/Source/com/drew/metadata/icc/IccDescriptor.java index fd9544a..13363a6 100644 --- a/Source/com/drew/metadata/icc/IccDescriptor.java +++ b/Source/com/drew/metadata/icc/IccDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,11 +29,15 @@ import com.drew.metadata.TagDescriptor; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.text.DecimalFormat; + +import static com.drew.metadata.icc.IccDirectory.*; /** * @author Yuri Binev * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class IccDescriptor extends TagDescriptor<IccDirectory> { public IccDescriptor(@NotNull IccDirectory directory) @@ -45,13 +49,13 @@ public class IccDescriptor extends TagDescriptor<IccDirectory> public String getDescription(int tagType) { switch (tagType) { - case IccDirectory.TAG_PROFILE_VERSION: + case TAG_PROFILE_VERSION: return getProfileVersionDescription(); - case IccDirectory.TAG_PROFILE_CLASS: + case TAG_PROFILE_CLASS: return getProfileClassDescription(); - case IccDirectory.TAG_PLATFORM: + case TAG_PLATFORM: return getPlatformDescription(); - case IccDirectory.TAG_RENDERING_INTENT: + case TAG_RENDERING_INTENT: return getRenderingIntentDescription(); } @@ -104,10 +108,10 @@ public class IccDescriptor extends TagDescriptor<IccDirectory> observerString = "Unknown"; break; case 1: - observerString = "1931 2°"; + observerString = "1931 2\u00B0"; break; case 2: - observerString = "1964 10°"; + observerString = "1964 10\u00B0"; break; default: observerString = String.format("Unknown %d", observerType); @@ -159,11 +163,13 @@ public class IccDescriptor extends TagDescriptor<IccDirectory> illuminantString = String.format("Unknown %d", illuminantType); break; } + DecimalFormat format = new DecimalFormat("0.###"); return String.format("%s Observer, Backing (%s, %s, %s), Geometry %s, Flare %d%%, Illuminant %s", - observerString, x, y, z, geometryString, Math.round(flare * 100), illuminantString); + observerString, format.format(x), format.format(y), format.format(z), geometryString, Math.round(flare * 100), illuminantString); } case ICC_TAG_TYPE_XYZ_ARRAY: { StringBuilder res = new StringBuilder(); + DecimalFormat format = new DecimalFormat("0.####"); int count = (bytes.length - 8) / 12; for (int i = 0; i < count; i++) { float x = reader.getS15Fixed16(8 + i * 12); @@ -171,7 +177,7 @@ public class IccDescriptor extends TagDescriptor<IccDirectory> float z = reader.getS15Fixed16(8 + i * 12 + 8); if (i > 0) res.append(", "); - res.append("(").append(x).append(", ").append(y).append(", ").append(z).append(")"); + res.append("(").append(format.format(x)).append(", ").append(format.format(y)).append(", ").append(format.format(z)).append(")"); } return res.toString(); } @@ -208,7 +214,7 @@ public class IccDescriptor extends TagDescriptor<IccDirectory> return res.toString(); } default: - return String.format("%s(0x%08X): %d bytes", IccReader.getStringFromInt32(iccTagType), iccTagType, bytes.length); + return String.format("%s (0x%08X): %d bytes", IccReader.getStringFromInt32(iccTagType), iccTagType, bytes.length); } } catch (IOException e) { // TODO decode these values during IccReader.extract so we can report any errors at that time @@ -242,29 +248,17 @@ public class IccDescriptor extends TagDescriptor<IccDirectory> @Nullable private String getRenderingIntentDescription() { - Integer value = _directory.getInteger(IccDirectory.TAG_RENDERING_INTENT); - - if (value == null) - return null; - - switch (value) { - case 0: - return "Perceptual"; - case 1: - return "Media-Relative Colorimetric"; - case 2: - return "Saturation"; - case 3: - return "ICC-Absolute Colorimetric"; - default: - return String.format("Unknown (%d)", value); - } + return getIndexedDescription(TAG_RENDERING_INTENT, + "Perceptual", + "Media-Relative Colorimetric", + "Saturation", + "ICC-Absolute Colorimetric"); } @Nullable private String getPlatformDescription() { - String str = _directory.getString(IccDirectory.TAG_PLATFORM); + String str = _directory.getString(TAG_PLATFORM); if (str==null) return null; // Because Java doesn't allow switching on string values, create an integer from the first four chars @@ -294,7 +288,7 @@ public class IccDescriptor extends TagDescriptor<IccDirectory> @Nullable private String getProfileClassDescription() { - String str = _directory.getString(IccDirectory.TAG_PROFILE_CLASS); + String str = _directory.getString(TAG_PROFILE_CLASS); if (str==null) return null; // Because Java doesn't allow switching on string values, create an integer from the first four chars @@ -328,7 +322,7 @@ public class IccDescriptor extends TagDescriptor<IccDirectory> @Nullable private String getProfileVersionDescription() { - Integer value = _directory.getInteger(IccDirectory.TAG_PROFILE_VERSION); + Integer value = _directory.getInteger(TAG_PROFILE_VERSION); if (value == null) return null; diff --git a/Source/com/drew/metadata/icc/IccDirectory.java b/Source/com/drew/metadata/icc/IccDirectory.java index 06d2454..182140a 100644 --- a/Source/com/drew/metadata/icc/IccDirectory.java +++ b/Source/com/drew/metadata/icc/IccDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import java.util.HashMap; * @author Yuri Binev * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class IccDirectory extends Directory { // These (smaller valued) tags have an integer value that's equal to their offset within the ICC data buffer. diff --git a/Source/com/drew/metadata/icc/IccReader.java b/Source/com/drew/metadata/icc/IccReader.java index 3d099a8..090c9b4 100644 --- a/Source/com/drew/metadata/icc/IccReader.java +++ b/Source/com/drew/metadata/icc/IccReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,58 +23,89 @@ package com.drew.metadata.icc; import com.drew.imaging.jpeg.JpegSegmentMetadataReader; import com.drew.imaging.jpeg.JpegSegmentType; import com.drew.lang.ByteArrayReader; +import com.drew.lang.DateUtil; import com.drew.lang.RandomAccessReader; 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.MetadataReader; import java.io.IOException; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Date; -import java.util.TimeZone; +import java.util.Collections; /** * Reads an ICC profile. + * <p> + * More information about ICC: * <ul> * <li>http://en.wikipedia.org/wiki/ICC_profile</li> * <li>http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/ICC_Profile.html</li> + * <li>https://developer.apple.com/library/mac/samplecode/ImageApp/Listings/ICC_h.html</li> * </ul> * * @author Yuri Binev - * @author Drew Noakes + * @author Drew Noakes https://drewnoakes.com */ public class IccReader implements JpegSegmentMetadataReader, MetadataReader { + public static final String JPEG_SEGMENT_PREAMBLE = "ICC_PROFILE"; + @NotNull public Iterable<JpegSegmentType> getSegmentTypes() { - return Arrays.asList(JpegSegmentType.APP2); + return Collections.singletonList(JpegSegmentType.APP2); } - public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType) + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) { - return segmentBytes.length > 10 && "ICC_PROFILE".equalsIgnoreCase(new String(segmentBytes, 0, 11)); + final int preambleLength = JPEG_SEGMENT_PREAMBLE.length(); + + // ICC data can be spread across multiple JPEG segments. + // We concat them together in this buffer for later processing. + byte[] buffer = null; + + for (byte[] segmentBytes : segments) { + // Skip any segments that do not contain the required preamble + if (segmentBytes.length < preambleLength || !JPEG_SEGMENT_PREAMBLE.equalsIgnoreCase(new String(segmentBytes, 0, preambleLength))) + continue; + + // NOTE we ignore three bytes here -- are they useful for anything? + + // Grow the buffer + if (buffer == null) { + buffer = new byte[segmentBytes.length - 14]; + // skip the first 14 bytes + System.arraycopy(segmentBytes, 14, buffer, 0, segmentBytes.length - 14); + } else { + byte[] newBuffer = new byte[buffer.length + segmentBytes.length - 14]; + System.arraycopy(buffer, 0, newBuffer, 0, buffer.length); + System.arraycopy(segmentBytes, 14, newBuffer, buffer.length, segmentBytes.length - 14); + buffer = newBuffer; + } + } + + if (buffer != null) + extract(new ByteArrayReader(buffer), metadata); } - public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) + public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata) { - // skip the first 14 bytes - byte[] iccProfileBytes = new byte[segmentBytes.length - 14]; - System.arraycopy(segmentBytes, 14, iccProfileBytes, 0, segmentBytes.length - 14); - - extract(new ByteArrayReader(iccProfileBytes), metadata); + extract(reader, metadata, null); } - public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata) + public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, @Nullable Directory parentDirectory) { - // TODO review whether the 'tagPtr' values below really do require ICC processing to work with a RandomAccessReader + // TODO review whether the 'tagPtr' values below really do require RandomAccessReader or whether SequentialReader may be used instead + + IccDirectory directory = new IccDirectory(); - final IccDirectory directory = metadata.getOrCreateDirectory(IccDirectory.class); + if (parentDirectory != null) + directory.setParent(parentDirectory); try { - directory.setInt(IccDirectory.TAG_PROFILE_BYTE_COUNT, reader.getInt32(IccDirectory.TAG_PROFILE_BYTE_COUNT)); + int profileByteCount = reader.getInt32(IccDirectory.TAG_PROFILE_BYTE_COUNT); + directory.setInt(IccDirectory.TAG_PROFILE_BYTE_COUNT, profileByteCount); // For these tags, the int value of the tag is in fact it's offset within the buffer. set4ByteString(directory, IccDirectory.TAG_CMM_TYPE, reader); @@ -122,6 +153,8 @@ public class IccReader implements JpegSegmentMetadataReader, MetadataReader } catch (IOException ex) { directory.addError("Exception reading ICC profile: " + ex.getMessage()); } + + metadata.addDirectory(directory); } private void set4ByteString(@NotNull Directory directory, int tagType, @NotNull RandomAccessReader reader) throws IOException @@ -156,12 +189,17 @@ public class IccReader implements JpegSegmentMetadataReader, MetadataReader final int M = reader.getUInt16(tagType + 8); final int s = reader.getUInt16(tagType + 10); -// final Date value = new Date(Date.UTC(y - 1900, m - 1, d, h, M, s)); - final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - calendar.set(y, m, d, h, M, s); - final Date value = calendar.getTime(); - - directory.setDate(tagType, value); + if (DateUtil.isValidDate(y, m - 1, d) && DateUtil.isValidTime(h, M, s)) + { + String dateString = String.format("%04d:%02d:%02d %02d:%02d:%02d", y, m, d, h, M, s); + directory.setString(tagType, dateString); + } + else + { + directory.addError(String.format( + "ICC data describes an invalid date/time: year=%d month=%d day=%d hour=%d minute=%d second=%d", + y, m, d, h, M, s)); + } } @NotNull diff --git a/Source/com/drew/metadata/icc/package-info.java b/Source/com/drew/metadata/icc/package-info.java new file mode 100644 index 0000000..9e347d5 --- /dev/null +++ b/Source/com/drew/metadata/icc/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of ICC (International Color Consortium) profile metadata. + */ +package com.drew.metadata.icc; diff --git a/Source/com/drew/metadata/icc/package.html b/Source/com/drew/metadata/icc/package.html deleted file mode 100644 index df41803..0000000 --- a/Source/com/drew/metadata/icc/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of ICC (International Color Consortium) profile metadata. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/metadata/ico/IcoDescriptor.java b/Source/com/drew/metadata/ico/IcoDescriptor.java new file mode 100644 index 0000000..952abcc --- /dev/null +++ b/Source/com/drew/metadata/ico/IcoDescriptor.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.ico; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.ico.IcoDirectory.*; + +/** + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class IcoDescriptor extends TagDescriptor<IcoDirectory> +{ + public IcoDescriptor(@NotNull IcoDirectory directory) + { + super(directory); + } + + @Override + public String getDescription(int tagType) + { + switch (tagType) { + case TAG_IMAGE_TYPE: + return getImageTypeDescription(); + case TAG_IMAGE_WIDTH: + return getImageWidthDescription(); + case TAG_IMAGE_HEIGHT: + return getImageHeightDescription(); + case TAG_COLOUR_PALETTE_SIZE: + return getColourPaletteSizeDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getImageTypeDescription() + { + return getIndexedDescription(TAG_IMAGE_TYPE, 1, "Icon", "Cursor"); + } + + @Nullable + public String getImageWidthDescription() + { + Integer width = _directory.getInteger(TAG_IMAGE_WIDTH); + if (width == null) + return null; + return (width == 0 ? 256 : width) + " pixels"; + } + + @Nullable + public String getImageHeightDescription() + { + Integer width = _directory.getInteger(TAG_IMAGE_HEIGHT); + if (width == null) + return null; + return (width == 0 ? 256 : width) + " pixels"; + } + + @Nullable + public String getColourPaletteSizeDescription() + { + Integer size = _directory.getInteger(TAG_COLOUR_PALETTE_SIZE); + if (size == null) + return null; + return size == 0 ? "No palette" : size + " colour" + (size == 1 ? "" : "s"); + } +} diff --git a/Source/com/drew/metadata/ico/IcoDirectory.java b/Source/com/drew/metadata/ico/IcoDirectory.java new file mode 100644 index 0000000..a4a5423 --- /dev/null +++ b/Source/com/drew/metadata/ico/IcoDirectory.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.ico; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class IcoDirectory extends Directory +{ + public static final int TAG_IMAGE_TYPE = 1; + + public static final int TAG_IMAGE_WIDTH = 2; + public static final int TAG_IMAGE_HEIGHT = 3; + public static final int TAG_COLOUR_PALETTE_SIZE = 4; + public static final int TAG_COLOUR_PLANES = 5; + public static final int TAG_CURSOR_HOTSPOT_X = 6; + public static final int TAG_BITS_PER_PIXEL = 7; + public static final int TAG_CURSOR_HOTSPOT_Y = 8; + public static final int TAG_IMAGE_SIZE_BYTES = 9; + public static final int TAG_IMAGE_OFFSET_BYTES = 10; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TAG_IMAGE_TYPE, "Image Type"); + _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width"); + _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height"); + _tagNameMap.put(TAG_COLOUR_PALETTE_SIZE, "Colour Palette Size"); + _tagNameMap.put(TAG_COLOUR_PLANES, "Colour Planes"); + _tagNameMap.put(TAG_CURSOR_HOTSPOT_X, "Hotspot X"); + _tagNameMap.put(TAG_BITS_PER_PIXEL, "Bits Per Pixel"); + _tagNameMap.put(TAG_CURSOR_HOTSPOT_Y, "Hotspot Y"); + _tagNameMap.put(TAG_IMAGE_SIZE_BYTES, "Image Size Bytes"); + _tagNameMap.put(TAG_IMAGE_OFFSET_BYTES, "Image Offset Bytes"); + } + + public IcoDirectory() + { + this.setDescriptor(new IcoDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "ICO"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/ico/IcoReader.java b/Source/com/drew/metadata/ico/IcoReader.java new file mode 100644 index 0000000..49df0f2 --- /dev/null +++ b/Source/com/drew/metadata/ico/IcoReader.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.ico; + +import com.drew.lang.SequentialReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; + +import java.io.IOException; + +/** + * Reads ICO (Windows Icon) file metadata. + * <ul> + * <li>https://en.wikipedia.org/wiki/ICO_(file_format)</li> + * </ul> + * + * @author Drew Noakes https://drewnoakes.com + */ +public class IcoReader +{ + public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata) + { + reader.setMotorolaByteOrder(false); + + int type; + int imageCount; + + // Read header (ICONDIR structure) + try { + int reserved = reader.getUInt16(); + + if (reserved != 0) { + IcoDirectory directory = new IcoDirectory(); + directory.addError("Invalid header bytes"); + metadata.addDirectory(directory); + return; + } + + type = reader.getUInt16(); + + if (type != 1 && type != 2) { + IcoDirectory directory = new IcoDirectory(); + directory.addError("Invalid type " + type + " -- expecting 1 or 2"); + metadata.addDirectory(directory); + return; + } + + imageCount = reader.getUInt16(); + + if (imageCount == 0) { + IcoDirectory directory = new IcoDirectory(); + directory.addError("Image count cannot be zero"); + metadata.addDirectory(directory); + return; + } + + } catch (IOException ex) { + IcoDirectory directory = new IcoDirectory(); + directory.addError("Exception reading ICO file metadata: " + ex.getMessage()); + metadata.addDirectory(directory); + return; + } + + // Read each embedded image + for (int imageIndex = 0; imageIndex < imageCount; imageIndex++) { + IcoDirectory directory = new IcoDirectory(); + try { + directory.setInt(IcoDirectory.TAG_IMAGE_TYPE, type); + + directory.setInt(IcoDirectory.TAG_IMAGE_WIDTH, reader.getUInt8()); + directory.setInt(IcoDirectory.TAG_IMAGE_HEIGHT, reader.getUInt8()); + directory.setInt(IcoDirectory.TAG_COLOUR_PALETTE_SIZE, reader.getUInt8()); + // Ignore this byte (normally zero, though .NET's System.Drawing.Icon.Save method writes 255) + reader.getUInt8(); + if (type == 1) { + // Icon + directory.setInt(IcoDirectory.TAG_COLOUR_PLANES, reader.getUInt16()); + directory.setInt(IcoDirectory.TAG_BITS_PER_PIXEL, reader.getUInt16()); + } else { + // Cursor + directory.setInt(IcoDirectory.TAG_CURSOR_HOTSPOT_X, reader.getUInt16()); + directory.setInt(IcoDirectory.TAG_CURSOR_HOTSPOT_Y, reader.getUInt16()); + } + directory.setLong(IcoDirectory.TAG_IMAGE_SIZE_BYTES, reader.getUInt32()); + directory.setLong(IcoDirectory.TAG_IMAGE_OFFSET_BYTES, reader.getUInt32()); + } catch (IOException ex) { + directory.addError("Exception reading ICO file metadata: " + ex.getMessage()); + } + metadata.addDirectory(directory); + } + } +} diff --git a/Source/com/drew/metadata/ico/package-info.java b/Source/com/drew/metadata/ico/package-info.java new file mode 100644 index 0000000..9f1b6e9 --- /dev/null +++ b/Source/com/drew/metadata/ico/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of ICO (Windows Icon) file metadata. + */ +package com.drew.metadata.ico; diff --git a/Source/com/drew/metadata/iptc/IptcDescriptor.java b/Source/com/drew/metadata/iptc/IptcDescriptor.java index 25189a8..468dda5 100644 --- a/Source/com/drew/metadata/iptc/IptcDescriptor.java +++ b/Source/com/drew/metadata/iptc/IptcDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; +import static com.drew.metadata.iptc.IptcDirectory.*; + /** * Provides human-readable string representations of tag values stored in a {@link IptcDirectory}. * <p> @@ -32,6 +34,7 @@ import com.drew.metadata.TagDescriptor; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class IptcDescriptor extends TagDescriptor<IptcDirectory> { public IptcDescriptor(@NotNull IptcDirectory directory) @@ -44,19 +47,63 @@ public class IptcDescriptor extends TagDescriptor<IptcDirectory> public String getDescription(int tagType) { switch (tagType) { - case IptcDirectory.TAG_FILE_FORMAT: + case TAG_DATE_CREATED: + return getDateCreatedDescription(); + case TAG_DIGITAL_DATE_CREATED: + return getDigitalDateCreatedDescription(); + case TAG_DATE_SENT: + return getDateSentDescription(); + case TAG_EXPIRATION_DATE: + return getExpirationDateDescription(); + case TAG_EXPIRATION_TIME: + return getExpirationTimeDescription(); + case TAG_FILE_FORMAT: return getFileFormatDescription(); - case IptcDirectory.TAG_KEYWORDS: + case TAG_KEYWORDS: return getKeywordsDescription(); + case TAG_REFERENCE_DATE: + return getReferenceDateDescription(); + case TAG_RELEASE_DATE: + return getReleaseDateDescription(); + case TAG_RELEASE_TIME: + return getReleaseTimeDescription(); + case TAG_TIME_CREATED: + return getTimeCreatedDescription(); + case TAG_DIGITAL_TIME_CREATED: + return getDigitalTimeCreatedDescription(); + case TAG_TIME_SENT: + return getTimeSentDescription(); default: return super.getDescription(tagType); } } + @Nullable + public String getDateDescription(int tagType) + { + String s = _directory.getString(tagType); + if (s == null) + return null; + if (s.length() == 8) + return s.substring(0, 4) + ':' + s.substring(4, 6) + ':' + s.substring(6); + return s; + } + + @Nullable + public String getTimeDescription(int tagType) + { + String s = _directory.getString(tagType); + if (s == null) + return null; + if (s.length() == 6 || s.length() == 11) + return s.substring(0, 2) + ':' + s.substring(2, 4) + ':' + s.substring(4); + return s; + } + @Nullable public String getFileFormatDescription() { - Integer value = _directory.getInteger(IptcDirectory.TAG_FILE_FORMAT); + Integer value = _directory.getInteger(TAG_FILE_FORMAT); if (value == null) return null; switch (value) { @@ -97,67 +144,91 @@ public class IptcDescriptor extends TagDescriptor<IptcDirectory> @Nullable public String getByLineDescription() { - return _directory.getString(IptcDirectory.TAG_BY_LINE); + return _directory.getString(TAG_BY_LINE); } @Nullable public String getByLineTitleDescription() { - return _directory.getString(IptcDirectory.TAG_BY_LINE_TITLE); + return _directory.getString(TAG_BY_LINE_TITLE); } @Nullable public String getCaptionDescription() { - return _directory.getString(IptcDirectory.TAG_CAPTION); + return _directory.getString(TAG_CAPTION); } @Nullable public String getCategoryDescription() { - return _directory.getString(IptcDirectory.TAG_CATEGORY); + return _directory.getString(TAG_CATEGORY); } @Nullable public String getCityDescription() { - return _directory.getString(IptcDirectory.TAG_CITY); + return _directory.getString(TAG_CITY); } @Nullable public String getCopyrightNoticeDescription() { - return _directory.getString(IptcDirectory.TAG_COPYRIGHT_NOTICE); + return _directory.getString(TAG_COPYRIGHT_NOTICE); } @Nullable public String getCountryOrPrimaryLocationDescription() { - return _directory.getString(IptcDirectory.TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME); + return _directory.getString(TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME); } @Nullable public String getCreditDescription() { - return _directory.getString(IptcDirectory.TAG_CREDIT); + return _directory.getString(TAG_CREDIT); } @Nullable public String getDateCreatedDescription() { - return _directory.getString(IptcDirectory.TAG_DATE_CREATED); + return getDateDescription(TAG_DATE_CREATED); + } + + @Nullable + public String getDigitalDateCreatedDescription() + { + return getDateDescription(TAG_DIGITAL_DATE_CREATED); + } + + @Nullable + public String getDateSentDescription() + { + return getDateDescription(TAG_DATE_SENT); + } + + @Nullable + public String getExpirationDateDescription() + { + return getDateDescription(TAG_EXPIRATION_DATE); + } + + @Nullable + public String getExpirationTimeDescription() + { + return getTimeDescription(TAG_EXPIRATION_TIME); } @Nullable public String getHeadlineDescription() { - return _directory.getString(IptcDirectory.TAG_HEADLINE); + return _directory.getString(TAG_HEADLINE); } @Nullable public String getKeywordsDescription() { - final String[] keywords = _directory.getStringArray(IptcDirectory.TAG_KEYWORDS); + final String[] keywords = _directory.getStringArray(TAG_KEYWORDS); if (keywords==null) return null; return StringUtil.join(keywords, ";"); @@ -166,78 +237,96 @@ public class IptcDescriptor extends TagDescriptor<IptcDirectory> @Nullable public String getObjectNameDescription() { - return _directory.getString(IptcDirectory.TAG_OBJECT_NAME); + return _directory.getString(TAG_OBJECT_NAME); } @Nullable public String getOriginalTransmissionReferenceDescription() { - return _directory.getString(IptcDirectory.TAG_ORIGINAL_TRANSMISSION_REFERENCE); + return _directory.getString(TAG_ORIGINAL_TRANSMISSION_REFERENCE); } @Nullable public String getOriginatingProgramDescription() { - return _directory.getString(IptcDirectory.TAG_ORIGINATING_PROGRAM); + return _directory.getString(TAG_ORIGINATING_PROGRAM); } @Nullable public String getProvinceOrStateDescription() { - return _directory.getString(IptcDirectory.TAG_PROVINCE_OR_STATE); + return _directory.getString(TAG_PROVINCE_OR_STATE); } @Nullable public String getRecordVersionDescription() { - return _directory.getString(IptcDirectory.TAG_APPLICATION_RECORD_VERSION); + return _directory.getString(TAG_APPLICATION_RECORD_VERSION); + } + + @Nullable + public String getReferenceDateDescription() + { + return getDateDescription(TAG_REFERENCE_DATE); } @Nullable public String getReleaseDateDescription() { - return _directory.getString(IptcDirectory.TAG_RELEASE_DATE); + return getDateDescription(TAG_RELEASE_DATE); } @Nullable public String getReleaseTimeDescription() { - return _directory.getString(IptcDirectory.TAG_RELEASE_TIME); + return getTimeDescription(TAG_RELEASE_TIME); } @Nullable public String getSourceDescription() { - return _directory.getString(IptcDirectory.TAG_SOURCE); + return _directory.getString(TAG_SOURCE); } @Nullable public String getSpecialInstructionsDescription() { - return _directory.getString(IptcDirectory.TAG_SPECIAL_INSTRUCTIONS); + return _directory.getString(TAG_SPECIAL_INSTRUCTIONS); } @Nullable public String getSupplementalCategoriesDescription() { - return _directory.getString(IptcDirectory.TAG_SUPPLEMENTAL_CATEGORIES); + return _directory.getString(TAG_SUPPLEMENTAL_CATEGORIES); } @Nullable public String getTimeCreatedDescription() { - return _directory.getString(IptcDirectory.TAG_TIME_CREATED); + return getTimeDescription(TAG_TIME_CREATED); + } + + @Nullable + public String getDigitalTimeCreatedDescription() + { + return getTimeDescription(TAG_DIGITAL_TIME_CREATED); + } + + @Nullable + public String getTimeSentDescription() + { + return getTimeDescription(TAG_TIME_SENT); } @Nullable public String getUrgencyDescription() { - return _directory.getString(IptcDirectory.TAG_URGENCY); + return _directory.getString(TAG_URGENCY); } @Nullable public String getWriterDescription() { - return _directory.getString(IptcDirectory.TAG_CAPTION_WRITER); + return _directory.getString(TAG_CAPTION_WRITER); } } diff --git a/Source/com/drew/metadata/iptc/IptcDirectory.java b/Source/com/drew/metadata/iptc/IptcDirectory.java index b864a9b..0e436df 100644 --- a/Source/com/drew/metadata/iptc/IptcDirectory.java +++ b/Source/com/drew/metadata/iptc/IptcDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,11 @@ import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.Directory; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.List; @@ -33,6 +37,7 @@ import java.util.List; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class IptcDirectory extends Directory { // IPTC EnvelopeRecord Tags @@ -230,9 +235,84 @@ public class IptcDirectory extends Directory @Nullable public List<String> getKeywords() { - final String[] array = getStringArray(IptcDirectory.TAG_KEYWORDS); + final String[] array = getStringArray(TAG_KEYWORDS); if (array==null) return null; return Arrays.asList(array); } + + /** + * Parses the Date Sent tag and the Time Sent tag to obtain a single Date object representing the + * date and time when the service sent this image. + * @return A Date object representing when the service sent this image, if possible, otherwise null + */ + @Nullable + public Date getDateSent() + { + return getDate(TAG_DATE_SENT, TAG_TIME_SENT); + } + + /** + * Parses the Release Date tag and the Release Time tag to obtain a single Date object representing the + * date and time when this image was released. + * @return A Date object representing when this image was released, if possible, otherwise null + */ + @Nullable + public Date getReleaseDate() + { + return getDate(TAG_RELEASE_DATE, TAG_RELEASE_TIME); + } + + /** + * Parses the Expiration Date tag and the Expiration Time tag to obtain a single Date object representing + * that this image should not used after this date and time. + * @return A Date object representing when this image was released, if possible, otherwise null + */ + @Nullable + public Date getExpirationDate() + { + return getDate(TAG_EXPIRATION_DATE, TAG_EXPIRATION_TIME); + } + + /** + * Parses the Date Created tag and the Time Created tag to obtain a single Date object representing the + * date and time when this image was captured. + * @return A Date object representing when this image was captured, if possible, otherwise null + */ + @Nullable + public Date getDateCreated() + { + return getDate(TAG_DATE_CREATED, TAG_TIME_CREATED); + } + + /** + * Parses the Digital Date Created tag and the Digital Time Created tag to obtain a single Date object + * representing the date and time when the digital representation of this image was created. + * @return A Date object representing when the digital representation of this image was created, + * if possible, otherwise null + */ + @Nullable + public Date getDigitalDateCreated() + { + return getDate(TAG_DIGITAL_DATE_CREATED, TAG_DIGITAL_TIME_CREATED); + } + + @Nullable + private Date getDate(int dateTagType, int timeTagType) + { + String date = getString(dateTagType); + String time = getString(timeTagType); + + if (date == null) + return null; + if (time == null) + return null; + + try { + DateFormat parser = new SimpleDateFormat("yyyyMMddHHmmssZ"); + return parser.parse(date + time); + } catch (ParseException e) { + return null; + } + } } diff --git a/Source/com/drew/metadata/iptc/IptcReader.java b/Source/com/drew/metadata/iptc/IptcReader.java index 1745f1a..3787bc7 100644 --- a/Source/com/drew/metadata/iptc/IptcReader.java +++ b/Source/com/drew/metadata/iptc/IptcReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,12 +25,14 @@ import com.drew.imaging.jpeg.JpegSegmentType; import com.drew.lang.SequentialByteArrayReader; import com.drew.lang.SequentialReader; 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.StringValue; import java.io.IOException; -import java.util.Arrays; -import java.util.Date; +import java.nio.charset.Charset; +import java.util.Collections; /** * Decodes IPTC binary data, populating a {@link Metadata} object with tag values in an {@link IptcDirectory}. @@ -55,30 +57,42 @@ public class IptcReader implements JpegSegmentMetadataReader public static final int DATA_RECORD = 8; public static final int POST_DATA_RECORD = 9; */ + private static final byte IptcMarkerByte = 0x1c; @NotNull public Iterable<JpegSegmentType> getSegmentTypes() { - return Arrays.asList(JpegSegmentType.APPD); + return Collections.singletonList(JpegSegmentType.APPD); } - public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType) + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) { - // Check whether the first byte resembles - return segmentBytes.length != 0 && segmentBytes[0] == 0x1c; + for (byte[] segmentBytes : segments) { + // Ensure data starts with the IPTC marker byte + if (segmentBytes.length != 0 && segmentBytes[0] == IptcMarkerByte) { + extract(new SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.length); + } + } } - public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) + /** + * Performs the IPTC data extraction, adding found values to the specified instance of {@link Metadata}. + */ + public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata, long length) { - extract(new SequentialByteArrayReader(segmentBytes), metadata, segmentBytes.length); + extract(reader, metadata, length, null); } /** * Performs the IPTC data extraction, adding found values to the specified instance of {@link Metadata}. */ - public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata, long length) + public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata, long length, @Nullable Directory parentDirectory) { - IptcDirectory directory = metadata.getOrCreateDirectory(IptcDirectory.class); + IptcDirectory directory = new IptcDirectory(); + metadata.addDirectory(directory); + + if (parentDirectory != null) + directory.setParent(parentDirectory); int offset = 0; @@ -95,16 +109,16 @@ public class IptcReader implements JpegSegmentMetadataReader return; } - if (startByte != 0x1c) { + if (startByte != IptcMarkerByte) { // NOTE have seen images where there was one extra byte at the end, giving // offset==length at this point, which is not worth logging as an error. if (offset != length) - directory.addError("Invalid IPTC tag marker at offset " + (offset - 1) + ". Expected '0x1c' but got '0x" + Integer.toHexString(startByte) + "'."); + directory.addError("Invalid IPTC tag marker at offset " + (offset - 1) + ". Expected '0x" + Integer.toHexString(IptcMarkerByte) + "' but got '0x" + Integer.toHexString(startByte) + "'."); return; } // we need at least five bytes left to read a tag - if (offset + 5 >= length) { + if (offset + 5 > length) { directory.addError("Too few bytes remain for a valid IPTC tag"); return; } @@ -152,18 +166,15 @@ public class IptcReader implements JpegSegmentMetadataReader return; } - String string = null; - switch (tagIdentifier) { case IptcDirectory.TAG_CODED_CHARACTER_SET: byte[] bytes = reader.getBytes(tagByteCount); - String charset = Iso2022Converter.convertISO2022CharsetToJavaCharset(bytes); - if (charset == null) { + String charsetName = Iso2022Converter.convertISO2022CharsetToJavaCharset(bytes); + if (charsetName == null) { // Unable to determine the charset, so fall through and treat tag as a regular string - string = new String(bytes); - break; + charsetName = new String(bytes); } - directory.setString(tagIdentifier, charset); + directory.setString(tagIdentifier, charsetName); return; case IptcDirectory.TAG_ENVELOPE_RECORD_VERSION: case IptcDirectory.TAG_APPLICATION_RECORD_VERSION: @@ -183,58 +194,44 @@ public class IptcReader implements JpegSegmentMetadataReader directory.setInt(tagIdentifier, reader.getUInt8()); reader.skip(tagByteCount - 1); return; - case IptcDirectory.TAG_RELEASE_DATE: - case IptcDirectory.TAG_DATE_CREATED: - // Date object - if (tagByteCount >= 8) { - string = reader.getString(tagByteCount); - try { - int year = Integer.parseInt(string.substring(0, 4)); - int month = Integer.parseInt(string.substring(4, 6)) - 1; - int day = Integer.parseInt(string.substring(6, 8)); - Date date = new java.util.GregorianCalendar(year, month, day).getTime(); - directory.setDate(tagIdentifier, date); - return; - } catch (NumberFormatException e) { - // fall through and we'll process the 'string' value below - } - } else { - reader.skip(tagByteCount); - } - case IptcDirectory.TAG_RELEASE_TIME: - case IptcDirectory.TAG_TIME_CREATED: - // time... default: // fall through } // If we haven't returned yet, treat it as a string // NOTE that there's a chance we've already loaded the value as a string above, but failed to parse the value - if (string == null) { - String encoding = directory.getString(IptcDirectory.TAG_CODED_CHARACTER_SET); - if (encoding != null) { - string = reader.getString(tagByteCount, encoding); - } else { - byte[] bytes = reader.getBytes(tagByteCount); - encoding = Iso2022Converter.guessEncoding(bytes); - string = encoding != null ? new String(bytes, encoding) : new String(bytes); - } + String charSetName = directory.getString(IptcDirectory.TAG_CODED_CHARACTER_SET); + Charset charset = null; + try { + if (charSetName != null) + charset = Charset.forName(charSetName); + } catch (Throwable ignored) { + } + + StringValue string; + if (charSetName != null) { + string = reader.getStringValue(tagByteCount, charset); + } else { + byte[] bytes = reader.getBytes(tagByteCount); + Charset charSet = Iso2022Converter.guessCharSet(bytes); + string = charSet != null ? new StringValue(bytes, charSet) : new StringValue(bytes, null); } if (directory.containsTag(tagIdentifier)) { - // this fancy string[] business avoids using an ArrayList for performance reasons - String[] oldStrings = directory.getStringArray(tagIdentifier); - String[] newStrings; + // this fancy StringValue[] business avoids using an ArrayList for performance reasons + StringValue[] oldStrings = directory.getStringValueArray(tagIdentifier); + StringValue[] newStrings; if (oldStrings == null) { - newStrings = new String[1]; + // TODO hitting this block means any prior value(s) are discarded + newStrings = new StringValue[1]; } else { - newStrings = new String[oldStrings.length + 1]; + newStrings = new StringValue[oldStrings.length + 1]; System.arraycopy(oldStrings, 0, newStrings, 0, oldStrings.length); } newStrings[newStrings.length - 1] = string; - directory.setStringArray(tagIdentifier, newStrings); + directory.setStringValueArray(tagIdentifier, newStrings); } else { - directory.setString(tagIdentifier, string); + directory.setStringValue(tagIdentifier, string); } } } diff --git a/Source/com/drew/metadata/iptc/Iso2022Converter.java b/Source/com/drew/metadata/iptc/Iso2022Converter.java index 5edd749..8beb9a1 100644 --- a/Source/com/drew/metadata/iptc/Iso2022Converter.java +++ b/Source/com/drew/metadata/iptc/Iso2022Converter.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.iptc; import com.drew.lang.annotations.NotNull; @@ -38,19 +58,19 @@ public final class Iso2022Converter } /** - * Attempts to guess the encoding of a string provided as a byte array. - * <p/> - * Encodings trialled are, in order: + * Attempts to guess the {@link Charset} of a string provided as a byte array. + * <p> + * Charsets trialled are, in order: * <ul> * <li>UTF-8</li> * <li><code>System.getProperty("file.encoding")</code></li> * <li>ISO-8859-1</li> * </ul> - * <p/> - * Its only purpose is to guess the encoding if and only if iptc tag coded character set is not set. If the + * <p> + * Its only purpose is to guess the Charset if and only if IPTC tag coded character set is not set. If the * encoding is not UTF-8, the tag should be set. Otherwise it is bad practice. This method tries to * workaround this issue since some metadata manipulating tools do not prevent such bad practice. - * <p/> + * <p> * About the reliability of this method: The check if some bytes are UTF-8 or not has a very high reliability. * The two other checks are less reliable. * @@ -58,17 +78,18 @@ public final class Iso2022Converter * @return the name of the encoding or null if none could be guessed */ @Nullable - static String guessEncoding(@NotNull final byte[] bytes) + static Charset guessCharSet(@NotNull final byte[] bytes) { String[] encodings = { UTF_8, System.getProperty("file.encoding"), ISO_8859_1 }; for (String encoding : encodings) { - CharsetDecoder cs = Charset.forName(encoding).newDecoder(); + Charset charset = Charset.forName(encoding); + CharsetDecoder cs = charset.newDecoder(); try { cs.decode(ByteBuffer.wrap(bytes)); - return encoding; + return charset; } catch (CharacterCodingException e) { // fall through... } diff --git a/Source/com/drew/metadata/iptc/package-info.java b/Source/com/drew/metadata/iptc/package-info.java new file mode 100644 index 0000000..8e0f4f9 --- /dev/null +++ b/Source/com/drew/metadata/iptc/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of IPTC metadata. + */ +package com.drew.metadata.iptc; diff --git a/Source/com/drew/metadata/iptc/package.html b/Source/com/drew/metadata/iptc/package.html deleted file mode 100644 index 40c60b3..0000000 --- a/Source/com/drew/metadata/iptc/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of IPTC metadata. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/metadata/jfif/JfifDescriptor.java b/Source/com/drew/metadata/jfif/JfifDescriptor.java index 25bf64d..d6f175e 100644 --- a/Source/com/drew/metadata/jfif/JfifDescriptor.java +++ b/Source/com/drew/metadata/jfif/JfifDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,13 +24,19 @@ import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; +import static com.drew.metadata.jfif.JfifDirectory.*; + /** * Provides human-readable string versions of the tags stored in a JfifDirectory. - * <p> - * More info at: http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format + * + * <ul> + * <li>http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format</li> + * <li>http://www.w3.org/Graphics/JPEG/jfif3.pdf</li> + * </ul> * * @author Yuri Binev, Drew Noakes */ +@SuppressWarnings("WeakerAccess") public class JfifDescriptor extends TagDescriptor<JfifDirectory> { public JfifDescriptor(@NotNull JfifDirectory directory) @@ -43,13 +49,13 @@ public class JfifDescriptor extends TagDescriptor<JfifDirectory> public String getDescription(int tagType) { switch (tagType) { - case JfifDirectory.TAG_RESX: + case TAG_RESX: return getImageResXDescription(); - case JfifDirectory.TAG_RESY: + case TAG_RESY: return getImageResYDescription(); - case JfifDirectory.TAG_VERSION: + case TAG_VERSION: return getImageVersionDescription(); - case JfifDirectory.TAG_UNITS: + case TAG_UNITS: return getImageResUnitsDescription(); default: return super.getDescription(tagType); @@ -59,7 +65,7 @@ public class JfifDescriptor extends TagDescriptor<JfifDirectory> @Nullable public String getImageVersionDescription() { - Integer value = _directory.getInteger(JfifDirectory.TAG_VERSION); + Integer value = _directory.getInteger(TAG_VERSION); if (value==null) return null; return String.format("%d.%d", (value & 0xFF00) >> 8, value & 0xFF); @@ -68,7 +74,7 @@ public class JfifDescriptor extends TagDescriptor<JfifDirectory> @Nullable public String getImageResYDescription() { - Integer value = _directory.getInteger(JfifDirectory.TAG_RESY); + Integer value = _directory.getInteger(TAG_RESY); if (value==null) return null; return String.format("%d dot%s", @@ -79,7 +85,7 @@ public class JfifDescriptor extends TagDescriptor<JfifDirectory> @Nullable public String getImageResXDescription() { - Integer value = _directory.getInteger(JfifDirectory.TAG_RESX); + Integer value = _directory.getInteger(TAG_RESX); if (value==null) return null; return String.format("%d dot%s", @@ -90,7 +96,7 @@ public class JfifDescriptor extends TagDescriptor<JfifDirectory> @Nullable public String getImageResUnitsDescription() { - Integer value = _directory.getInteger(JfifDirectory.TAG_UNITS); + Integer value = _directory.getInteger(TAG_UNITS); if (value==null) return null; switch (value) { diff --git a/Source/com/drew/metadata/jfif/JfifDirectory.java b/Source/com/drew/metadata/jfif/JfifDirectory.java index 947153d..b9e477d 100644 --- a/Source/com/drew/metadata/jfif/JfifDirectory.java +++ b/Source/com/drew/metadata/jfif/JfifDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import java.util.HashMap; * * @author Yuri Binev, Drew Noakes */ +@SuppressWarnings("WeakerAccess") public class JfifDirectory extends Directory { public static final int TAG_VERSION = 5; @@ -38,6 +39,8 @@ public class JfifDirectory extends Directory public static final int TAG_UNITS = 7; public static final int TAG_RESX = 8; public static final int TAG_RESY = 10; + public static final int TAG_THUMB_WIDTH = 12; + public static final int TAG_THUMB_HEIGHT = 13; @NotNull protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); @@ -48,6 +51,8 @@ public class JfifDirectory extends Directory _tagNameMap.put(TAG_UNITS, "Resolution Units"); _tagNameMap.put(TAG_RESY, "Y Resolution"); _tagNameMap.put(TAG_RESX, "X Resolution"); + _tagNameMap.put(TAG_THUMB_WIDTH, "Thumbnail Width Pixels"); + _tagNameMap.put(TAG_THUMB_HEIGHT, "Thumbnail Height Pixels"); } public JfifDirectory() @@ -79,14 +84,31 @@ public class JfifDirectory extends Directory return getInt(JfifDirectory.TAG_UNITS); } + /** + * @deprecated use {@link #getResY} instead. + */ + @Deprecated public int getImageWidth() throws MetadataException { return getInt(JfifDirectory.TAG_RESY); } + public int getResY() throws MetadataException + { + return getInt(JfifDirectory.TAG_RESY); + } + + /** + * @deprecated use {@link #getResX} instead. + */ + @Deprecated public int getImageHeight() throws MetadataException { return getInt(JfifDirectory.TAG_RESX); } + public int getResX() throws MetadataException + { + return getInt(JfifDirectory.TAG_RESX); + } } diff --git a/Source/com/drew/metadata/jfif/JfifReader.java b/Source/com/drew/metadata/jfif/JfifReader.java index f29fd90..55db301 100644 --- a/Source/com/drew/metadata/jfif/JfifReader.java +++ b/Source/com/drew/metadata/jfif/JfifReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,31 +29,35 @@ import com.drew.metadata.Metadata; import com.drew.metadata.MetadataReader; import java.io.IOException; -import java.util.Arrays; +import java.util.Collections; /** * Reader for JFIF data, found in the APP0 JPEG segment. - * <p> - * More info at: http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format + * + * <ul> + * <li>http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format</li> + * <li>http://www.w3.org/Graphics/JPEG/jfif3.pdf</li> + * </ul> * * @author Yuri Binev, Drew Noakes, Markus Meyer */ public class JfifReader implements JpegSegmentMetadataReader, MetadataReader { + public static final String PREAMBLE = "JFIF"; + @NotNull public Iterable<JpegSegmentType> getSegmentTypes() { - return Arrays.asList(JpegSegmentType.APP0); - } - - public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType) - { - return segmentBytes.length > 3 && "JFIF".equals(new String(segmentBytes, 0, 4)); + return Collections.singletonList(JpegSegmentType.APP0); } - public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) { - extract(new ByteArrayReader(segmentBytes), metadata); + for (byte[] segmentBytes : segments) { + // Skip segments not starting with the required header + if (segmentBytes.length >= PREAMBLE.length() && PREAMBLE.equals(new String(segmentBytes, 0, PREAMBLE.length()))) + extract(new ByteArrayReader(segmentBytes), metadata); + } } /** @@ -62,23 +66,18 @@ public class JfifReader implements JpegSegmentMetadataReader, MetadataReader */ public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata) { - JfifDirectory directory = metadata.getOrCreateDirectory(JfifDirectory.class); + JfifDirectory directory = new JfifDirectory(); + metadata.addDirectory(directory); try { // For JFIF, the tag number is also the offset into the segment - int ver = reader.getUInt16(JfifDirectory.TAG_VERSION); - directory.setInt(JfifDirectory.TAG_VERSION, ver); - - int units = reader.getUInt8(JfifDirectory.TAG_UNITS); - directory.setInt(JfifDirectory.TAG_UNITS, units); - - int height = reader.getUInt16(JfifDirectory.TAG_RESX); - directory.setInt(JfifDirectory.TAG_RESX, height); - - int width = reader.getUInt16(JfifDirectory.TAG_RESY); - directory.setInt(JfifDirectory.TAG_RESY, width); - + directory.setInt(JfifDirectory.TAG_VERSION, reader.getUInt16(JfifDirectory.TAG_VERSION)); + directory.setInt(JfifDirectory.TAG_UNITS, reader.getUInt8(JfifDirectory.TAG_UNITS)); + directory.setInt(JfifDirectory.TAG_RESX, reader.getUInt16(JfifDirectory.TAG_RESX)); + directory.setInt(JfifDirectory.TAG_RESY, reader.getUInt16(JfifDirectory.TAG_RESY)); + directory.setInt(JfifDirectory.TAG_THUMB_WIDTH, reader.getUInt8(JfifDirectory.TAG_THUMB_WIDTH)); + directory.setInt(JfifDirectory.TAG_THUMB_HEIGHT, reader.getUInt8(JfifDirectory.TAG_THUMB_HEIGHT)); } catch (IOException me) { directory.addError(me.getMessage()); } diff --git a/Source/com/drew/metadata/jfif/package-info.java b/Source/com/drew/metadata/jfif/package-info.java new file mode 100644 index 0000000..1c65e5b --- /dev/null +++ b/Source/com/drew/metadata/jfif/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of JFIF metadata. + */ +package com.drew.metadata.jfif; diff --git a/Source/com/drew/metadata/jfif/package.html b/Source/com/drew/metadata/jfif/package.html deleted file mode 100644 index 384a7d9..0000000 --- a/Source/com/drew/metadata/jfif/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of JFIF metadata. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/metadata/jfxx/JfxxDescriptor.java b/Source/com/drew/metadata/jfxx/JfxxDescriptor.java new file mode 100644 index 0000000..4e0ce98 --- /dev/null +++ b/Source/com/drew/metadata/jfxx/JfxxDescriptor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.jfxx; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.jfxx.JfxxDirectory.*; + +/** + * Provides human-readable string versions of the tags stored in a JfxxDirectory. + * + * <ul> + * <li>http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format</li> + * <li>http://www.w3.org/Graphics/JPEG/jfif3.pdf</li> + * </ul> + * + * @author Drew Noakes + */ +@SuppressWarnings("WeakerAccess") +public class JfxxDescriptor extends TagDescriptor<JfxxDirectory> +{ + public JfxxDescriptor(@NotNull JfxxDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TAG_EXTENSION_CODE: + return getExtensionCodeDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getExtensionCodeDescription() + { + Integer value = _directory.getInteger(TAG_EXTENSION_CODE); + if (value==null) + return null; + switch (value) { + case 0x10: return "Thumbnail coded using JPEG"; + case 0x11: return "Thumbnail stored using 1 byte/pixel"; + case 0x13: return "Thumbnail stored using 3 bytes/pixel"; + default: return "Unknown extension code " + value; + } + } +} diff --git a/Source/com/drew/metadata/jfxx/JfxxDirectory.java b/Source/com/drew/metadata/jfxx/JfxxDirectory.java new file mode 100644 index 0000000..dd101da --- /dev/null +++ b/Source/com/drew/metadata/jfxx/JfxxDirectory.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.jfxx; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; +import com.drew.metadata.MetadataException; + +import java.util.HashMap; + +/** + * Directory of tags and values for the SOF0 JFXX segment. + * + * @author Drew Noakes + */ +@SuppressWarnings("WeakerAccess") +public class JfxxDirectory extends Directory +{ + public static final int TAG_EXTENSION_CODE = 5; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TAG_EXTENSION_CODE, "Extension Code"); + } + + public JfxxDirectory() + { + this.setDescriptor(new JfxxDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "JFXX"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } + + public int getExtensionCode() throws MetadataException + { + return getInt(JfxxDirectory.TAG_EXTENSION_CODE); + } +} diff --git a/Source/com/drew/metadata/jfxx/JfxxReader.java b/Source/com/drew/metadata/jfxx/JfxxReader.java new file mode 100644 index 0000000..ca68ad9 --- /dev/null +++ b/Source/com/drew/metadata/jfxx/JfxxReader.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.jfxx; + +import com.drew.imaging.jpeg.JpegSegmentMetadataReader; +import com.drew.imaging.jpeg.JpegSegmentType; +import com.drew.lang.ByteArrayReader; +import com.drew.lang.RandomAccessReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; +import com.drew.metadata.MetadataReader; + +import java.io.IOException; +import java.util.Collections; + +/** + * Reader for JFXX (JFIF extensions) data, found in the APP0 JPEG segment. + * + * <ul> + * <li>http://en.wikipedia.org/wiki/JPEG_File_Interchange_Format</li> + * <li>http://www.w3.org/Graphics/JPEG/jfif3.pdf</li> + * </ul> + * + * @author Drew Noakes + */ +public class JfxxReader implements JpegSegmentMetadataReader, MetadataReader +{ + public static final String PREAMBLE = "JFXX"; + + @NotNull + public Iterable<JpegSegmentType> getSegmentTypes() + { + return Collections.singletonList(JpegSegmentType.APP0); + } + + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) + { + for (byte[] segmentBytes : segments) { + // Skip segments not starting with the required header + if (segmentBytes.length >= PREAMBLE.length() && PREAMBLE.equals(new String(segmentBytes, 0, PREAMBLE.length()))) + extract(new ByteArrayReader(segmentBytes), metadata); + } + } + + /** + * Performs the JFXX data extraction, adding found values to the specified + * instance of {@link Metadata}. + */ + public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata) + { + JfxxDirectory directory = new JfxxDirectory(); + metadata.addDirectory(directory); + + try { + // For JFXX, the tag number is also the offset into the segment + + directory.setInt(JfxxDirectory.TAG_EXTENSION_CODE, reader.getUInt8(JfxxDirectory.TAG_EXTENSION_CODE)); + } catch (IOException me) { + directory.addError(me.getMessage()); + } + } +} diff --git a/Source/com/drew/metadata/jfxx/package-info.java b/Source/com/drew/metadata/jfxx/package-info.java new file mode 100644 index 0000000..8396977 --- /dev/null +++ b/Source/com/drew/metadata/jfxx/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of JFXX (JFIF extension) metadata. + */ +package com.drew.metadata.jfxx; diff --git a/Source/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java b/Source/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java new file mode 100644 index 0000000..cb83321 --- /dev/null +++ b/Source/com/drew/metadata/jpeg/HuffmanTablesDescriptor.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.jpeg; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.jpeg.HuffmanTablesDirectory.*; + +/** + * Provides a human-readable string version of the tag stored in a HuffmanTableDirectory. + * + * <ul> + * <li>https://en.wikipedia.org/wiki/Huffman_coding</li> + * <li>http://stackoverflow.com/a/4954117</li> + * </ul> + * + * @author Nadahar + */ +@SuppressWarnings("WeakerAccess") +public class HuffmanTablesDescriptor extends TagDescriptor<HuffmanTablesDirectory> +{ + public HuffmanTablesDescriptor(@NotNull HuffmanTablesDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + case TAG_NUMBER_OF_TABLES: + return getNumberOfTablesDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getNumberOfTablesDescription() + { + Integer value = _directory.getInteger(TAG_NUMBER_OF_TABLES); + if (value==null) + return null; + return value + (value == 1 ? " Huffman table" : " Huffman tables"); + } +} diff --git a/Source/com/drew/metadata/jpeg/HuffmanTablesDirectory.java b/Source/com/drew/metadata/jpeg/HuffmanTablesDirectory.java new file mode 100644 index 0000000..b03a518 --- /dev/null +++ b/Source/com/drew/metadata/jpeg/HuffmanTablesDirectory.java @@ -0,0 +1,360 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.jpeg; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; +import com.drew.metadata.MetadataException; + +/** + * Directory of tables for the DHT (Define Huffman Table(s)) segment. + * + * @author Nadahar + */ +@SuppressWarnings("WeakerAccess") +public class HuffmanTablesDirectory extends Directory { + + public static final int TAG_NUMBER_OF_TABLES = 1; + + protected static final byte[] TYPICAL_LUMINANCE_DC_LENGTHS = { + (byte) 0x00, (byte) 0x01, (byte) 0x05, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, + (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 + }; + + protected static final byte[] TYPICAL_LUMINANCE_DC_VALUES = { + (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, + (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B + }; + + protected static final byte[] TYPICAL_CHROMINANCE_DC_LENGTHS = { + (byte) 0x00, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x01, + (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 + }; + + protected static final byte[] TYPICAL_CHROMINANCE_DC_VALUES = { + (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04, (byte) 0x05, (byte) 0x06, (byte) 0x07, + (byte) 0x08, (byte) 0x09, (byte) 0x0A, (byte) 0x0B + }; + + protected static final byte[] TYPICAL_LUMINANCE_AC_LENGTHS = { + (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x03, (byte) 0x03, (byte) 0x02, (byte) 0x04, (byte) 0x03, + (byte) 0x05, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x00, (byte) 0x01, (byte) 0x7D + }; + + protected static final byte[] TYPICAL_LUMINANCE_AC_VALUES = { + (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x00, (byte) 0x04, (byte) 0x11, (byte) 0x05, (byte) 0x12, + (byte) 0x21, (byte) 0x31, (byte) 0x41, (byte) 0x06, (byte) 0x13, (byte) 0x51, (byte) 0x61, (byte) 0x07, + (byte) 0x22, (byte) 0x71, (byte) 0x14, (byte) 0x32, (byte) 0x81, (byte) 0x91, (byte) 0xA1, (byte) 0x08, + (byte) 0x23, (byte) 0x42, (byte) 0xB1, (byte) 0xC1, (byte) 0x15, (byte) 0x52, (byte) 0xD1, (byte) 0xF0, + (byte) 0x24, (byte) 0x33, (byte) 0x62, (byte) 0x72, (byte) 0x82, (byte) 0x09, (byte) 0x0A, (byte) 0x16, + (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1A, (byte) 0x25, (byte) 0x26, (byte) 0x27, (byte) 0x28, + (byte) 0x29, (byte) 0x2A, (byte) 0x34, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38, (byte) 0x39, + (byte) 0x3A, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, (byte) 0x49, + (byte) 0x4A, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, (byte) 0x59, + (byte) 0x5A, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, (byte) 0x69, + (byte) 0x6A, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, (byte) 0x79, + (byte) 0x7A, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, (byte) 0x88, (byte) 0x89, + (byte) 0x8A, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, (byte) 0x97, (byte) 0x98, + (byte) 0x99, (byte) 0x9A, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, (byte) 0xA6, (byte) 0xA7, + (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, (byte) 0xB5, (byte) 0xB6, + (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xC2, (byte) 0xC3, (byte) 0xC4, (byte) 0xC5, + (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xD2, (byte) 0xD3, (byte) 0xD4, + (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, (byte) 0xE1, (byte) 0xE2, + (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, (byte) 0xEA, + (byte) 0xF1, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, + (byte) 0xF9, (byte) 0xFA + }; + + protected static final byte[] TYPICAL_CHROMINANCE_AC_LENGTHS = { + (byte) 0x00, (byte) 0x02, (byte) 0x01, (byte) 0x02, (byte) 0x04, (byte) 0x04, (byte) 0x03, (byte) 0x04, + (byte) 0x07, (byte) 0x05, (byte) 0x04, (byte) 0x04, (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x77 + }; + + protected static final byte[] TYPICAL_CHROMINANCE_AC_VALUES = { + (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x11, (byte) 0x04, (byte) 0x05, (byte) 0x21, + (byte) 0x31, (byte) 0x06, (byte) 0x12, (byte) 0x41, (byte) 0x51, (byte) 0x07, (byte) 0x61, (byte) 0x71, + (byte) 0x13, (byte) 0x22, (byte) 0x32, (byte) 0x81, (byte) 0x08, (byte) 0x14, (byte) 0x42, (byte) 0x91, + (byte) 0xA1, (byte) 0xB1, (byte) 0xC1, (byte) 0x09, (byte) 0x23, (byte) 0x33, (byte) 0x52, (byte) 0xF0, + (byte) 0x15, (byte) 0x62, (byte) 0x72, (byte) 0xD1, (byte) 0x0A, (byte) 0x16, (byte) 0x24, (byte) 0x34, + (byte) 0xE1, (byte) 0x25, (byte) 0xF1, (byte) 0x17, (byte) 0x18, (byte) 0x19, (byte) 0x1A, (byte) 0x26, + (byte) 0x27, (byte) 0x28, (byte) 0x29, (byte) 0x2A, (byte) 0x35, (byte) 0x36, (byte) 0x37, (byte) 0x38, + (byte) 0x39, (byte) 0x3A, (byte) 0x43, (byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, (byte) 0x48, + (byte) 0x49, (byte) 0x4A, (byte) 0x53, (byte) 0x54, (byte) 0x55, (byte) 0x56, (byte) 0x57, (byte) 0x58, + (byte) 0x59, (byte) 0x5A, (byte) 0x63, (byte) 0x64, (byte) 0x65, (byte) 0x66, (byte) 0x67, (byte) 0x68, + (byte) 0x69, (byte) 0x6A, (byte) 0x73, (byte) 0x74, (byte) 0x75, (byte) 0x76, (byte) 0x77, (byte) 0x78, + (byte) 0x79, (byte) 0x7A, (byte) 0x82, (byte) 0x83, (byte) 0x84, (byte) 0x85, (byte) 0x86, (byte) 0x87, + (byte) 0x88, (byte) 0x89, (byte) 0x8A, (byte) 0x92, (byte) 0x93, (byte) 0x94, (byte) 0x95, (byte) 0x96, + (byte) 0x97, (byte) 0x98, (byte) 0x99, (byte) 0x9A, (byte) 0xA2, (byte) 0xA3, (byte) 0xA4, (byte) 0xA5, + (byte) 0xA6, (byte) 0xA7, (byte) 0xA8, (byte) 0xA9, (byte) 0xAA, (byte) 0xB2, (byte) 0xB3, (byte) 0xB4, + (byte) 0xB5, (byte) 0xB6, (byte) 0xB7, (byte) 0xB8, (byte) 0xB9, (byte) 0xBA, (byte) 0xC2, (byte) 0xC3, + (byte) 0xC4, (byte) 0xC5, (byte) 0xC6, (byte) 0xC7, (byte) 0xC8, (byte) 0xC9, (byte) 0xCA, (byte) 0xD2, + (byte) 0xD3, (byte) 0xD4, (byte) 0xD5, (byte) 0xD6, (byte) 0xD7, (byte) 0xD8, (byte) 0xD9, (byte) 0xDA, + (byte) 0xE2, (byte) 0xE3, (byte) 0xE4, (byte) 0xE5, (byte) 0xE6, (byte) 0xE7, (byte) 0xE8, (byte) 0xE9, + (byte) 0xEA, (byte) 0xF2, (byte) 0xF3, (byte) 0xF4, (byte) 0xF5, (byte) 0xF6, (byte) 0xF7, (byte) 0xF8, + (byte) 0xF9, (byte) 0xFA + }; + + @NotNull + protected final List<HuffmanTable> tables = new ArrayList<HuffmanTable>(4); + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static + { + _tagNameMap.put(TAG_NUMBER_OF_TABLES, "Number of Tables"); + } + + public HuffmanTablesDirectory() + { + this.setDescriptor(new HuffmanTablesDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "Huffman"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } + + /** + * @param tableNumber The zero-based index of the table. This number is normally between 0 and 3. + * Use {@link #getNumberOfTables} for bounds-checking. + * @return The {@link HuffmanTable} having the specified number. + */ + @NotNull + public HuffmanTable getTable(int tableNumber) + { + return tables.get(tableNumber); + } + + /** + * @return The number of Huffman tables held by this {@link HuffmanTablesDirectory} instance. + */ + public int getNumberOfTables() throws MetadataException + { + return getInt(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES); + } + + /** + * @return The {@link List} of {@link HuffmanTable}s in this + * {@link Directory}. + */ + @NotNull + protected List<HuffmanTable> getTables() { + return tables; + } + + /** + * Evaluates whether all the tables in this {@link HuffmanTablesDirectory} + * are "typical" Huffman tables. + * <p> + * "Typical" has a special meaning in this context as the JPEG standard + * (ISO/IEC 10918 or ITU-T T.81) defines 4 Huffman tables that has been + * developed from the average statistics of a large set of images with 8-bit + * precision. Using these instead of calculating the optimal Huffman tables + * for a given image is faster, and is preferred by many hardware encoders + * and some hardware decoders. + * <p> + * Even though the JPEG standard doesn't define these as "standard tables" + * and requires a decoder to be able to read any valid Huffman tables, some + * are in reality limited decoding images using these "typical" tables. + * Standards like DCF (Design rule for Camera File system) and DLNA (Digital + * Living Network Alliance) actually requires any compliant JPEG to use only + * the "typical" Huffman tables. + * <p> + * This is also related to the term "optimized" JPEG. An "optimized" JPEG is + * a JPEG that doesn't use the "typical" Huffman tables. + * + * @return Whether or not all the tables in this + * {@link HuffmanTablesDirectory} are the predefined "typical" + * Huffman tables. + */ + public boolean isTypical() { + if (tables.size() == 0) { + return false; + } + for (HuffmanTable table : tables) { + if (!table.isTypical()) { + return false; + } + } + return true; + } + + /** + * The opposite of {@link #isTypical()}. + * + * @return Whether or not the tables in this {@link HuffmanTablesDirectory} + * are "optimized" - which means that at least one of them aren't + * one of the "typical" Huffman tables. + */ + public boolean isOptimized() { + return !isTypical(); + } + + /** + * An instance of this class holds a JPEG Huffman table. + */ + public static class HuffmanTable { + private final int tableLength; + private final HuffmanTableClass tableClass; + private final int tableDestinationId; + private final byte[] lengthBytes; + private final byte[] valueBytes; + + public HuffmanTable ( + @NotNull HuffmanTableClass + tableClass, + int tableDestinationId, + @NotNull byte[] lBytes, + @NotNull byte[] vBytes + ) { + this.tableClass = tableClass; + this.tableDestinationId = tableDestinationId; + this.lengthBytes = lBytes; + this.valueBytes = vBytes; + this.tableLength = vBytes.length + 17; + } + + /** + * @return The table length in bytes. + */ + public int getTableLength() { + return tableLength; + } + + + /** + * @return The {@link HuffmanTableClass} of this table. + */ + public HuffmanTableClass getTableClass() { + return tableClass; + } + + + /** + * @return the the destination identifier for this table. + */ + public int getTableDestinationId() { + return tableDestinationId; + } + + + /** + * @return A byte array with the L values for this table. + */ + public byte[] getLengthBytes() { + if (lengthBytes == null) + return null; + byte[] result = new byte[lengthBytes.length]; + System.arraycopy(lengthBytes, 0, result, 0, lengthBytes.length); + return result; + } + + + /** + * @return A byte array with the V values for this table. + */ + public byte[] getValueBytes() { + if (valueBytes == null) + return null; + byte[] result = new byte[valueBytes.length]; + System.arraycopy(valueBytes, 0, result, 0, valueBytes.length); + return result; + } + + /** + * Evaluates whether this table is a "typical" Huffman table. + * <p> + * "Typical" has a special meaning in this context as the JPEG standard + * (ISO/IEC 10918 or ITU-T T.81) defines 4 Huffman tables that has been + * developed from the average statistics of a large set of images with + * 8-bit precision. Using these instead of calculating the optimal + * Huffman tables for a given image is faster, and is preferred by many + * hardware encoders and some hardware decoders. + * <p> + * Even though the JPEG standard doesn't define these as + * "standard tables" and requires a decoder to be able to read any valid + * Huffman tables, some are in reality limited decoding images using + * these "typical" tables. Standards like DCF (Design rule for Camera + * File system) and DLNA (Digital Living Network Alliance) actually + * requires any compliant JPEG to use only the "typical" Huffman tables. + * <p> + * This is also related to the term "optimized" JPEG. An "optimized" + * JPEG is a JPEG that doesn't use the "typical" Huffman tables. + * + * @return Whether or not this table is one of the predefined "typical" + * Huffman tables. + */ + public boolean isTypical() { + if (tableClass == HuffmanTableClass.DC) { + return + Arrays.equals(lengthBytes, TYPICAL_LUMINANCE_DC_LENGTHS) && + Arrays.equals(valueBytes, TYPICAL_LUMINANCE_DC_VALUES) || + Arrays.equals(lengthBytes, TYPICAL_CHROMINANCE_DC_LENGTHS) && + Arrays.equals(valueBytes, TYPICAL_CHROMINANCE_DC_VALUES); + } else if (tableClass == HuffmanTableClass.AC) { + return + Arrays.equals(lengthBytes, TYPICAL_LUMINANCE_AC_LENGTHS) && + Arrays.equals(valueBytes, TYPICAL_LUMINANCE_AC_VALUES) || + Arrays.equals(lengthBytes, TYPICAL_CHROMINANCE_AC_LENGTHS) && + Arrays.equals(valueBytes, TYPICAL_CHROMINANCE_AC_VALUES); + } + return false; + } + + /** + * The opposite of {@link #isTypical()}. + * + * @return Whether or not this table is "optimized" - which means that + * it isn't one of the "typical" Huffman tables. + */ + public boolean isOptimized() { + return !isTypical(); + } + + public enum HuffmanTableClass { + DC, + AC, + UNKNOWN; + + public static HuffmanTableClass typeOf(int value) { + switch (value) { + case 0: return DC; + case 1 : return AC; + default: return UNKNOWN; + } + } + } + } +} diff --git a/Source/com/drew/metadata/jpeg/JpegCommentDescriptor.java b/Source/com/drew/metadata/jpeg/JpegCommentDescriptor.java index c5a67f8..98f7f44 100644 --- a/Source/com/drew/metadata/jpeg/JpegCommentDescriptor.java +++ b/Source/com/drew/metadata/jpeg/JpegCommentDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import com.drew.metadata.TagDescriptor; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class JpegCommentDescriptor extends TagDescriptor<JpegCommentDirectory> { public JpegCommentDescriptor(@NotNull JpegCommentDirectory directory) diff --git a/Source/com/drew/metadata/jpeg/JpegCommentDirectory.java b/Source/com/drew/metadata/jpeg/JpegCommentDirectory.java index 7e077fa..69975de 100644 --- a/Source/com/drew/metadata/jpeg/JpegCommentDirectory.java +++ b/Source/com/drew/metadata/jpeg/JpegCommentDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class JpegCommentDirectory extends Directory { /** diff --git a/Source/com/drew/metadata/jpeg/JpegCommentReader.java b/Source/com/drew/metadata/jpeg/JpegCommentReader.java index 794c50c..8170ba5 100644 --- a/Source/com/drew/metadata/jpeg/JpegCommentReader.java +++ b/Source/com/drew/metadata/jpeg/JpegCommentReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,9 @@ import com.drew.imaging.jpeg.JpegSegmentMetadataReader; import com.drew.imaging.jpeg.JpegSegmentType; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Metadata; +import com.drew.metadata.StringValue; -import java.util.Arrays; +import java.util.Collections; /** * Decodes the comment stored within JPEG files, populating a {@link Metadata} object with tag values in a @@ -38,20 +39,17 @@ public class JpegCommentReader implements JpegSegmentMetadataReader @NotNull public Iterable<JpegSegmentType> getSegmentTypes() { - return Arrays.asList(JpegSegmentType.COM); + return Collections.singletonList(JpegSegmentType.COM); } - public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType) + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) { - // The entire contents of the byte[] is the comment. There's nothing here to discriminate upon. - return true; - } - - public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) - { - JpegCommentDirectory directory = metadata.getOrCreateDirectory(JpegCommentDirectory.class); + for (byte[] segmentBytes : segments) { + JpegCommentDirectory directory = new JpegCommentDirectory(); + metadata.addDirectory(directory); - // The entire contents of the directory are the comment - directory.setString(JpegCommentDirectory.TAG_COMMENT, new String(segmentBytes)); + // The entire contents of the directory are the comment + directory.setStringValue(JpegCommentDirectory.TAG_COMMENT, new StringValue(segmentBytes, null)); + } } } diff --git a/Source/com/drew/metadata/jpeg/JpegComponent.java b/Source/com/drew/metadata/jpeg/JpegComponent.java index 06558ec..7554523 100644 --- a/Source/com/drew/metadata/jpeg/JpegComponent.java +++ b/Source/com/drew/metadata/jpeg/JpegComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ */ package com.drew.metadata.jpeg; -import com.drew.lang.annotations.Nullable; +import com.drew.lang.annotations.NotNull; import java.io.Serializable; @@ -54,7 +54,7 @@ public class JpegComponent implements Serializable * Returns the component name (one of: Y, Cb, Cr, I, or Q) * @return the component name */ - @Nullable + @NotNull public String getComponentName() { switch (_componentId) @@ -69,8 +69,9 @@ public class JpegComponent implements Serializable return "I"; case 5: return "Q"; + default: + return String.format("Unknown (%s)", _componentId); } - return null; } public int getQuantizationTableNumber() @@ -80,11 +81,22 @@ public class JpegComponent implements Serializable public int getHorizontalSamplingFactor() { - return _samplingFactorByte & 0x0F; + return (_samplingFactorByte>>4) & 0x0F; } public int getVerticalSamplingFactor() { - return (_samplingFactorByte>>4) & 0x0F; + return _samplingFactorByte & 0x0F; + } + + @NotNull + @Override + public String toString() { + return String.format( + "Quantization table %d, Sampling factors %d horiz/%d vert", + _quantizationTableNumber, + getHorizontalSamplingFactor(), + getVerticalSamplingFactor() + ); } } diff --git a/Source/com/drew/metadata/jpeg/JpegDescriptor.java b/Source/com/drew/metadata/jpeg/JpegDescriptor.java index 3989d20..c9aeeb7 100644 --- a/Source/com/drew/metadata/jpeg/JpegDescriptor.java +++ b/Source/com/drew/metadata/jpeg/JpegDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,12 +24,15 @@ import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; +import static com.drew.metadata.jpeg.JpegDirectory.*; + /** * Provides human-readable string versions of the tags stored in a JpegDirectory. * Thanks to Darrell Silver (www.darrellsilver.com) for the initial version of this class. * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class JpegDescriptor extends TagDescriptor<JpegDirectory> { public JpegDescriptor(@NotNull JpegDirectory directory) @@ -43,21 +46,21 @@ public class JpegDescriptor extends TagDescriptor<JpegDirectory> { switch (tagType) { - case JpegDirectory.TAG_COMPRESSION_TYPE: + case TAG_COMPRESSION_TYPE: return getImageCompressionTypeDescription(); - case JpegDirectory.TAG_COMPONENT_DATA_1: + case TAG_COMPONENT_DATA_1: return getComponentDataDescription(0); - case JpegDirectory.TAG_COMPONENT_DATA_2: + case TAG_COMPONENT_DATA_2: return getComponentDataDescription(1); - case JpegDirectory.TAG_COMPONENT_DATA_3: + case TAG_COMPONENT_DATA_3: return getComponentDataDescription(2); - case JpegDirectory.TAG_COMPONENT_DATA_4: + case TAG_COMPONENT_DATA_4: return getComponentDataDescription(3); - case JpegDirectory.TAG_DATA_PRECISION: + case TAG_DATA_PRECISION: return getDataPrecisionDescription(); - case JpegDirectory.TAG_IMAGE_HEIGHT: + case TAG_IMAGE_HEIGHT: return getImageHeightDescription(); - case JpegDirectory.TAG_IMAGE_WIDTH: + case TAG_IMAGE_WIDTH: return getImageWidthDescription(); default: return super.getDescription(tagType); @@ -67,33 +70,29 @@ public class JpegDescriptor extends TagDescriptor<JpegDirectory> @Nullable public String getImageCompressionTypeDescription() { - Integer value = _directory.getInteger(JpegDirectory.TAG_COMPRESSION_TYPE); - if (value==null) - return null; - // Note there is no 2 or 12 - switch (value) { - case 0: return "Baseline"; - case 1: return "Extended sequential, Huffman"; - case 2: return "Progressive, Huffman"; - case 3: return "Lossless, Huffman"; - case 5: return "Differential sequential, Huffman"; - case 6: return "Differential progressive, Huffman"; - case 7: return "Differential lossless, Huffman"; - case 8: return "Reserved for JPEG extensions"; - case 9: return "Extended sequential, arithmetic"; - case 10: return "Progressive, arithmetic"; - case 11: return "Lossless, arithmetic"; - case 13: return "Differential sequential, arithmetic"; - case 14: return "Differential progressive, arithmetic"; - case 15: return "Differential lossless, arithmetic"; - default: - return "Unknown type: "+ value; - } + return getIndexedDescription(TAG_COMPRESSION_TYPE, + "Baseline", + "Extended sequential, Huffman", + "Progressive, Huffman", + "Lossless, Huffman", + null, // no 4 + "Differential sequential, Huffman", + "Differential progressive, Huffman", + "Differential lossless, Huffman", + "Reserved for JPEG extensions", + "Extended sequential, arithmetic", + "Progressive, arithmetic", + "Lossless, arithmetic", + null, // no 12 + "Differential sequential, arithmetic", + "Differential progressive, arithmetic", + "Differential lossless, arithmetic"); } + @Nullable public String getImageWidthDescription() { - final String value = _directory.getString(JpegDirectory.TAG_IMAGE_WIDTH); + final String value = _directory.getString(TAG_IMAGE_WIDTH); if (value==null) return null; return value + " pixels"; @@ -102,7 +101,7 @@ public class JpegDescriptor extends TagDescriptor<JpegDirectory> @Nullable public String getImageHeightDescription() { - final String value = _directory.getString(JpegDirectory.TAG_IMAGE_HEIGHT); + final String value = _directory.getString(TAG_IMAGE_HEIGHT); if (value==null) return null; return value + " pixels"; @@ -111,7 +110,7 @@ public class JpegDescriptor extends TagDescriptor<JpegDirectory> @Nullable public String getDataPrecisionDescription() { - final String value = _directory.getString(JpegDirectory.TAG_DATA_PRECISION); + final String value = _directory.getString(TAG_DATA_PRECISION); if (value==null) return null; return value + " bits"; @@ -125,8 +124,6 @@ public class JpegDescriptor extends TagDescriptor<JpegDirectory> if (value==null) return null; - return value.getComponentName() + " component: Quantization table " + value.getQuantizationTableNumber() - + ", Sampling factors " + value.getHorizontalSamplingFactor() - + " horiz/" + value.getVerticalSamplingFactor() + " vert"; + return value.getComponentName() + " component: " + value; } } diff --git a/Source/com/drew/metadata/jpeg/JpegDhtReader.java b/Source/com/drew/metadata/jpeg/JpegDhtReader.java new file mode 100644 index 0000000..f8f7691 --- /dev/null +++ b/Source/com/drew/metadata/jpeg/JpegDhtReader.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.jpeg; + +import com.drew.imaging.jpeg.JpegSegmentMetadataReader; +import com.drew.imaging.jpeg.JpegSegmentType; +import com.drew.lang.SequentialByteArrayReader; +import com.drew.lang.SequentialReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; +import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable; +import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable.HuffmanTableClass; +import java.io.IOException; +import java.util.Collections; + +/** + * Reader for JPEG Huffman tables, found in the DHT JPEG segment. + * + * @author Nadahar + */ +public class JpegDhtReader implements JpegSegmentMetadataReader +{ + @NotNull + public Iterable<JpegSegmentType> getSegmentTypes() + { + return Collections.singletonList(JpegSegmentType.DHT); + } + + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) + { + for (byte[] segmentBytes : segments) { + extract(new SequentialByteArrayReader(segmentBytes), metadata); + } + } + + /** + * Performs the DHT tables extraction, adding found tables to the specified + * instance of {@link Metadata}. + */ + public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata) + { + HuffmanTablesDirectory directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class); + if (directory == null) { + directory = new HuffmanTablesDirectory(); + metadata.addDirectory(directory); + } + + try { + while (reader.available() > 0) { + byte header = reader.getByte(); + HuffmanTableClass tableClass = HuffmanTableClass.typeOf((header & 0xF0) >> 4); + int tableDestinationId = header & 0xF; + + byte[] lBytes = getBytes(reader, 16); + int vCount = 0; + for (byte b : lBytes) { + vCount += (b & 0xFF); + } + byte[] vBytes = getBytes(reader, vCount); + directory.getTables().add(new HuffmanTable(tableClass, tableDestinationId, lBytes, vBytes)); + } + } catch (IOException me) { + directory.addError(me.getMessage()); + } + + directory.setInt(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES, directory.getTables().size()); + } + + private byte[] getBytes(@NotNull final SequentialReader reader, int count) throws IOException { + byte[] bytes = new byte[count]; + for (int i = 0; i < count; i++) { + byte b = reader.getByte(); + if ((b & 0xFF) == 0xFF) { + byte stuffing = reader.getByte(); + if (stuffing != 0x00) { + throw new IOException("Marker " + JpegSegmentType.fromByte(stuffing) + " found inside DHT segment"); + } + } + bytes[i] = b; + } + return bytes; + } +} diff --git a/Source/com/drew/metadata/jpeg/JpegDirectory.java b/Source/com/drew/metadata/jpeg/JpegDirectory.java index 37eb488..09a3004 100644 --- a/Source/com/drew/metadata/jpeg/JpegDirectory.java +++ b/Source/com/drew/metadata/jpeg/JpegDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import java.util.HashMap; * * @author Darrell Silver http://www.darrellsilver.com and Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class JpegDirectory extends Directory { public static final int TAG_COMPRESSION_TYPE = -3; diff --git a/Source/com/drew/metadata/jpeg/JpegDnlReader.java b/Source/com/drew/metadata/jpeg/JpegDnlReader.java new file mode 100644 index 0000000..f7ed753 --- /dev/null +++ b/Source/com/drew/metadata/jpeg/JpegDnlReader.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.jpeg; + +import com.drew.imaging.jpeg.JpegSegmentMetadataReader; +import com.drew.imaging.jpeg.JpegSegmentType; +import com.drew.lang.SequentialByteArrayReader; +import com.drew.lang.SequentialReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.ErrorDirectory; +import com.drew.metadata.Metadata; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +/** + * Decodes JPEG DNL data, adjusting the image height with information missing from the JPEG SOFx segment. + * + * @author Nadahar + */ +public class JpegDnlReader implements JpegSegmentMetadataReader +{ + @NotNull + public Iterable<JpegSegmentType> getSegmentTypes() + { + return Collections.singletonList(JpegSegmentType.DNL); + } + + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) + { + for (byte[] segmentBytes : segments) { + extract(segmentBytes, metadata, segmentType); + } + } + + public void extract(byte[] segmentBytes, Metadata metadata, JpegSegmentType segmentType) + { + JpegDirectory directory = metadata.getFirstDirectoryOfType(JpegDirectory.class); + if (directory == null) { + ErrorDirectory errorDirectory = new ErrorDirectory(); + metadata.addDirectory(errorDirectory); + errorDirectory.addError("DNL segment found without SOFx - illegal JPEG format"); + return; + } + + SequentialReader reader = new SequentialByteArrayReader(segmentBytes); + + try { + // Only set height from DNL if it's not already defined + Integer i = directory.getInteger(JpegDirectory.TAG_IMAGE_HEIGHT); + if (i == null || i == 0) { + directory.setInt(JpegDirectory.TAG_IMAGE_HEIGHT, reader.getUInt16()); + } + } catch (IOException ex) { + directory.addError(ex.getMessage()); + } + } +} diff --git a/Source/com/drew/metadata/jpeg/JpegReader.java b/Source/com/drew/metadata/jpeg/JpegReader.java index 99389fe..9e6bf66 100644 --- a/Source/com/drew/metadata/jpeg/JpegReader.java +++ b/Source/com/drew/metadata/jpeg/JpegReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,7 @@ public class JpegReader implements JpegSegmentMetadataReader JpegSegmentType.SOF5, JpegSegmentType.SOF6, JpegSegmentType.SOF7, - JpegSegmentType.SOF8, +// JpegSegmentType.JPG, JpegSegmentType.SOF9, JpegSegmentType.SOF10, JpegSegmentType.SOF11, @@ -62,20 +62,17 @@ public class JpegReader implements JpegSegmentMetadataReader ); } - public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType) + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) { - return true; + for (byte[] segmentBytes : segments) { + extract(segmentBytes, metadata, segmentType); + } } - public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) + public void extract(byte[] segmentBytes, Metadata metadata, JpegSegmentType segmentType) { - if (metadata.containsDirectory(JpegDirectory.class)) { - // If this directory is already present, discontinue this operation. - // We only store metadata for the *first* matching SOFn segment. - return; - } - - JpegDirectory directory = metadata.getOrCreateDirectory(JpegDirectory.class); + JpegDirectory directory = new JpegDirectory(); + metadata.addDirectory(directory); // The value of TAG_COMPRESSION_TYPE is determined by the segment type found directory.setInt(JpegDirectory.TAG_COMPRESSION_TYPE, segmentType.byteValue - JpegSegmentType.SOF0.byteValue); @@ -100,7 +97,6 @@ public class JpegReader implements JpegSegmentMetadataReader final JpegComponent component = new JpegComponent(componentId, samplingFactorByte, quantizationTableNumber); directory.setObject(JpegDirectory.TAG_COMPONENT_DATA_1 + i, component); } - } catch (IOException ex) { directory.addError(ex.getMessage()); } diff --git a/Source/com/drew/metadata/jpeg/package-info.java b/Source/com/drew/metadata/jpeg/package-info.java new file mode 100644 index 0000000..ad36e22 --- /dev/null +++ b/Source/com/drew/metadata/jpeg/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of JPEG file format metadata. + */ +package com.drew.metadata.jpeg; diff --git a/Source/com/drew/metadata/jpeg/package.html b/Source/com/drew/metadata/jpeg/package.html deleted file mode 100644 index 5e9b32a..0000000 --- a/Source/com/drew/metadata/jpeg/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of JPEG file format metadata. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/metadata/package-info.java b/Source/com/drew/metadata/package-info.java new file mode 100644 index 0000000..2037413 --- /dev/null +++ b/Source/com/drew/metadata/package-info.java @@ -0,0 +1,6 @@ +/** + * Provides classes for generic modelling of metadata directories and tags. + * <p /> + * Contains base types for metadata processing abstraction. + */ +package com.drew.metadata; diff --git a/Source/com/drew/metadata/package.html b/Source/com/drew/metadata/package.html deleted file mode 100644 index f7b7916..0000000 --- a/Source/com/drew/metadata/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Provides classes for generic modelling of metadata directories and tags. Contains base types for metadata processing abstraction. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/metadata/pcx/PcxDescriptor.java b/Source/com/drew/metadata/pcx/PcxDescriptor.java new file mode 100644 index 0000000..6800624 --- /dev/null +++ b/Source/com/drew/metadata/pcx/PcxDescriptor.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.pcx; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +import static com.drew.metadata.pcx.PcxDirectory.*; + +/** + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PcxDescriptor extends TagDescriptor<PcxDirectory> +{ + public PcxDescriptor(@NotNull PcxDirectory directory) + { + super(directory); + } + + @Override + public String getDescription(int tagType) + { + switch (tagType) { + case TAG_VERSION: + return getVersionDescription(); + case TAG_COLOR_PLANES: + return getColorPlanesDescription(); + case TAG_PALETTE_TYPE: + return getPaletteTypeDescription(); + default: + return super.getDescription(tagType); + } + } + + @Nullable + public String getVersionDescription() + { + // Prior to v2.5 of PC Paintbrush, the PCX image file format was considered proprietary information + // by ZSoft Corporation + + return getIndexedDescription(TAG_VERSION, + "2.5 with fixed EGA palette information", + null, + "2.8 with modifiable EGA palette information", + "2.8 without palette information (default palette)", + "PC Paintbrush for Windows", + "3.0 or better"); + } + + @Nullable + public String getColorPlanesDescription() + { + return getIndexedDescription(TAG_COLOR_PLANES, 3, + "24-bit color", + "16 colors"); + } + + @Nullable + public String getPaletteTypeDescription() + { + return getIndexedDescription(TAG_PALETTE_TYPE, 1, + "Color or B&W", + "Grayscale"); + } +} diff --git a/Source/com/drew/metadata/pcx/PcxDirectory.java b/Source/com/drew/metadata/pcx/PcxDirectory.java new file mode 100644 index 0000000..5af228f --- /dev/null +++ b/Source/com/drew/metadata/pcx/PcxDirectory.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.pcx; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class PcxDirectory extends Directory +{ + public static final int TAG_VERSION = 1; + public static final int TAG_BITS_PER_PIXEL = 2; + public static final int TAG_XMIN = 3; + public static final int TAG_YMIN = 4; + public static final int TAG_XMAX = 5; + public static final int TAG_YMAX = 6; + public static final int TAG_HORIZONTAL_DPI = 7; + public static final int TAG_VERTICAL_DPI = 8; + public static final int TAG_PALETTE = 9; + public static final int TAG_COLOR_PLANES = 10; + public static final int TAG_BYTES_PER_LINE = 11; + public static final int TAG_PALETTE_TYPE = 12; + public static final int TAG_HSCR_SIZE = 13; + public static final int TAG_VSCR_SIZE = 14; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TAG_VERSION, "Version"); + _tagNameMap.put(TAG_BITS_PER_PIXEL, "Bits Per Pixel"); + _tagNameMap.put(TAG_XMIN, "X Min"); + _tagNameMap.put(TAG_YMIN, "Y Min"); + _tagNameMap.put(TAG_XMAX, "X Max"); + _tagNameMap.put(TAG_YMAX, "Y Max"); + _tagNameMap.put(TAG_HORIZONTAL_DPI, "Horizontal DPI"); + _tagNameMap.put(TAG_VERTICAL_DPI, "Vertical DPI"); + _tagNameMap.put(TAG_PALETTE, "Palette"); + _tagNameMap.put(TAG_COLOR_PLANES, "Color Planes"); + _tagNameMap.put(TAG_BYTES_PER_LINE, "Bytes Per Line"); + _tagNameMap.put(TAG_PALETTE_TYPE, "Palette Type"); + _tagNameMap.put(TAG_HSCR_SIZE, "H Scr Size"); + _tagNameMap.put(TAG_VSCR_SIZE, "V Scr Size"); + } + + public PcxDirectory() + { + this.setDescriptor(new PcxDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "PCX"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/pcx/PcxReader.java b/Source/com/drew/metadata/pcx/PcxReader.java new file mode 100644 index 0000000..4acf82d --- /dev/null +++ b/Source/com/drew/metadata/pcx/PcxReader.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.pcx; + +import com.drew.imaging.ImageProcessingException; +import com.drew.lang.SequentialReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; + +/** + * Reads PCX image file metadata. + * + * <ul> + * <li>https://courses.engr.illinois.edu/ece390/books/labmanual/graphics-pcx.html</li> + * <li>http://www.fileformat.info/format/pcx/egff.htm</li> + * <li>http://fileformats.archiveteam.org/wiki/PCX</li> + * </ul> + * + * @author Drew Noakes https://drewnoakes.com + */ +public class PcxReader +{ + public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata) + { + reader.setMotorolaByteOrder(false); + + PcxDirectory directory = new PcxDirectory(); + metadata.addDirectory(directory); + + try { + byte identifier = reader.getInt8(); + if (identifier != 0x0A) + throw new ImageProcessingException("Invalid PCX identifier byte"); + + directory.setInt(PcxDirectory.TAG_VERSION, reader.getInt8()); + + byte encoding = reader.getInt8(); + if (encoding != 0x01) + throw new ImageProcessingException("Invalid PCX encoding byte"); + + directory.setInt(PcxDirectory.TAG_BITS_PER_PIXEL, reader.getUInt8()); + directory.setInt(PcxDirectory.TAG_XMIN, reader.getUInt16()); + directory.setInt(PcxDirectory.TAG_YMIN, reader.getUInt16()); + directory.setInt(PcxDirectory.TAG_XMAX, reader.getUInt16()); + directory.setInt(PcxDirectory.TAG_YMAX, reader.getUInt16()); + directory.setInt(PcxDirectory.TAG_HORIZONTAL_DPI, reader.getUInt16()); + directory.setInt(PcxDirectory.TAG_VERTICAL_DPI, reader.getUInt16()); + directory.setByteArray(PcxDirectory.TAG_PALETTE, reader.getBytes(48)); + reader.skip(1); + directory.setInt(PcxDirectory.TAG_COLOR_PLANES, reader.getUInt8()); + directory.setInt(PcxDirectory.TAG_BYTES_PER_LINE, reader.getUInt16()); + + int paletteType = reader.getUInt16(); + if (paletteType != 0) + directory.setInt(PcxDirectory.TAG_PALETTE_TYPE, paletteType); + + int hScrSize = reader.getUInt16(); + if (hScrSize != 0) + directory.setInt(PcxDirectory.TAG_HSCR_SIZE, hScrSize); + + int vScrSize = reader.getUInt16(); + if (vScrSize != 0) + directory.setInt(PcxDirectory.TAG_VSCR_SIZE, vScrSize); + + } catch (Exception ex) { + directory.addError("Exception reading PCX file metadata: " + ex.getMessage()); + } + } +} diff --git a/Source/com/drew/metadata/pcx/package-info.java b/Source/com/drew/metadata/pcx/package-info.java new file mode 100644 index 0000000..0c7bc07 --- /dev/null +++ b/Source/com/drew/metadata/pcx/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of PCX image file metadata. + */ +package com.drew.metadata.pcx; diff --git a/Source/com/drew/metadata/photoshop/DuckyDirectory.java b/Source/com/drew/metadata/photoshop/DuckyDirectory.java new file mode 100644 index 0000000..5a9c41c --- /dev/null +++ b/Source/com/drew/metadata/photoshop/DuckyDirectory.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.photoshop; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; +import com.drew.metadata.TagDescriptor; + +import java.util.HashMap; + +/** + * Holds the data found in Photoshop "ducky" segments, created during Save-for-Web. + * + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class DuckyDirectory extends Directory +{ + public static final int TAG_QUALITY = 1; + public static final int TAG_COMMENT = 2; + public static final int TAG_COPYRIGHT = 3; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TAG_QUALITY, "Quality"); + _tagNameMap.put(TAG_COMMENT, "Comment"); + _tagNameMap.put(TAG_COPYRIGHT, "Copyright"); + } + + public DuckyDirectory() + { + this.setDescriptor(new TagDescriptor<DuckyDirectory>(this)); + } + + @Override + @NotNull + public String getName() + { + return "Ducky"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/photoshop/DuckyReader.java b/Source/com/drew/metadata/photoshop/DuckyReader.java new file mode 100644 index 0000000..f740aca --- /dev/null +++ b/Source/com/drew/metadata/photoshop/DuckyReader.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.photoshop; + +import com.drew.imaging.jpeg.JpegSegmentMetadataReader; +import com.drew.imaging.jpeg.JpegSegmentType; +import com.drew.lang.Charsets; +import com.drew.lang.SequentialByteArrayReader; +import com.drew.lang.SequentialReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; + +import java.io.IOException; +import java.util.Collections; + +/** + * Reads Photoshop "ducky" segments, created during Save-for-Web. + * + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class DuckyReader implements JpegSegmentMetadataReader +{ + @NotNull + private static final String JPEG_SEGMENT_PREAMBLE = "Ducky"; + + @NotNull + public Iterable<JpegSegmentType> getSegmentTypes() + { + return Collections.singletonList(JpegSegmentType.APPC); + } + + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) + { + final int preambleLength = JPEG_SEGMENT_PREAMBLE.length(); + + for (byte[] segmentBytes : segments) { + // Ensure data starts with the necessary preamble + if (segmentBytes.length < preambleLength || !JPEG_SEGMENT_PREAMBLE.equals(new String(segmentBytes, 0, preambleLength))) + continue; + + extract( + new SequentialByteArrayReader(segmentBytes, preambleLength), + metadata); + } + } + + public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata) + { + DuckyDirectory directory = new DuckyDirectory(); + metadata.addDirectory(directory); + + try + { + while (true) + { + int tag = reader.getUInt16(); + + // End of Segment is marked with zero + if (tag == 0) + break; + + int length = reader.getUInt16(); + + switch (tag) + { + case DuckyDirectory.TAG_QUALITY: + { + if (length != 4) + { + directory.addError("Unexpected length for the quality tag"); + return; + } + directory.setInt(tag, reader.getInt32()); + break; + } + case DuckyDirectory.TAG_COMMENT: + case DuckyDirectory.TAG_COPYRIGHT: + { + reader.skip(4); + directory.setStringValue(tag, reader.getStringValue(length - 4, Charsets.UTF_16BE)); + break; + } + default: + { + // Unexpected tag + directory.setByteArray(tag, reader.getBytes(length)); + break; + } + } + } + } + catch (IOException e) + { + directory.addError(e.getMessage()); + } + } +} diff --git a/Source/com/drew/metadata/photoshop/PhotoshopDescriptor.java b/Source/com/drew/metadata/photoshop/PhotoshopDescriptor.java index 5ca1098..0465641 100644 --- a/Source/com/drew/metadata/photoshop/PhotoshopDescriptor.java +++ b/Source/com/drew/metadata/photoshop/PhotoshopDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,11 +27,15 @@ import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; import java.io.IOException; +import java.text.DecimalFormat; + +import static com.drew.metadata.photoshop.PhotoshopDirectory.*; /** * @author Yuri Binev * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> { public PhotoshopDescriptor(@NotNull PhotoshopDirectory directory) @@ -43,32 +47,32 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> public String getDescription(int tagType) { switch (tagType) { - case PhotoshopDirectory.TAG_THUMBNAIL: - case PhotoshopDirectory.TAG_THUMBNAIL_OLD: + case TAG_THUMBNAIL: + case TAG_THUMBNAIL_OLD: return getThumbnailDescription(tagType); - case PhotoshopDirectory.TAG_URL: - case PhotoshopDirectory.TAG_XML: + case TAG_URL: + case TAG_XML: return getSimpleString(tagType); - case PhotoshopDirectory.TAG_IPTC: + case TAG_IPTC: return getBinaryDataString(tagType); - case PhotoshopDirectory.TAG_SLICES: + case TAG_SLICES: return getSlicesDescription(); - case PhotoshopDirectory.TAG_VERSION: + case TAG_VERSION: return getVersionDescription(); - case PhotoshopDirectory.TAG_COPYRIGHT: + case TAG_COPYRIGHT: return getBooleanString(tagType); - case PhotoshopDirectory.TAG_RESOLUTION_INFO: + case TAG_RESOLUTION_INFO: return getResolutionInfoDescription(); - case PhotoshopDirectory.TAG_GLOBAL_ANGLE: - case PhotoshopDirectory.TAG_GLOBAL_ALTITUDE: - case PhotoshopDirectory.TAG_URL_LIST: - case PhotoshopDirectory.TAG_SEED_NUMBER: + case TAG_GLOBAL_ANGLE: + case TAG_GLOBAL_ALTITUDE: + case TAG_URL_LIST: + case TAG_SEED_NUMBER: return get32BitNumberString(tagType); - case PhotoshopDirectory.TAG_JPEG_QUALITY: + case TAG_JPEG_QUALITY: return getJpegQualityString(); - case PhotoshopDirectory.TAG_PRINT_SCALE: + case TAG_PRINT_SCALE: return getPrintScaleDescription(); - case PhotoshopDirectory.TAG_PIXEL_ASPECT_RATIO: + case TAG_PIXEL_ASPECT_RATIO: return getPixelAspectRatioString(); default: return super.getDescription(tagType); @@ -79,21 +83,21 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> public String getJpegQualityString() { try { - byte[] b = _directory.getByteArray(PhotoshopDirectory.TAG_JPEG_QUALITY); + byte[] b = _directory.getByteArray(TAG_JPEG_QUALITY); + if (b == null) - return _directory.getString(PhotoshopDirectory.TAG_JPEG_QUALITY); + return _directory.getString(TAG_JPEG_QUALITY); + RandomAccessReader reader = new ByteArrayReader(b); int q = reader.getUInt16(0); // & 0xFFFF; int f = reader.getUInt16(2); // & 0xFFFF; int s = reader.getUInt16(4); - int q1; - if (q <= 0xFFFF && q >= 0xFFFD) - q1 = q - 0xFFFC; - else if (q <= 8) - q1 = q + 4; - else - q1 = q; + int q1 = q <= 0xFFFF && q >= 0xFFFD + ? q - 0xFFFC + : q <= 8 + ? q + 4 + : q; String quality; switch (q) { @@ -120,6 +124,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> default: quality = "Unknown"; } + String format; switch (f) { case 0x0000: @@ -129,14 +134,16 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> format = "Optimised"; break; case 0x0101: - format = "Progressive "; + format = "Progressive"; break; default: format = String.format("Unknown 0x%04X", f); } + String scans = s >= 1 && s <= 3 ? String.format("%d", s + 2) : String.format("Unknown 0x%04X", s); + return String.format("%d (%s), %s format, %s scans", q1, quality, format, scans); } catch (IOException e) { return null; @@ -147,7 +154,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> public String getPixelAspectRatioString() { try { - byte[] bytes = _directory.getByteArray(PhotoshopDirectory.TAG_PIXEL_ASPECT_RATIO); + byte[] bytes = _directory.getByteArray(TAG_PIXEL_ASPECT_RATIO); if (bytes == null) return null; RandomAccessReader reader = new ByteArrayReader(bytes); @@ -162,7 +169,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> public String getPrintScaleDescription() { try { - byte bytes[] = _directory.getByteArray(PhotoshopDirectory.TAG_PRINT_SCALE); + byte bytes[] = _directory.getByteArray(TAG_PRINT_SCALE); if (bytes == null) return null; RandomAccessReader reader = new ByteArrayReader(bytes); @@ -189,13 +196,14 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> public String getResolutionInfoDescription() { try { - byte[] bytes = _directory.getByteArray(PhotoshopDirectory.TAG_RESOLUTION_INFO); + byte[] bytes = _directory.getByteArray(TAG_RESOLUTION_INFO); if (bytes == null) return null; RandomAccessReader reader = new ByteArrayReader(bytes); float resX = reader.getS15Fixed16(0); float resY = reader.getS15Fixed16(8); // is this the correct offset? it's only reading 4 bytes each time - return resX + "x" + resY + " DPI"; + DecimalFormat format = new DecimalFormat("0.##"); + return format.format(resX) + "x" + format.format(resY) + " DPI"; } catch (Exception e) { return null; } @@ -205,7 +213,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> public String getVersionDescription() { try { - final byte[] bytes = _directory.getByteArray(PhotoshopDirectory.TAG_VERSION); + final byte[] bytes = _directory.getByteArray(TAG_VERSION); if (bytes == null) return null; RandomAccessReader reader = new ByteArrayReader(bytes); @@ -232,7 +240,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> public String getSlicesDescription() { try { - final byte bytes[] = _directory.getByteArray(PhotoshopDirectory.TAG_SLICES); + final byte bytes[] = _directory.getByteArray(TAG_SLICES); if (bytes == null) return null; RandomAccessReader reader = new ByteArrayReader(bytes); @@ -240,16 +248,8 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> String name = reader.getString(24, nameLength * 2, "UTF-16"); int pos = 24 + nameLength * 2; int sliceCount = reader.getInt32(pos); - //pos += 4; return String.format("%s (%d,%d,%d,%d) %d Slices", name, reader.getInt32(4), reader.getInt32(8), reader.getInt32(12), reader.getInt32(16), sliceCount); - /*for (int i=0;i<sliceCount;i++){ - pos+=16; - int slNameLen=getInt32(b,pos); - pos+=4; - String slName=new String(b, pos, slNameLen*2,"UTF-16"); - res+=slName; - }*/ } catch (IOException e) { return null; } @@ -263,22 +263,14 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> if (v == null) return null; RandomAccessReader reader = new ByteArrayReader(v); - //int pos = 0; int format = reader.getInt32(0); - //pos += 4; int width = reader.getInt32(4); - //pos += 4; int height = reader.getInt32(8); - //pos += 4; - //pos += 4; //skip WidthBytes + //skip WidthBytes int totalSize = reader.getInt32(16); - //pos += 4; int compSize = reader.getInt32(20); - //pos += 4; int bpp = reader.getInt32(24); - //pos+=2; - //pos+=2; //skip Number of planes - //int thumbSize=v.length-pos; + //skip Number of planes return String.format("%s, %dx%d, Decomp %d bytes, %d bpp, %d bytes", format == 1 ? "JpegRGB" : "RawRGB", width, height, totalSize, bpp, compSize); @@ -291,7 +283,7 @@ public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory> private String getBooleanString(int tag) { final byte[] bytes = _directory.getByteArray(tag); - if (bytes == null) + if (bytes == null || bytes.length == 0) return null; return bytes[0] == 0 ? "No" : "Yes"; } diff --git a/Source/com/drew/metadata/photoshop/PhotoshopDirectory.java b/Source/com/drew/metadata/photoshop/PhotoshopDirectory.java index 7e80ecc..bd20fde 100644 --- a/Source/com/drew/metadata/photoshop/PhotoshopDirectory.java +++ b/Source/com/drew/metadata/photoshop/PhotoshopDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,53 +30,100 @@ import java.util.HashMap; /** * Holds the metadata found in the APPD segment of a JPEG file saved by Photoshop. * - * @author Yuri Binev, Drew Noakes https://drewnoakes.com + * @author Drew Noakes https://drewnoakes.com + * @author Yuri Binev */ +@SuppressWarnings("WeakerAccess") public class PhotoshopDirectory extends Directory { - public static final int TAG_CHANNELS_ROWS_COLUMNS_DEPTH_MODE = 0x03E8; - public static final int TAG_MAC_PRINT_INFO = 0x03E9; - public static final int TAG_XML = 0x03EA; - public static final int TAG_INDEXED_COLOR_TABLE = 0x03EB; - public static final int TAG_RESOLUTION_INFO = 0x03ED; - public static final int TAG_ALPHA_CHANNELS = 0x03EE; - public static final int TAG_DISPLAY_INFO = 0x03EF; - public static final int TAG_CAPTION = 0x03F0; - public static final int TAG_BORDER_INFORMATION = 0x03F1; - public static final int TAG_BACKGROUND_COLOR = 0x03F2; - public static final int TAG_PRINT_FLAGS = 0x03F3; + public static final int TAG_CHANNELS_ROWS_COLUMNS_DEPTH_MODE = 0x03E8; + public static final int TAG_MAC_PRINT_INFO = 0x03E9; + public static final int TAG_XML = 0x03EA; + public static final int TAG_INDEXED_COLOR_TABLE = 0x03EB; + public static final int TAG_RESOLUTION_INFO = 0x03ED; + public static final int TAG_ALPHA_CHANNELS = 0x03EE; + public static final int TAG_DISPLAY_INFO_OBSOLETE = 0x03EF; + public static final int TAG_CAPTION = 0x03F0; + public static final int TAG_BORDER_INFORMATION = 0x03F1; + public static final int TAG_BACKGROUND_COLOR = 0x03F2; + public static final int TAG_PRINT_FLAGS = 0x03F3; public static final int TAG_GRAYSCALE_AND_MULTICHANNEL_HALFTONING_INFORMATION = 0x03F4; - public static final int TAG_COLOR_HALFTONING_INFORMATION = 0x03F5; - public static final int TAG_DUOTONE_HALFTONING_INFORMATION = 0x03F6; - public static final int TAG_GRAYSCALE_AND_MULTICHANNEL_TRANSFER_FUNCTION = 0x03F7; - public static final int TAG_COLOR_TRANSFER_FUNCTIONS = 0x03F8; - public static final int TAG_DUOTONE_TRANSFER_FUNCTIONS = 0x03F9; - public static final int TAG_DUOTONE_IMAGE_INFORMATION = 0x03FA; - public static final int TAG_EFFECTIVE_BLACK_AND_WHITE_VALUES = 0x03FB; - public static final int TAG_EPS_OPTIONS = 0x03FD; - public static final int TAG_QUICK_MASK_INFORMATION = 0x03FE; - public static final int TAG_LAYER_STATE_INFORMATION = 0x0400; - public static final int TAG_LAYERS_GROUP_INFORMATION = 0x0402; - public static final int TAG_IPTC = 0x0404; - public static final int TAG_IMAGE_MODE_FOR_RAW_FORMAT_FILES = 0x0405; - public static final int TAG_JPEG_QUALITY = 0x0406; - public static final int TAG_GRID_AND_GUIDES_INFORMATION = 0x0408; - public static final int TAG_THUMBNAIL_OLD = 0x0409; - public static final int TAG_COPYRIGHT = 0x040A; - public static final int TAG_URL = 0x040B; - public static final int TAG_THUMBNAIL = 0x040C; - public static final int TAG_GLOBAL_ANGLE = 0x040D; - public static final int TAG_ICC_UNTAGGED_PROFILE = 0x0411; - public static final int TAG_SEED_NUMBER = 0x0414; - public static final int TAG_GLOBAL_ALTITUDE = 0x0419; - public static final int TAG_SLICES = 0x041A; - public static final int TAG_URL_LIST = 0x041E; - public static final int TAG_VERSION = 0x0421; - public static final int TAG_CAPTION_DIGEST = 0x0425; - public static final int TAG_PRINT_SCALE = 0x0426; - public static final int TAG_PIXEL_ASPECT_RATIO = 0x0428; - public static final int TAG_PRINT_INFO = 0x042F; - public static final int TAG_PRINT_FLAGS_INFO = 0x2710; + public static final int TAG_COLOR_HALFTONING_INFORMATION = 0x03F5; + public static final int TAG_DUOTONE_HALFTONING_INFORMATION = 0x03F6; + public static final int TAG_GRAYSCALE_AND_MULTICHANNEL_TRANSFER_FUNCTION = 0x03F7; + public static final int TAG_COLOR_TRANSFER_FUNCTIONS = 0x03F8; + public static final int TAG_DUOTONE_TRANSFER_FUNCTIONS = 0x03F9; + public static final int TAG_DUOTONE_IMAGE_INFORMATION = 0x03FA; + public static final int TAG_EFFECTIVE_BLACK_AND_WHITE_VALUES = 0x03FB; + // OBSOLETE 0x03FC + public static final int TAG_EPS_OPTIONS = 0x03FD; + public static final int TAG_QUICK_MASK_INFORMATION = 0x03FE; + // OBSOLETE 0x03FF + public static final int TAG_LAYER_STATE_INFORMATION = 0x0400; + // Working path (not saved) 0x0401 + public static final int TAG_LAYERS_GROUP_INFORMATION = 0x0402; + // OBSOLETE 0x0403 + public static final int TAG_IPTC = 0x0404; + public static final int TAG_IMAGE_MODE_FOR_RAW_FORMAT_FILES = 0x0405; + public static final int TAG_JPEG_QUALITY = 0x0406; + public static final int TAG_GRID_AND_GUIDES_INFORMATION = 0x0408; + public static final int TAG_THUMBNAIL_OLD = 0x0409; + public static final int TAG_COPYRIGHT = 0x040A; + public static final int TAG_URL = 0x040B; + public static final int TAG_THUMBNAIL = 0x040C; + public static final int TAG_GLOBAL_ANGLE = 0x040D; + // OBSOLETE 0x040E + public static final int TAG_ICC_PROFILE_BYTES = 0x040F; + public static final int TAG_WATERMARK = 0x0410; + public static final int TAG_ICC_UNTAGGED_PROFILE = 0x0411; + public static final int TAG_EFFECTS_VISIBLE = 0x0412; + public static final int TAG_SPOT_HALFTONE = 0x0413; + public static final int TAG_SEED_NUMBER = 0x0414; + public static final int TAG_UNICODE_ALPHA_NAMES = 0x0415; + public static final int TAG_INDEXED_COLOR_TABLE_COUNT = 0x0416; + public static final int TAG_TRANSPARENCY_INDEX = 0x0417; + public static final int TAG_GLOBAL_ALTITUDE = 0x0419; + public static final int TAG_SLICES = 0x041A; + public static final int TAG_WORKFLOW_URL = 0x041B; + public static final int TAG_JUMP_TO_XPEP = 0x041C; + public static final int TAG_ALPHA_IDENTIFIERS = 0x041D; + public static final int TAG_URL_LIST = 0x041E; + public static final int TAG_VERSION = 0x0421; + public static final int TAG_EXIF_DATA_1 = 0x0422; + public static final int TAG_EXIF_DATA_3 = 0x0423; + public static final int TAG_XMP_DATA = 0x0424; + public static final int TAG_CAPTION_DIGEST = 0x0425; + public static final int TAG_PRINT_SCALE = 0x0426; + public static final int TAG_PIXEL_ASPECT_RATIO = 0x0428; + public static final int TAG_LAYER_COMPS = 0x0429; + public static final int TAG_ALTERNATE_DUOTONE_COLORS = 0x042A; + public static final int TAG_ALTERNATE_SPOT_COLORS = 0x042B; + public static final int TAG_LAYER_SELECTION_IDS = 0x042D; + public static final int TAG_HDR_TONING_INFO = 0x042E; + public static final int TAG_PRINT_INFO = 0x042F; + public static final int TAG_LAYER_GROUPS_ENABLED_ID = 0x0430; + public static final int TAG_COLOR_SAMPLERS = 0x0431; + public static final int TAG_MEASUREMENT_SCALE = 0x0432; + public static final int TAG_TIMELINE_INFORMATION = 0x0433; + public static final int TAG_SHEET_DISCLOSURE = 0x0434; + public static final int TAG_DISPLAY_INFO = 0x0435; + public static final int TAG_ONION_SKINS = 0x0436; + public static final int TAG_COUNT_INFORMATION = 0x0438; + public static final int TAG_PRINT_INFO_2 = 0x043A; + public static final int TAG_PRINT_STYLE = 0x043B; + public static final int TAG_MAC_NSPRINTINFO = 0x043C; + public static final int TAG_WIN_DEVMODE = 0x043D; + public static final int TAG_AUTO_SAVE_FILE_PATH = 0x043E; + public static final int TAG_AUTO_SAVE_FORMAT = 0x043F; + public static final int TAG_PATH_SELECTION_STATE = 0x0440; + // CLIPPING PATHS 0x07D0 -> 0x0BB6 + public static final int TAG_CLIPPING_PATH_NAME = 0x0BB7; + public static final int TAG_ORIGIN_PATH_INFO = 0x0BB8; + // PLUG IN RESOURCES 0x0FA0 -> 0x1387 + public static final int TAG_IMAGE_READY_VARIABLES_XML = 0x1B58; + public static final int TAG_IMAGE_READY_DATA_SETS = 0x1B59; + public static final int TAG_LIGHTROOM_WORKFLOW = 0x1F40; + public static final int TAG_PRINT_FLAGS_INFO = 0x2710; @NotNull protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); @@ -88,7 +135,7 @@ public class PhotoshopDirectory extends Directory _tagNameMap.put(TAG_INDEXED_COLOR_TABLE, "Indexed Color Table"); _tagNameMap.put(TAG_RESOLUTION_INFO, "Resolution Info"); _tagNameMap.put(TAG_ALPHA_CHANNELS, "Alpha Channels"); - _tagNameMap.put(TAG_DISPLAY_INFO, "Display Info"); + _tagNameMap.put(TAG_DISPLAY_INFO_OBSOLETE, "Display Info (Obsolete)"); _tagNameMap.put(TAG_CAPTION, "Caption"); _tagNameMap.put(TAG_BORDER_INFORMATION, "Border Information"); _tagNameMap.put(TAG_BACKGROUND_COLOR, "Background Color"); @@ -114,16 +161,54 @@ public class PhotoshopDirectory extends Directory _tagNameMap.put(TAG_URL, "URL"); _tagNameMap.put(TAG_THUMBNAIL, "Thumbnail Data"); _tagNameMap.put(TAG_GLOBAL_ANGLE, "Global Angle"); + _tagNameMap.put(TAG_ICC_PROFILE_BYTES, "ICC Profile Bytes"); + _tagNameMap.put(TAG_WATERMARK, "Watermark"); _tagNameMap.put(TAG_ICC_UNTAGGED_PROFILE, "ICC Untagged Profile"); + _tagNameMap.put(TAG_EFFECTS_VISIBLE, "Effects Visible"); + _tagNameMap.put(TAG_SPOT_HALFTONE, "Spot Halftone"); _tagNameMap.put(TAG_SEED_NUMBER, "Seed Number"); + _tagNameMap.put(TAG_UNICODE_ALPHA_NAMES, "Unicode Alpha Names"); + _tagNameMap.put(TAG_INDEXED_COLOR_TABLE_COUNT, "Indexed Color Table Count"); + _tagNameMap.put(TAG_TRANSPARENCY_INDEX, "Transparency Index"); _tagNameMap.put(TAG_GLOBAL_ALTITUDE, "Global Altitude"); _tagNameMap.put(TAG_SLICES, "Slices"); + _tagNameMap.put(TAG_WORKFLOW_URL, "Workflow URL"); + _tagNameMap.put(TAG_JUMP_TO_XPEP, "Jump To XPEP"); + _tagNameMap.put(TAG_ALPHA_IDENTIFIERS, "Alpha Identifiers"); _tagNameMap.put(TAG_URL_LIST, "URL List"); _tagNameMap.put(TAG_VERSION, "Version Info"); + _tagNameMap.put(TAG_EXIF_DATA_1, "EXIF Data 1"); + _tagNameMap.put(TAG_EXIF_DATA_3, "EXIF Data 3"); + _tagNameMap.put(TAG_XMP_DATA, "XMP Data"); _tagNameMap.put(TAG_CAPTION_DIGEST, "Caption Digest"); _tagNameMap.put(TAG_PRINT_SCALE, "Print Scale"); _tagNameMap.put(TAG_PIXEL_ASPECT_RATIO, "Pixel Aspect Ratio"); + _tagNameMap.put(TAG_LAYER_COMPS, "Layer Comps"); + _tagNameMap.put(TAG_ALTERNATE_DUOTONE_COLORS, "Alternate Duotone Colors"); + _tagNameMap.put(TAG_ALTERNATE_SPOT_COLORS, "Alternate Spot Colors"); + _tagNameMap.put(TAG_LAYER_SELECTION_IDS, "Layer Selection IDs"); + _tagNameMap.put(TAG_HDR_TONING_INFO, "HDR Toning Info"); _tagNameMap.put(TAG_PRINT_INFO, "Print Info"); + _tagNameMap.put(TAG_LAYER_GROUPS_ENABLED_ID, "Layer Groups Enabled ID"); + _tagNameMap.put(TAG_COLOR_SAMPLERS, "Color Samplers"); + _tagNameMap.put(TAG_MEASUREMENT_SCALE, "Measurement Scale"); + _tagNameMap.put(TAG_TIMELINE_INFORMATION, "Timeline Information"); + _tagNameMap.put(TAG_SHEET_DISCLOSURE, "Sheet Disclosure"); + _tagNameMap.put(TAG_DISPLAY_INFO, "Display Info"); + _tagNameMap.put(TAG_ONION_SKINS, "Onion Skins"); + _tagNameMap.put(TAG_COUNT_INFORMATION, "Count information"); + _tagNameMap.put(TAG_PRINT_INFO_2, "Print Info 2"); + _tagNameMap.put(TAG_PRINT_STYLE, "Print Style"); + _tagNameMap.put(TAG_MAC_NSPRINTINFO, "Mac NSPrintInfo"); + _tagNameMap.put(TAG_WIN_DEVMODE, "Win DEVMODE"); + _tagNameMap.put(TAG_AUTO_SAVE_FILE_PATH, "Auto Save File Path"); + _tagNameMap.put(TAG_AUTO_SAVE_FORMAT, "Auto Save Format"); + _tagNameMap.put(TAG_PATH_SELECTION_STATE, "Path Selection State"); + _tagNameMap.put(TAG_CLIPPING_PATH_NAME, "Clipping Path Name"); + _tagNameMap.put(TAG_ORIGIN_PATH_INFO, "Origin Path Info"); + _tagNameMap.put(TAG_IMAGE_READY_VARIABLES_XML, "Image Ready Variables XML"); + _tagNameMap.put(TAG_IMAGE_READY_DATA_SETS, "Image Ready Data Sets"); + _tagNameMap.put(TAG_LIGHTROOM_WORKFLOW, "Lightroom Workflow"); _tagNameMap.put(TAG_PRINT_FLAGS_INFO, "Print Flags Information"); } @@ -152,7 +237,7 @@ public class PhotoshopDirectory extends Directory byte[] storedBytes = getByteArray(PhotoshopDirectory.TAG_THUMBNAIL); if (storedBytes == null) storedBytes = getByteArray(PhotoshopDirectory.TAG_THUMBNAIL_OLD); - if (storedBytes == null) + if (storedBytes == null || storedBytes.length <= 28) return null; int thumbSize = storedBytes.length - 28; diff --git a/Source/com/drew/metadata/photoshop/PhotoshopReader.java b/Source/com/drew/metadata/photoshop/PhotoshopReader.java index 1069e06..d8ff7e2 100644 --- a/Source/com/drew/metadata/photoshop/PhotoshopReader.java +++ b/Source/com/drew/metadata/photoshop/PhotoshopReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,105 +20,125 @@ */ package com.drew.metadata.photoshop; +import com.drew.imaging.ImageProcessingException; import com.drew.imaging.jpeg.JpegSegmentMetadataReader; import com.drew.imaging.jpeg.JpegSegmentType; import com.drew.lang.ByteArrayReader; -import com.drew.lang.RandomAccessReader; import com.drew.lang.SequentialByteArrayReader; +import com.drew.lang.SequentialReader; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Metadata; -import com.drew.metadata.MetadataReader; +import com.drew.metadata.exif.ExifReader; +import com.drew.metadata.icc.IccReader; import com.drew.metadata.iptc.IptcReader; +import com.drew.metadata.xmp.XmpReader; -import java.io.IOException; -import java.util.Arrays; +import java.util.Collections; /** * Reads metadata created by Photoshop and stored in the APPD segment of JPEG files. * Note that IPTC data may be stored within this segment, in which case this reader will * create both a {@link PhotoshopDirectory} and a {@link com.drew.metadata.iptc.IptcDirectory}. * - * @author Yuri Binev, Drew Noakes https://drewnoakes.com + * @author Yuri Binev + * @author Drew Noakes https://drewnoakes.com */ -public class PhotoshopReader implements JpegSegmentMetadataReader, MetadataReader +public class PhotoshopReader implements JpegSegmentMetadataReader { + @NotNull + private static final String JPEG_SEGMENT_PREAMBLE = "Photoshop 3.0"; + @NotNull public Iterable<JpegSegmentType> getSegmentTypes() { - return Arrays.asList(JpegSegmentType.APPD); + return Collections.singletonList(JpegSegmentType.APPD); } - public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType) + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) { - return segmentBytes.length > 12 && "Photoshop 3.0".equals(new String(segmentBytes, 0, 13)); - } + final int preambleLength = JPEG_SEGMENT_PREAMBLE.length(); - public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) - { - extract(new ByteArrayReader(segmentBytes), metadata); + for (byte[] segmentBytes : segments) { + // Ensure data starts with the necessary preamble + if (segmentBytes.length < preambleLength + 1 || !JPEG_SEGMENT_PREAMBLE.equals(new String(segmentBytes, 0, preambleLength))) + continue; + + extract( + new SequentialByteArrayReader(segmentBytes, preambleLength + 1), + segmentBytes.length - preambleLength - 1, + metadata); + } } - public void extract(@NotNull final RandomAccessReader reader, final @NotNull Metadata metadata) + public void extract(@NotNull final SequentialReader reader, int length, @NotNull final Metadata metadata) { - final PhotoshopDirectory directory = metadata.getOrCreateDirectory(PhotoshopDirectory.class); + PhotoshopDirectory directory = new PhotoshopDirectory(); + metadata.addDirectory(directory); - int pos; - try { - pos = reader.getString(0, 13).equals("Photoshop 3.0") ? 14 : 0; - } catch (IOException e) { - directory.addError("Unable to read header"); - return; - } - - long length; - try { - length = reader.getLength(); - } catch (IOException e) { - directory.addError("Unable to read Photoshop data: " + e.getMessage()); - return; - } + // Data contains a sequence of Image Resource Blocks (IRBs): + // + // 4 bytes - Signature; mostly "8BIM" but "PHUT", "AgHg" and "DCSR" are also found + // 2 bytes - Resource identifier + // String - Pascal string, padded to make length even + // 4 bytes - Size of resource data which follows + // Data - The resource data, padded to make size even + // + // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037504 + int pos = 0; while (pos < length) { try { - // 4 bytes for the signature. Should always be "8BIM". - //String signature = new String(data, pos, 4); + // 4 bytes for the signature ("8BIM", "PHUT", etc.) + String signature = reader.getString(4); pos += 4; // 2 bytes for the resource identifier (tag type). - int tagType = reader.getUInt16(pos); // segment type + int tagType = reader.getUInt16(); // segment type pos += 2; // A variable number of bytes holding a pascal string (two leading bytes for length). - int descriptionLength = reader.getUInt16(pos); - pos += 2; + short descriptionLength = reader.getUInt8(); + pos += 1; // Some basic bounds checking if (descriptionLength < 0 || descriptionLength + pos > length) - return; - //String description = new String(data, pos, descriptionLength); + throw new ImageProcessingException("Invalid string length"); + // We don't use the string value here + reader.skip(descriptionLength); pos += descriptionLength; // The number of bytes is padded with a trailing zero, if needed, to make the size even. - if (pos % 2 != 0) + if (pos % 2 != 0) { + reader.skip(1); pos++; + } // 4 bytes for the size of the resource data that follows. - int byteCount = reader.getInt32(pos); + int byteCount = reader.getInt32(); pos += 4; // The resource data. - byte[] tagBytes = reader.getBytes(pos, byteCount); + byte[] tagBytes = reader.getBytes(byteCount); pos += byteCount; // The number of bytes is padded with a trailing zero, if needed, to make the size even. - if (pos % 2 != 0) + if (pos % 2 != 0) { + reader.skip(1); pos++; + } - directory.setByteArray(tagType, tagBytes); - - // TODO allow rebasing the reader with a new zero-point, rather than copying data here - if (tagType == PhotoshopDirectory.TAG_IPTC) - new IptcReader().extract(new SequentialByteArrayReader(tagBytes), metadata, tagBytes.length); + if (signature.equals("8BIM")) { + if (tagType == PhotoshopDirectory.TAG_IPTC) + new IptcReader().extract(new SequentialByteArrayReader(tagBytes), metadata, tagBytes.length, directory); + else if (tagType == PhotoshopDirectory.TAG_ICC_PROFILE_BYTES) + new IccReader().extract(new ByteArrayReader(tagBytes), metadata, directory); + else if (tagType == PhotoshopDirectory.TAG_EXIF_DATA_1 || tagType == PhotoshopDirectory.TAG_EXIF_DATA_3) + new ExifReader().extract(new ByteArrayReader(tagBytes), metadata, 0, directory); + else if (tagType == PhotoshopDirectory.TAG_XMP_DATA) + new XmpReader().extract(tagBytes, metadata, directory); + else + directory.setByteArray(tagType, tagBytes); - if (tagType >= 0x0fa0 && tagType <= 0x1387) - PhotoshopDirectory._tagNameMap.put(tagType, String.format("Plug-in %d Data", tagType - 0x0fa0 + 1)); - } catch (IOException ex) { + if (tagType >= 0x0fa0 && tagType <= 0x1387) + PhotoshopDirectory._tagNameMap.put(tagType, String.format("Plug-in %d Data", tagType - 0x0fa0 + 1)); + } + } catch (Exception ex) { directory.addError(ex.getMessage()); return; } diff --git a/Source/com/drew/metadata/photoshop/PsdHeaderDescriptor.java b/Source/com/drew/metadata/photoshop/PsdHeaderDescriptor.java index d14c235..b651ec9 100644 --- a/Source/com/drew/metadata/photoshop/PsdHeaderDescriptor.java +++ b/Source/com/drew/metadata/photoshop/PsdHeaderDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,9 +25,12 @@ import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; +import static com.drew.metadata.photoshop.PsdHeaderDirectory.*; + /** * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class PsdHeaderDescriptor extends TagDescriptor<PsdHeaderDirectory> { public PsdHeaderDescriptor(@NotNull PsdHeaderDirectory directory) @@ -39,15 +42,15 @@ public class PsdHeaderDescriptor extends TagDescriptor<PsdHeaderDirectory> public String getDescription(int tagType) { switch (tagType) { - case PsdHeaderDirectory.TAG_CHANNEL_COUNT: + case TAG_CHANNEL_COUNT: return getChannelCountDescription(); - case PsdHeaderDirectory.TAG_BITS_PER_CHANNEL: + case TAG_BITS_PER_CHANNEL: return getBitsPerChannelDescription(); - case PsdHeaderDirectory.TAG_COLOR_MODE: + case TAG_COLOR_MODE: return getColorModeDescription(); - case PsdHeaderDirectory.TAG_IMAGE_HEIGHT: + case TAG_IMAGE_HEIGHT: return getImageHeightDescription(); - case PsdHeaderDirectory.TAG_IMAGE_WIDTH: + case TAG_IMAGE_WIDTH: return getImageWidthDescription(); default: return super.getDescription(tagType); @@ -57,71 +60,53 @@ public class PsdHeaderDescriptor extends TagDescriptor<PsdHeaderDirectory> @Nullable public String getChannelCountDescription() { - try { - Integer value = _directory.getInteger(PsdHeaderDirectory.TAG_CHANNEL_COUNT); - if (value == null) - return null; - return value + " channel" + (value == 1 ? "" : "s"); - } catch (Exception e) { + // Supported range is 1 to 56. + Integer value = _directory.getInteger(TAG_CHANNEL_COUNT); + if (value == null) return null; - } + return value + " channel" + (value == 1 ? "" : "s"); } @Nullable public String getBitsPerChannelDescription() { - try { - Integer value = _directory.getInteger(PsdHeaderDirectory.TAG_BITS_PER_CHANNEL); - if (value == null) - return null; - return value + " bit" + (value == 1 ? "" : "s") + " per channel"; - } catch (Exception e) { + // Supported values are 1, 8, 16 and 32. + Integer value = _directory.getInteger(TAG_BITS_PER_CHANNEL); + if (value == null) return null; - } + return value + " bit" + (value == 1 ? "" : "s") + " per channel"; } @Nullable public String getColorModeDescription() { - // Bitmap = 0; Grayscale = 1; Indexed = 2; RGB = 3; CMYK = 4; Multichannel = 7; Duotone = 8; Lab = 9 - try { - Integer value = _directory.getInteger(PsdHeaderDirectory.TAG_COLOR_MODE); - if (value == null) - return null; - switch (value){ - case 0: return "Bitmap"; - case 1: return "Grayscale"; - case 2: return "Indexed"; - case 3: return "RGB"; - case 4: return "CMYK"; - case 7: return "Multichannel"; - case 8: return "Duotone"; - case 9: return "Lab"; - default: return "Unknown color mode (" + value + ")"; - } - } catch (Exception e) { - return null; - } + return getIndexedDescription(TAG_COLOR_MODE, + "Bitmap", + "Grayscale", + "Indexed", + "RGB", + "CMYK", + null, + null, + "Multichannel", + "Duotone", + "Lab"); } @Nullable public String getImageHeightDescription() { - try { - Integer value = _directory.getInteger(PsdHeaderDirectory.TAG_IMAGE_HEIGHT); - if (value == null) - return null; - return value + " pixel" + (value == 1 ? "" : "s"); - } catch (Exception e) { + Integer value = _directory.getInteger(TAG_IMAGE_HEIGHT); + if (value == null) return null; - } + return value + " pixel" + (value == 1 ? "" : "s"); } @Nullable public String getImageWidthDescription() { try { - Integer value = _directory.getInteger(PsdHeaderDirectory.TAG_IMAGE_WIDTH); + Integer value = _directory.getInteger(TAG_IMAGE_WIDTH); if (value == null) return null; return value + " pixel" + (value == 1 ? "" : "s"); diff --git a/Source/com/drew/metadata/photoshop/PsdHeaderDirectory.java b/Source/com/drew/metadata/photoshop/PsdHeaderDirectory.java index ad38774..43bbe5e 100644 --- a/Source/com/drew/metadata/photoshop/PsdHeaderDirectory.java +++ b/Source/com/drew/metadata/photoshop/PsdHeaderDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import java.util.HashMap; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class PsdHeaderDirectory extends Directory { /** diff --git a/Source/com/drew/metadata/photoshop/PsdReader.java b/Source/com/drew/metadata/photoshop/PsdReader.java index fe43c07..55542d7 100644 --- a/Source/com/drew/metadata/photoshop/PsdReader.java +++ b/Source/com/drew/metadata/photoshop/PsdReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,9 @@ package com.drew.metadata.photoshop; -import com.drew.lang.RandomAccessReader; +import com.drew.lang.SequentialReader; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Metadata; -import com.drew.metadata.MetadataReader; import java.io.IOException; @@ -33,21 +32,24 @@ import java.io.IOException; * * @author Drew Noakes https://drewnoakes.com */ -public class PsdReader implements MetadataReader +public class PsdReader { - public void extract(@NotNull final RandomAccessReader reader, final @NotNull Metadata metadata) + public void extract(@NotNull final SequentialReader reader, @NotNull final Metadata metadata) { - final PsdHeaderDirectory directory = metadata.getOrCreateDirectory(PsdHeaderDirectory.class); + PsdHeaderDirectory directory = new PsdHeaderDirectory(); + metadata.addDirectory(directory); + + // FILE HEADER SECTION try { - final int signature = reader.getInt32(0); - if (signature != 0x38425053) + final int signature = reader.getInt32(); + if (signature != 0x38425053) // "8BPS" { directory.addError("Invalid PSD file signature"); return; } - final int version = reader.getUInt16(4); + final int version = reader.getUInt16(); if (version != 1 && version != 2) { directory.addError("Invalid PSD file version (must be 1 or 2)"); @@ -55,25 +57,65 @@ public class PsdReader implements MetadataReader } // 6 reserved bytes are skipped here. They should be zero. + reader.skip(6); - final int channelCount = reader.getUInt16(12); + final int channelCount = reader.getUInt16(); directory.setInt(PsdHeaderDirectory.TAG_CHANNEL_COUNT, channelCount); // even though this is probably an unsigned int, the max height in practice is 300,000 - final int imageHeight = reader.getInt32(14); + final int imageHeight = reader.getInt32(); directory.setInt(PsdHeaderDirectory.TAG_IMAGE_HEIGHT, imageHeight); // even though this is probably an unsigned int, the max width in practice is 300,000 - final int imageWidth = reader.getInt32(18); + final int imageWidth = reader.getInt32(); directory.setInt(PsdHeaderDirectory.TAG_IMAGE_WIDTH, imageWidth); - final int bitsPerChannel = reader.getUInt16(22); + final int bitsPerChannel = reader.getUInt16(); directory.setInt(PsdHeaderDirectory.TAG_BITS_PER_CHANNEL, bitsPerChannel); - final int colorMode = reader.getUInt16(24); + final int colorMode = reader.getUInt16(); directory.setInt(PsdHeaderDirectory.TAG_COLOR_MODE, colorMode); } catch (IOException e) { directory.addError("Unable to read PSD header"); + return; } + + // COLOR MODE DATA SECTION + + try { + long sectionLength = reader.getUInt32(); + + /* + * Only indexed color and duotone (see the mode field in the File header section) have color mode data. + * For all other modes, this section is just the 4-byte length field, which is set to zero. + * + * Indexed color images: length is 768; color data contains the color table for the image, + * in non-interleaved order. + * Duotone images: color data contains the duotone specification (the format of which is not documented). + * Other applications that read Photoshop files can treat a duotone image as a gray image, + * and just preserve the contents of the duotone information when reading and writing the + * file. + */ + + reader.skip(sectionLength); + } catch (IOException e) { + return; + } + + // IMAGE RESOURCES SECTION + + try { + long sectionLength = reader.getUInt32(); + + assert(sectionLength <= Integer.MAX_VALUE); + + new PhotoshopReader().extract(reader, (int)sectionLength, metadata); + } catch (IOException e) { + // ignore + } + + // LAYER AND MASK INFORMATION SECTION (skipped) + + // IMAGE DATA SECTION (skipped) } } diff --git a/Source/com/drew/metadata/photoshop/package-info.java b/Source/com/drew/metadata/photoshop/package-info.java new file mode 100644 index 0000000..90804c4 --- /dev/null +++ b/Source/com/drew/metadata/photoshop/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of Photoshop metadata. + */ +package com.drew.metadata.photoshop; diff --git a/Source/com/drew/metadata/photoshop/package.html b/Source/com/drew/metadata/photoshop/package.html deleted file mode 100644 index 1bdf41e..0000000 --- a/Source/com/drew/metadata/photoshop/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of Photoshop metadata. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/metadata/png/PngChromaticitiesDirectory.java b/Source/com/drew/metadata/png/PngChromaticitiesDirectory.java index 77f9c3f..c64b88e 100644 --- a/Source/com/drew/metadata/png/PngChromaticitiesDirectory.java +++ b/Source/com/drew/metadata/png/PngChromaticitiesDirectory.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.lang.annotations.NotNull; @@ -9,6 +29,7 @@ import java.util.HashMap; /** * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class PngChromaticitiesDirectory extends Directory { public static final int TAG_WHITE_POINT_X = 1; diff --git a/Source/com/drew/metadata/png/PngDescriptor.java b/Source/com/drew/metadata/png/PngDescriptor.java index dd9d29f..b196987 100644 --- a/Source/com/drew/metadata/png/PngDescriptor.java +++ b/Source/com/drew/metadata/png/PngDescriptor.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.imaging.png.PngColorType; @@ -11,9 +31,12 @@ import com.drew.metadata.TagDescriptor; import java.io.IOException; import java.util.List; +import static com.drew.metadata.png.PngDirectory.*; + /** * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class PngDescriptor extends TagDescriptor<PngDirectory> { public PngDescriptor(@NotNull PngDirectory directory) @@ -26,22 +49,24 @@ public class PngDescriptor extends TagDescriptor<PngDirectory> public String getDescription(int tagType) { switch (tagType) { - case PngDirectory.TAG_COLOR_TYPE: + case TAG_COLOR_TYPE: return getColorTypeDescription(); - case PngDirectory.TAG_COMPRESSION_TYPE: + case TAG_COMPRESSION_TYPE: return getCompressionTypeDescription(); - case PngDirectory.TAG_FILTER_METHOD: + case TAG_FILTER_METHOD: return getFilterMethodDescription(); - case PngDirectory.TAG_INTERLACE_METHOD: + case TAG_INTERLACE_METHOD: return getInterlaceMethodDescription(); - case PngDirectory.TAG_PALETTE_HAS_TRANSPARENCY: + case TAG_PALETTE_HAS_TRANSPARENCY: return getPaletteHasTransparencyDescription(); - case PngDirectory.TAG_SRGB_RENDERING_INTENT: + case TAG_SRGB_RENDERING_INTENT: return getIsSrgbColorSpaceDescription(); - case PngDirectory.TAG_TEXTUAL_DATA: + case TAG_TEXTUAL_DATA: return getTextualDataDescription(); - case PngDirectory.TAG_BACKGROUND_COLOR: + case TAG_BACKGROUND_COLOR: return getBackgroundColorDescription(); + case TAG_UNIT_SPECIFIER: + return getUnitSpecifierDescription(); default: return super.getDescription(tagType); } @@ -50,7 +75,7 @@ public class PngDescriptor extends TagDescriptor<PngDirectory> @Nullable public String getColorTypeDescription() { - Integer value = _directory.getInteger(PngDirectory.TAG_COLOR_TYPE); + Integer value = _directory.getInteger(TAG_COLOR_TYPE); if (value == null) return null; PngColorType colorType = PngColorType.fromNumericValue(value); @@ -62,32 +87,32 @@ public class PngDescriptor extends TagDescriptor<PngDirectory> @Nullable public String getCompressionTypeDescription() { - return getIndexedDescription(PngDirectory.TAG_COMPRESSION_TYPE, "Deflate"); + return getIndexedDescription(TAG_COMPRESSION_TYPE, "Deflate"); } @Nullable public String getFilterMethodDescription() { - return getIndexedDescription(PngDirectory.TAG_FILTER_METHOD, "Adaptive"); + return getIndexedDescription(TAG_FILTER_METHOD, "Adaptive"); } @Nullable public String getInterlaceMethodDescription() { - return getIndexedDescription(PngDirectory.TAG_INTERLACE_METHOD, "No Interlace", "Adam7 Interlace"); + return getIndexedDescription(TAG_INTERLACE_METHOD, "No Interlace", "Adam7 Interlace"); } @Nullable public String getPaletteHasTransparencyDescription() { - return getIndexedDescription(PngDirectory.TAG_PALETTE_HAS_TRANSPARENCY, null, "Yes"); + return getIndexedDescription(TAG_PALETTE_HAS_TRANSPARENCY, null, "Yes"); } @Nullable public String getIsSrgbColorSpaceDescription() { return getIndexedDescription( - PngDirectory.TAG_SRGB_RENDERING_INTENT, + TAG_SRGB_RENDERING_INTENT, "Perceptual", "Relative Colorimetric", "Saturation", @@ -95,10 +120,20 @@ public class PngDescriptor extends TagDescriptor<PngDirectory> ); } + @Nullable + public String getUnitSpecifierDescription() + { + return getIndexedDescription( + TAG_UNIT_SPECIFIER, + "Unspecified", + "Metres" + ); + } + @Nullable public String getTextualDataDescription() { - Object object = _directory.getObject(PngDirectory.TAG_TEXTUAL_DATA); + Object object = _directory.getObject(TAG_TEXTUAL_DATA); if (object == null) { return null; } @@ -106,7 +141,9 @@ public class PngDescriptor extends TagDescriptor<PngDirectory> List<KeyValuePair> keyValues = (List<KeyValuePair>)object; StringBuilder sb = new StringBuilder(); for (KeyValuePair keyValue : keyValues) { - sb.append(String.format("%s: %s\n", keyValue.getKey(), keyValue.getValue())); + if (sb.length() != 0) + sb.append('\n'); + sb.append(String.format("%s: %s", keyValue.getKey(), keyValue.getValue())); } return sb.toString(); } @@ -114,8 +151,8 @@ public class PngDescriptor extends TagDescriptor<PngDirectory> @Nullable public String getBackgroundColorDescription() { - byte[] bytes = _directory.getByteArray(PngDirectory.TAG_BACKGROUND_COLOR); - Integer colorType = _directory.getInteger(PngDirectory.TAG_COLOR_TYPE); + byte[] bytes = _directory.getByteArray(TAG_BACKGROUND_COLOR); + Integer colorType = _directory.getInteger(TAG_COLOR_TYPE); if (bytes == null || colorType == null) { return null; } diff --git a/Source/com/drew/metadata/png/PngDirectory.java b/Source/com/drew/metadata/png/PngDirectory.java index c10f2f3..1940630 100644 --- a/Source/com/drew/metadata/png/PngDirectory.java +++ b/Source/com/drew/metadata/png/PngDirectory.java @@ -1,5 +1,26 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; +import com.drew.imaging.png.PngChunkType; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Directory; @@ -8,6 +29,7 @@ import java.util.HashMap; /** * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class PngDirectory extends Directory { public static final int TAG_IMAGE_WIDTH = 1; @@ -26,6 +48,12 @@ public class PngDirectory extends Directory public static final int TAG_LAST_MODIFICATION_TIME = 14; public static final int TAG_BACKGROUND_COLOR = 15; + public static final int TAG_PIXELS_PER_UNIT_X = 16; + public static final int TAG_PIXELS_PER_UNIT_Y = 17; + public static final int TAG_UNIT_SPECIFIER = 18; + + public static final int TAG_SIGNIFICANT_BITS = 19; + @NotNull protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); @@ -45,18 +73,32 @@ public class PngDirectory extends Directory _tagNameMap.put(TAG_TEXTUAL_DATA, "Textual Data"); _tagNameMap.put(TAG_LAST_MODIFICATION_TIME, "Last Modification Time"); _tagNameMap.put(TAG_BACKGROUND_COLOR, "Background Color"); + _tagNameMap.put(TAG_PIXELS_PER_UNIT_X, "Pixels Per Unit X"); + _tagNameMap.put(TAG_PIXELS_PER_UNIT_Y, "Pixels Per Unit Y"); + _tagNameMap.put(TAG_UNIT_SPECIFIER, "Unit Specifier"); + _tagNameMap.put(TAG_SIGNIFICANT_BITS, "Significant Bits"); } - public PngDirectory() + private final PngChunkType _pngChunkType; + + public PngDirectory(@NotNull PngChunkType pngChunkType) { + _pngChunkType = pngChunkType; + this.setDescriptor(new PngDescriptor(this)); } + @NotNull + public PngChunkType getPngChunkType() + { + return _pngChunkType; + } + @Override @NotNull public String getName() { - return "PNG"; + return "PNG-" + _pngChunkType.getIdentifier(); } @Override diff --git a/Source/com/drew/metadata/png/package-info.java b/Source/com/drew/metadata/png/package-info.java new file mode 100644 index 0000000..d427c0d --- /dev/null +++ b/Source/com/drew/metadata/png/package-info.java @@ -0,0 +1,6 @@ +/** + * Contains classes for the extraction and modelling of PNG file metadata. + * + * @since 2.7.0 + */ +package com.drew.metadata.png; diff --git a/Source/com/drew/metadata/png/package.html b/Source/com/drew/metadata/png/package.html deleted file mode 100644 index 20b9ba8..0000000 --- a/Source/com/drew/metadata/png/package.html +++ /dev/null @@ -1,34 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of PNG file metadata. - -<!-- Put @see and @since tags down here. --> -@since 2.7.0 - -</body> -</html> diff --git a/Source/com/drew/metadata/tiff/DirectoryTiffHandler.java b/Source/com/drew/metadata/tiff/DirectoryTiffHandler.java index eb5e25a..163e86a 100644 --- a/Source/com/drew/metadata/tiff/DirectoryTiffHandler.java +++ b/Source/com/drew/metadata/tiff/DirectoryTiffHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,9 @@ import com.drew.imaging.tiff.TiffHandler; import com.drew.lang.Rational; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Directory; +import com.drew.metadata.ErrorDirectory; import com.drew.metadata.Metadata; +import com.drew.metadata.StringValue; import java.util.Stack; @@ -40,10 +42,9 @@ public abstract class DirectoryTiffHandler implements TiffHandler protected Directory _currentDirectory; protected final Metadata _metadata; - protected DirectoryTiffHandler(Metadata metadata, Class<? extends Directory> initialDirectory) + protected DirectoryTiffHandler(Metadata metadata) { _metadata = metadata; - _currentDirectory = _metadata.getOrCreateDirectory(initialDirectory); } public void endingIFD() @@ -53,19 +54,49 @@ public abstract class DirectoryTiffHandler implements TiffHandler protected void pushDirectory(@NotNull Class<? extends Directory> directoryClass) { - assert(directoryClass != _currentDirectory.getClass()); - _directoryStack.push(_currentDirectory); - _currentDirectory = _metadata.getOrCreateDirectory(directoryClass); + Directory newDirectory = null; + + try { + newDirectory = directoryClass.newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + + if (newDirectory != null) + { + // If this is the first directory, don't add to the stack + if (_currentDirectory != null) + { + _directoryStack.push(_currentDirectory); + newDirectory.setParent(_currentDirectory); + } + _currentDirectory = newDirectory; + _metadata.addDirectory(_currentDirectory); + } } public void warn(@NotNull String message) { - _currentDirectory.addError(message); + getCurrentOrErrorDirectory().addError(message); } public void error(@NotNull String message) { - _currentDirectory.addError(message); + getCurrentOrErrorDirectory().addError(message); + } + + @NotNull + private Directory getCurrentOrErrorDirectory() + { + if (_currentDirectory != null) + return _currentDirectory; + ErrorDirectory error = _metadata.getFirstDirectoryOfType(ErrorDirectory.class); + if (error != null) + return error; + pushDirectory(ErrorDirectory.class); + return _currentDirectory; } public void setByteArray(int tagId, @NotNull byte[] bytes) @@ -73,9 +104,9 @@ public abstract class DirectoryTiffHandler implements TiffHandler _currentDirectory.setByteArray(tagId, bytes); } - public void setString(int tagId, @NotNull String string) + public void setString(int tagId, @NotNull StringValue string) { - _currentDirectory.setString(tagId, string); + _currentDirectory.setStringValue(tagId, string); } public void setRational(int tagId, @NotNull Rational rational) diff --git a/Source/com/drew/metadata/tiff/package-info.java b/Source/com/drew/metadata/tiff/package-info.java new file mode 100644 index 0000000..28f57bb --- /dev/null +++ b/Source/com/drew/metadata/tiff/package-info.java @@ -0,0 +1,6 @@ +/** + * Contains classes for the extraction and modelling of TIFF file metadata. + * + * @since 2.7.0 + */ +package com.drew.metadata.tiff; diff --git a/Source/com/drew/metadata/tiff/package.html b/Source/com/drew/metadata/tiff/package.html deleted file mode 100644 index b0cc736..0000000 --- a/Source/com/drew/metadata/tiff/package.html +++ /dev/null @@ -1,34 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of TIFF file metadata. - -<!-- Put @see and @since tags down here. --> -@since 2.7.0 - -</body> -</html> diff --git a/Source/com/drew/metadata/webp/WebpDescriptor.java b/Source/com/drew/metadata/webp/WebpDescriptor.java new file mode 100644 index 0000000..7876f44 --- /dev/null +++ b/Source/com/drew/metadata/webp/WebpDescriptor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.webp; + +import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; +import com.drew.metadata.TagDescriptor; + +/** + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class WebpDescriptor extends TagDescriptor<WebpDirectory> +{ + public WebpDescriptor(@NotNull WebpDirectory directory) + { + super(directory); + } + + @Override + @Nullable + public String getDescription(int tagType) + { + switch (tagType) { + default: + return super.getDescription(tagType); + } + } +} diff --git a/Source/com/drew/metadata/webp/WebpDirectory.java b/Source/com/drew/metadata/webp/WebpDirectory.java new file mode 100644 index 0000000..39ff922 --- /dev/null +++ b/Source/com/drew/metadata/webp/WebpDirectory.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.webp; + +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Directory; + +import java.util.HashMap; + +/** + * @author Drew Noakes https://drewnoakes.com + */ +@SuppressWarnings("WeakerAccess") +public class WebpDirectory extends Directory +{ + public static final int TAG_IMAGE_HEIGHT = 1; + public static final int TAG_IMAGE_WIDTH = 2; + public static final int TAG_HAS_ALPHA = 3; + public static final int TAG_IS_ANIMATION = 4; + + @NotNull + protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); + + static { + _tagNameMap.put(TAG_IMAGE_HEIGHT, "Image Height"); + _tagNameMap.put(TAG_IMAGE_WIDTH, "Image Width"); + _tagNameMap.put(TAG_HAS_ALPHA, "Has Alpha"); + _tagNameMap.put(TAG_IS_ANIMATION, "Is Animation"); + } + + public WebpDirectory() + { + this.setDescriptor(new WebpDescriptor(this)); + } + + @Override + @NotNull + public String getName() + { + return "WebP"; + } + + @Override + @NotNull + protected HashMap<Integer, String> getTagNameMap() + { + return _tagNameMap; + } +} diff --git a/Source/com/drew/metadata/webp/WebpRiffHandler.java b/Source/com/drew/metadata/webp/WebpRiffHandler.java new file mode 100644 index 0000000..208b39c --- /dev/null +++ b/Source/com/drew/metadata/webp/WebpRiffHandler.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.webp; + +import com.drew.imaging.riff.RiffHandler; +import com.drew.lang.ByteArrayReader; +import com.drew.lang.RandomAccessReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; +import com.drew.metadata.exif.ExifReader; +import com.drew.metadata.icc.IccReader; +import com.drew.metadata.xmp.XmpReader; + +import java.io.IOException; + +/** + * Implementation of {@link RiffHandler} specialising in WebP support. + * + * Extracts data from chunk types: + * + * <ul> + * <li><code>"VP8X"</code>: width, height, is animation, has alpha</li> + * <li><code>"EXIF"</code>: full Exif data</li> + * <li><code>"ICCP"</code>: full ICC profile</li> + * <li><code>"XMP "</code>: full XMP data</li> + * </ul> + */ +public class WebpRiffHandler implements RiffHandler +{ + @NotNull + private final Metadata _metadata; + + public WebpRiffHandler(@NotNull Metadata metadata) + { + _metadata = metadata; + } + + public boolean shouldAcceptRiffIdentifier(@NotNull String identifier) + { + return identifier.equals("WEBP"); + } + + public boolean shouldAcceptChunk(@NotNull String fourCC) + { + return fourCC.equals("VP8X") + || fourCC.equals("VP8L") + || fourCC.equals("VP8 ") + || fourCC.equals("EXIF") + || fourCC.equals("ICCP") + || fourCC.equals("XMP "); + } + + public void processChunk(@NotNull String fourCC, @NotNull byte[] payload) + { +// System.out.println("Chunk " + fourCC + " " + payload.length + " bytes"); + + if (fourCC.equals("EXIF")) { + new ExifReader().extract(new ByteArrayReader(payload), _metadata); + } else if (fourCC.equals("ICCP")) { + new IccReader().extract(new ByteArrayReader(payload), _metadata); + } else if (fourCC.equals("XMP ")) { + new XmpReader().extract(payload, _metadata); + } else if (fourCC.equals("VP8X") && payload.length == 10) { + RandomAccessReader reader = new ByteArrayReader(payload); + reader.setMotorolaByteOrder(false); + + try { + // Flags +// boolean hasFragments = reader.getBit(0); + boolean isAnimation = reader.getBit(1); +// boolean hasXmp = reader.getBit(2); +// boolean hasExif = reader.getBit(3); + boolean hasAlpha = reader.getBit(4); +// boolean hasIcc = reader.getBit(5); + + // Image size + int widthMinusOne = reader.getInt24(4); + int heightMinusOne = reader.getInt24(7); + + WebpDirectory directory = new WebpDirectory(); + directory.setInt(WebpDirectory.TAG_IMAGE_WIDTH, widthMinusOne + 1); + directory.setInt(WebpDirectory.TAG_IMAGE_HEIGHT, heightMinusOne + 1); + directory.setBoolean(WebpDirectory.TAG_HAS_ALPHA, hasAlpha); + directory.setBoolean(WebpDirectory.TAG_IS_ANIMATION, isAnimation); + + _metadata.addDirectory(directory); + + } catch (IOException e) { + e.printStackTrace(System.err); + } + } else if (fourCC.equals("VP8L") && payload.length > 4) { + RandomAccessReader reader = new ByteArrayReader(payload); + reader.setMotorolaByteOrder(false); + + try { + // https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification#2_riff_header + + // Expect the signature byte + if (reader.getInt8(0) != 0x2F) + return; + int b1 = reader.getUInt8(1); + int b2 = reader.getUInt8(2); + int b3 = reader.getUInt8(3); + int b4 = reader.getUInt8(4); + // 14 bits for width + int widthMinusOne = (b2 & 0x3F) << 8 | b1; + // 14 bits for height + int heightMinusOne = (b4 & 0x0F) << 10 | b3 << 2 | (b2 & 0xC0) >> 6; + + WebpDirectory directory = new WebpDirectory(); + directory.setInt(WebpDirectory.TAG_IMAGE_WIDTH, widthMinusOne + 1); + directory.setInt(WebpDirectory.TAG_IMAGE_HEIGHT, heightMinusOne + 1); + + _metadata.addDirectory(directory); + + } catch (IOException e) { + e.printStackTrace(System.err); + } + } else if (fourCC.equals("VP8 ") && payload.length > 9) { + RandomAccessReader reader = new ByteArrayReader(payload); + reader.setMotorolaByteOrder(false); + + try { + // https://tools.ietf.org/html/rfc6386#section-9.1 + // https://github.com/webmproject/libwebp/blob/master/src/enc/syntax.c#L115 + + // Expect the signature bytes + if (reader.getUInt8(3) != 0x9D || + reader.getUInt8(4) != 0x01 || + reader.getUInt8(5) != 0x2A) + return; + int width = reader.getUInt16(6); + int height = reader.getUInt16(8); + + WebpDirectory directory = new WebpDirectory(); + directory.setInt(WebpDirectory.TAG_IMAGE_WIDTH, width); + directory.setInt(WebpDirectory.TAG_IMAGE_HEIGHT, height); + + _metadata.addDirectory(directory); + + } catch (IOException e) { + e.printStackTrace(System.err); + } + } + } +} diff --git a/Source/com/drew/metadata/webp/package-info.java b/Source/com/drew/metadata/webp/package-info.java new file mode 100644 index 0000000..26547ef --- /dev/null +++ b/Source/com/drew/metadata/webp/package-info.java @@ -0,0 +1,6 @@ +/** + * Contains classes for the extraction and modelling of WebP file metadata. + * + * @since 2.8.0 + */ +package com.drew.metadata.webp; diff --git a/Source/com/drew/metadata/xmp/XmpDescriptor.java b/Source/com/drew/metadata/xmp/XmpDescriptor.java index 5d9b88b..475f27f 100644 --- a/Source/com/drew/metadata/xmp/XmpDescriptor.java +++ b/Source/com/drew/metadata/xmp/XmpDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,156 +20,20 @@ */ package com.drew.metadata.xmp; -import com.drew.imaging.PhotographicConversions; -import com.drew.lang.Rational; import com.drew.lang.annotations.NotNull; -import com.drew.lang.annotations.Nullable; import com.drew.metadata.TagDescriptor; -import java.text.DecimalFormat; - /** * Contains all logic for the presentation of xmp data, as stored in Xmp-Segment. Use * this class to provide human-readable descriptions of tag values. * * @author Torsten Skadell, Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class XmpDescriptor extends TagDescriptor<XmpDirectory> { - // TODO some of these methods look similar to those found in Exif*Descriptor... extract common functionality from both - - @NotNull - private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#"); - public XmpDescriptor(@NotNull XmpDirectory directory) { super(directory); } - - /** Do some simple formatting, dependant upon tagType */ - @Override - public String getDescription(int tagType) - { - switch (tagType) { - case XmpDirectory.TAG_MAKE: - case XmpDirectory.TAG_MODEL: - return _directory.getString(tagType); - case XmpDirectory.TAG_EXPOSURE_TIME: - return getExposureTimeDescription(); - case XmpDirectory.TAG_EXPOSURE_PROGRAM: - return getExposureProgramDescription(); - case XmpDirectory.TAG_SHUTTER_SPEED: - return getShutterSpeedDescription(); - case XmpDirectory.TAG_F_NUMBER: - return getFNumberDescription(); - case XmpDirectory.TAG_LENS: - case XmpDirectory.TAG_LENS_INFO: - case XmpDirectory.TAG_CAMERA_SERIAL_NUMBER: - case XmpDirectory.TAG_FIRMWARE: - return _directory.getString(tagType); - case XmpDirectory.TAG_FOCAL_LENGTH: - return getFocalLengthDescription(); - case XmpDirectory.TAG_APERTURE_VALUE: - return getApertureValueDescription(); - default: - return super.getDescription(tagType); - } - } - - /** Do a simple formatting like ExifSubIFDDescriptor.java */ - @Nullable - public String getExposureTimeDescription() - { - final String value = _directory.getString(XmpDirectory.TAG_EXPOSURE_TIME); - if (value==null) - return null; - return value + " sec"; - } - - /** This code is from ExifSubIFDDescriptor.java */ - @Nullable - public String getExposureProgramDescription() - { - // '1' means manual control, '2' program normal, '3' aperture priority, - // '4' shutter priority, '5' program creative (slow program), - // '6' program action(high-speed program), '7' portrait mode, '8' landscape mode. - final Integer value = _directory.getInteger(XmpDirectory.TAG_EXPOSURE_PROGRAM); - if (value==null) - return null; - switch (value) { - case 1: - return "Manual control"; - case 2: - return "Program normal"; - case 3: - return "Aperture priority"; - case 4: - return "Shutter priority"; - case 5: - return "Program creative (slow program)"; - case 6: - return "Program action (high-speed program)"; - case 7: - return "Portrait mode"; - case 8: - return "Landscape mode"; - default: - return "Unknown program (" + value + ")"; - } - } - - - /** This code is from ExifSubIFDDescriptor.java */ - @Nullable - public String getShutterSpeedDescription() - { - final Float value = _directory.getFloatObject(XmpDirectory.TAG_SHUTTER_SPEED); - if (value==null) - return null; - - // thanks to Mark Edwards for spotting and patching a bug in the calculation of this - // description (spotted bug using a Canon EOS 300D) - // thanks also to Gli Blr for spotting this bug - if (value <= 1) { - float apexPower = (float) (1 / (Math.exp(value * Math.log(2)))); - long apexPower10 = Math.round((double) apexPower * 10.0); - float fApexPower = (float) apexPower10 / 10.0f; - return fApexPower + " sec"; - } else { - int apexPower = (int) ((Math.exp(value * Math.log(2)))); - return "1/" + apexPower + " sec"; - } - } - - /** Do a simple formatting like ExifSubIFDDescriptor.java */ - @Nullable - public String getFNumberDescription() - { - final Rational value = _directory.getRational(XmpDirectory.TAG_F_NUMBER); - if (value==null) - return null; - return "F" + SimpleDecimalFormatter.format(value.doubleValue()); - } - - /** This code is from ExifSubIFDDescriptor.java */ - @Nullable - public String getFocalLengthDescription() - { - final Rational value = _directory.getRational(XmpDirectory.TAG_FOCAL_LENGTH); - if (value==null) - return null; - java.text.DecimalFormat formatter = new DecimalFormat("0.0##"); - return formatter.format(value.doubleValue()) + " mm"; - } - - /** This code is from ExifSubIFDDescriptor.java */ - @Nullable - public String getApertureValueDescription() - { - final Double value = _directory.getDoubleObject(XmpDirectory.TAG_APERTURE_VALUE); - if (value==null) - return null; - double fStop = PhotographicConversions.apertureToFStop(value); - return "F" + SimpleDecimalFormatter.format(fStop); - } } diff --git a/Source/com/drew/metadata/xmp/XmpDirectory.java b/Source/com/drew/metadata/xmp/XmpDirectory.java index b261344..7f647d4 100644 --- a/Source/com/drew/metadata/xmp/XmpDirectory.java +++ b/Source/com/drew/metadata/xmp/XmpDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,117 +20,39 @@ */ package com.drew.metadata.xmp; +import com.adobe.xmp.XMPException; import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.impl.XMPMetaImpl; +import com.adobe.xmp.properties.XMPPropertyInfo; import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.Directory; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; /** + * Wraps an instance of Adobe's {@link XMPMeta} object, which holds XMP data. + * <p /> + * XMP uses a namespace and path format for identifying values, which does not map to metadata-extractor's + * integer based tag identifiers. Therefore, XMP data is extracted and exposed via {@link XmpDirectory#getXMPMeta()} + * which returns an instance of Adobe's {@link XMPMeta} which exposes the full XMP data set. + * * @author Torsten Skadell * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("WeakerAccess") public class XmpDirectory extends Directory { - // These are some Tags, belonging to xmp-data-tags - // The numeration is more like enums. The real xmp-tags are strings, - // so we do some kind of mapping here... - public static final int TAG_MAKE = 0x0001; - public static final int TAG_MODEL = 0x0002; - public static final int TAG_EXPOSURE_TIME = 0x0003; - public static final int TAG_SHUTTER_SPEED = 0x0004; - public static final int TAG_F_NUMBER = 0x0005; - public static final int TAG_LENS_INFO = 0x0006; - public static final int TAG_LENS = 0x0007; - public static final int TAG_CAMERA_SERIAL_NUMBER = 0x0008; - public static final int TAG_FIRMWARE = 0x0009; - public static final int TAG_FOCAL_LENGTH = 0x000a; - public static final int TAG_APERTURE_VALUE = 0x000b; - public static final int TAG_EXPOSURE_PROGRAM = 0x000c; - public static final int TAG_DATETIME_ORIGINAL = 0x000d; - public static final int TAG_DATETIME_DIGITIZED = 0x000e; - - /** - * A value from 0 to 5, or -1 if the image is rejected. - */ - public static final int TAG_RATING = 0x1001; - -/* - // dublin core properties - // this requires further research - public static int TAG_TITLE = 0x100; - public static int TAG_SUBJECT = 0x1001; - public static int TAG_DATE = 0x1002; - public static int TAG_TYPE = 0x1003; - public static int TAG_DESCRIPTION = 0x1004; - public static int TAG_RELATION = 0x1005; - public static int TAG_COVERAGE = 0x1006; - public static int TAG_CREATOR = 0x1007; - public static int TAG_PUBLISHER = 0x1008; - public static int TAG_CONTRIBUTOR = 0x1009; - public static int TAG_RIGHTS = 0x100A; - public static int TAG_FORMAT = 0x100B; - public static int TAG_IDENTIFIER = 0x100C; - public static int TAG_LANGUAGE = 0x100D; - public static int TAG_AUDIENCE = 0x100E; - public static int TAG_PROVENANCE = 0x100F; - public static int TAG_RIGHTS_HOLDER = 0x1010; - public static int TAG_INSTRUCTIONAL_METHOD = 0x1011; - public static int TAG_ACCRUAL_METHOD = 0x1012; - public static int TAG_ACCRUAL_PERIODICITY = 0x1013; - public static int TAG_ACCRUAL_POLICY = 0x1014; -*/ + public static final int TAG_XMP_VALUE_COUNT = 0xFFFF; @NotNull protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>(); - @NotNull - private final Map<String, String> _propertyValueByPath = new HashMap<String, String>(); static { - _tagNameMap.put(TAG_MAKE, "Make"); - _tagNameMap.put(TAG_MODEL, "Model"); - _tagNameMap.put(TAG_EXPOSURE_TIME, "Exposure Time"); - _tagNameMap.put(TAG_SHUTTER_SPEED, "Shutter Speed Value"); - _tagNameMap.put(TAG_F_NUMBER, "F-Number"); - _tagNameMap.put(TAG_LENS_INFO, "Lens Information"); - _tagNameMap.put(TAG_LENS, "Lens"); - _tagNameMap.put(TAG_CAMERA_SERIAL_NUMBER, "Serial Number"); - _tagNameMap.put(TAG_FIRMWARE, "Firmware"); - _tagNameMap.put(TAG_FOCAL_LENGTH, "Focal Length"); - _tagNameMap.put(TAG_APERTURE_VALUE, "Aperture Value"); - _tagNameMap.put(TAG_EXPOSURE_PROGRAM, "Exposure Program"); - _tagNameMap.put(TAG_DATETIME_ORIGINAL, "Date/Time Original"); - _tagNameMap.put(TAG_DATETIME_DIGITIZED, "Date/Time Digitized"); - - _tagNameMap.put(TAG_RATING, "Rating"); - -/* - // this requires further research - _tagNameMap.put(TAG_TITLE, "Title"); - _tagNameMap.put(TAG_SUBJECT, "Subject"); - _tagNameMap.put(TAG_DATE, "Date"); - _tagNameMap.put(TAG_TYPE, "Type"); - _tagNameMap.put(TAG_DESCRIPTION, "Description"); - _tagNameMap.put(TAG_RELATION, "Relation"); - _tagNameMap.put(TAG_COVERAGE, "Coverage"); - _tagNameMap.put(TAG_CREATOR, "Creator"); - _tagNameMap.put(TAG_PUBLISHER, "Publisher"); - _tagNameMap.put(TAG_CONTRIBUTOR, "Contributor"); - _tagNameMap.put(TAG_RIGHTS, "Rights"); - _tagNameMap.put(TAG_FORMAT, "Format"); - _tagNameMap.put(TAG_IDENTIFIER, "Identifier"); - _tagNameMap.put(TAG_LANGUAGE, "Language"); - _tagNameMap.put(TAG_AUDIENCE, "Audience"); - _tagNameMap.put(TAG_PROVENANCE, "Provenance"); - _tagNameMap.put(TAG_RIGHTS_HOLDER, "Rights Holder"); - _tagNameMap.put(TAG_INSTRUCTIONAL_METHOD, "Instructional Method"); - _tagNameMap.put(TAG_ACCRUAL_METHOD, "Accrual Method"); - _tagNameMap.put(TAG_ACCRUAL_PERIODICITY, "Accrual Periodicity"); - _tagNameMap.put(TAG_ACCRUAL_POLICY, "Accrual Policy"); -*/ + _tagNameMap.put(TAG_XMP_VALUE_COUNT, "XMP Value Count"); } @Nullable @@ -145,7 +67,7 @@ public class XmpDirectory extends Directory @NotNull public String getName() { - return "Xmp"; + return "XMP"; } @Override @@ -155,13 +77,8 @@ public class XmpDirectory extends Directory return _tagNameMap; } - void addProperty(@NotNull String path, @NotNull String value) - { - _propertyValueByPath.put(path, value); - } - /** - * Gets a map of all XMP properties in this directory, not just the known ones. + * Gets a map of all XMP properties in this directory. * <p> * This is required because XMP properties are represented as strings, whereas the rest of this library * uses integers for keys. @@ -169,20 +86,52 @@ public class XmpDirectory extends Directory @NotNull public Map<String, String> getXmpProperties() { - return Collections.unmodifiableMap(_propertyValueByPath); + Map<String, String> propertyValueByPath = new HashMap<String, String>(); + + if (_xmpMeta != null) + { + try { + for (Iterator i = _xmpMeta.iterator(); i.hasNext(); ) { + XMPPropertyInfo prop = (XMPPropertyInfo)i.next(); + String path = prop.getPath(); + String value = prop.getValue(); + if (path != null && value != null) { + propertyValueByPath.put(path, value); + } + } + } catch (XMPException ignored) { + } + } + + return Collections.unmodifiableMap(propertyValueByPath); } public void setXMPMeta(@NotNull XMPMeta xmpMeta) { _xmpMeta = xmpMeta; + + try { + int valueCount = 0; + for (Iterator i = _xmpMeta.iterator(); i.hasNext(); ) { + XMPPropertyInfo prop = (XMPPropertyInfo)i.next(); + if (prop.getPath() != null) { + valueCount++; + } + } + setInt(TAG_XMP_VALUE_COUNT, valueCount); + } catch (XMPException ignored) { + } } /** - * Gets the XMPMeta object used to populate this directory. It can be used for more XMP-oriented operations. + * Gets the XMPMeta object used to populate this directory. It can be used for more XMP-oriented operations. + * If one does not exist it will be created. */ - @Nullable + @NotNull public XMPMeta getXMPMeta() { + if (_xmpMeta == null) + _xmpMeta = new XMPMetaImpl(); return _xmpMeta; } } diff --git a/Source/com/drew/metadata/xmp/XmpReader.java b/Source/com/drew/metadata/xmp/XmpReader.java index 4c9adcc..bfcb2bb 100644 --- a/Source/com/drew/metadata/xmp/XmpReader.java +++ b/Source/com/drew/metadata/xmp/XmpReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,88 +24,105 @@ import com.adobe.xmp.XMPException; import com.adobe.xmp.XMPIterator; import com.adobe.xmp.XMPMeta; import com.adobe.xmp.XMPMetaFactory; +import com.adobe.xmp.impl.ByteBuffer; import com.adobe.xmp.properties.XMPPropertyInfo; import com.drew.imaging.jpeg.JpegSegmentMetadataReader; import com.drew.imaging.jpeg.JpegSegmentType; -import com.drew.lang.Rational; +import com.drew.lang.SequentialByteArrayReader; +import com.drew.lang.SequentialReader; +import com.drew.metadata.Directory; import com.drew.lang.annotations.NotNull; +import com.drew.lang.annotations.Nullable; import com.drew.metadata.Metadata; +import com.drew.metadata.StringValue; -import java.util.Arrays; -import java.util.Calendar; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; /** - * Extracts XMP data from a JPEG header segment. + * Extracts XMP data from JPEG APP1 segments. + * <p> + * Note that XMP uses a namespace and path format for identifying values, which does not map to metadata-extractor's + * integer based tag identifiers. Therefore, XMP data is extracted and exposed via {@link XmpDirectory#getXMPMeta()} + * which returns an instance of Adobe's {@link XMPMeta} which exposes the full XMP data set. * <p> * The extraction is done with Adobe's XmpCore-Library (XMP-Toolkit) * Copyright (c) 1999 - 2007, Adobe Systems Incorporated All rights reserved. * * @author Torsten Skadell * @author Drew Noakes https://drewnoakes.com + * @author https://github.com/bezineb5 */ public class XmpReader implements JpegSegmentMetadataReader { - private static final int FMT_STRING = 1; - private static final int FMT_RATIONAL = 2; - private static final int FMT_INT = 3; - private static final int FMT_DOUBLE = 4; - - /** - * XMP tag namespace. - * TODO the older "xap", "xapBJ", "xapMM" or "xapRights" namespace prefixes should be translated to the newer "xmp", "xmpBJ", "xmpMM" and "xmpRights" prefixes for use in family 1 group names - */ @NotNull - private static final String SCHEMA_XMP_PROPERTIES = "http://ns.adobe.com/xap/1.0/"; + private static final String XMP_JPEG_PREAMBLE = "http://ns.adobe.com/xap/1.0/\0"; @NotNull - private static final String SCHEMA_EXIF_SPECIFIC_PROPERTIES = "http://ns.adobe.com/exif/1.0/"; + private static final String XMP_EXTENSION_JPEG_PREAMBLE = "http://ns.adobe.com/xmp/extension/\0"; @NotNull - private static final String SCHEMA_EXIF_ADDITIONAL_PROPERTIES = "http://ns.adobe.com/exif/1.0/aux/"; + private static final String SCHEMA_XMP_NOTES = "http://ns.adobe.com/xmp/note/"; @NotNull - private static final String SCHEMA_EXIF_TIFF_PROPERTIES = "http://ns.adobe.com/tiff/1.0/"; -// @NotNull -// private static final String SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES = "http://purl.org/dc/elements/1.1/"; + private static final String ATTRIBUTE_EXTENDED_XMP = "xmpNote:HasExtendedXMP"; + + /** + * Extended XMP constants + */ + private static final int EXTENDED_XMP_GUID_LENGTH = 32; + private static final int EXTENDED_XMP_INT_LENGTH = 4; @NotNull public Iterable<JpegSegmentType> getSegmentTypes() { - return Arrays.asList(JpegSegmentType.APP1); - } - - public boolean canProcess(@NotNull byte[] segmentBytes, @NotNull JpegSegmentType segmentType) - { - return segmentBytes.length > 27 && "http://ns.adobe.com/xap/1.0/".equalsIgnoreCase(new String(segmentBytes, 0, 28)); + return Collections.singletonList(JpegSegmentType.APP1); } /** * Version specifically for dealing with XMP found in JPEG segments. This form of XMP has a peculiar preamble, which * must be removed before parsing the XML. * - * @param segmentBytes The byte array from which the metadata should be extracted. + * @param segments The byte array from which the metadata should be extracted. * @param metadata The {@link Metadata} object into which extracted values should be merged. * @param segmentType The {@link JpegSegmentType} being read. */ - public void extract(@NotNull byte[] segmentBytes, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) + public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType) { - XmpDirectory directory = metadata.getOrCreateDirectory(XmpDirectory.class); + final int preambleLength = XMP_JPEG_PREAMBLE.length(); + final int extensionPreambleLength = XMP_EXTENSION_JPEG_PREAMBLE.length(); + String extendedXMPGUID = null; + byte[] extendedXMPBuffer = null; - // XMP in a JPEG file has a 29 byte preamble which is not valid XML. - final int preambleLength = 29; + for (byte[] segmentBytes : segments) { + // XMP in a JPEG file has an identifying preamble which is not valid XML + if (segmentBytes.length >= preambleLength) { + // NOTE we expect the full preamble here, but some images (such as that reported on GitHub #102) + // start with "XMP\0://ns.adobe.com/xap/1.0/" which appears to be an error but is easily recovered + // from. In such cases, the actual XMP data begins at the same offset. + if (XMP_JPEG_PREAMBLE.equalsIgnoreCase(new String(segmentBytes, 0, preambleLength)) || + "XMP".equalsIgnoreCase(new String(segmentBytes, 0, 3))) { - // check for the header length - if (segmentBytes.length <= preambleLength + 1) { - directory.addError(String.format("Xmp data segment must contain at least %d bytes", preambleLength + 1)); - return; - } + byte[] xmlBytes = new byte[segmentBytes.length - preambleLength]; + System.arraycopy(segmentBytes, preambleLength, xmlBytes, 0, xmlBytes.length); + extract(xmlBytes, metadata); + // Check in the Standard XMP if there should be a Extended XMP part in other chunks. + extendedXMPGUID = getExtendedXMPGUID(metadata); + continue; + } + } - String preamble = new String(segmentBytes, 0, preambleLength); - if (!"http://ns.adobe.com/xap/1.0/\0".equals(preamble)) { - directory.addError("XMP data segment doesn't begin with 'http://ns.adobe.com/xap/1.0/'"); - return; + // If we know that there's Extended XMP chunks, look for them. + if (extendedXMPGUID != null && + segmentBytes.length >= extensionPreambleLength && + XMP_EXTENSION_JPEG_PREAMBLE.equalsIgnoreCase(new String(segmentBytes, 0, extensionPreambleLength))) { + + extendedXMPBuffer = processExtendedXMPChunk(metadata, segmentBytes, extendedXMPGUID, extendedXMPBuffer); + } } - byte[] xmlBytes = new byte[segmentBytes.length - preambleLength]; - System.arraycopy(segmentBytes, 29, xmlBytes, 0, xmlBytes.length); - extract(xmlBytes, metadata); + // Now that the Extended XMP chunks have been concatenated, let's parse and merge with the Standard XMP. + if (extendedXMPBuffer != null) { + extract(extendedXMPBuffer, metadata); + } } /** @@ -115,14 +132,49 @@ public class XmpReader implements JpegSegmentMetadataReader */ public void extract(@NotNull final byte[] xmpBytes, @NotNull Metadata metadata) { - XmpDirectory directory = metadata.getOrCreateDirectory(XmpDirectory.class); + extract(xmpBytes, metadata, null); + } + + /** + * Performs the XMP data extraction, adding found values to the specified instance of {@link Metadata}. + * <p> + * The extraction is done with Adobe's XMPCore library. + */ + public void extract(@NotNull final byte[] xmpBytes, @NotNull Metadata metadata, @Nullable Directory parentDirectory) + { + extract(xmpBytes, 0, xmpBytes.length, metadata, parentDirectory); + } + + /** + * Performs the XMP data extraction, adding found values to the specified instance of {@link Metadata}. + * <p> + * The extraction is done with Adobe's XMPCore library. + */ + public void extract(@NotNull final byte[] xmpBytes, int offset, int length, @NotNull Metadata metadata, @Nullable Directory parentDirectory) + { + XmpDirectory directory = new XmpDirectory(); + + if (parentDirectory != null) + directory.setParent(parentDirectory); try { - XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes); - processXmpTags(directory, xmpMeta); + XMPMeta xmpMeta; + + // If all xmpBytes are requested, no need to make a new ByteBuffer + if (offset == 0 && length == xmpBytes.length) { + xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes); + } else { + ByteBuffer buffer = new ByteBuffer(xmpBytes, offset, length); + xmpMeta = XMPMetaFactory.parse(buffer.getByteStream()); + } + + directory.setXMPMeta(xmpMeta); } catch (XMPException e) { directory.addError("Error processing XMP data: " + e.getMessage()); } + + if (!directory.isEmpty()) + metadata.addDirectory(directory); } /** @@ -132,130 +184,124 @@ public class XmpReader implements JpegSegmentMetadataReader */ public void extract(@NotNull final String xmpString, @NotNull Metadata metadata) { - XmpDirectory directory = metadata.getOrCreateDirectory(XmpDirectory.class); + extract(xmpString, metadata, null); + } + + /** + * Performs the XMP data extraction, adding found values to the specified instance of {@link Metadata}. + * <p> + * The extraction is done with Adobe's XMPCore library. + */ + public void extract(@NotNull final StringValue xmpString, @NotNull Metadata metadata) + { + extract(xmpString.getBytes(), metadata, null); + } + + /** + * Performs the XMP data extraction, adding found values to the specified instance of {@link Metadata}. + * <p> + * The extraction is done with Adobe's XMPCore library. + */ + public void extract(@NotNull final String xmpString, @NotNull Metadata metadata, @Nullable Directory parentDirectory) + { + XmpDirectory directory = new XmpDirectory(); + + if (parentDirectory != null) + directory.setParent(parentDirectory); try { XMPMeta xmpMeta = XMPMetaFactory.parseFromString(xmpString); - processXmpTags(directory, xmpMeta); + directory.setXMPMeta(xmpMeta); } catch (XMPException e) { directory.addError("Error processing XMP data: " + e.getMessage()); } + + if (!directory.isEmpty()) + metadata.addDirectory(directory); } - private static void processXmpTags(XmpDirectory directory, XMPMeta xmpMeta) throws XMPException + /** + * Determine if there is an extended XMP section based on the standard XMP part. + * The xmpNote:HasExtendedXMP attribute contains the GUID of the Extended XMP chunks. + */ + @Nullable + private static String getExtendedXMPGUID(@NotNull Metadata metadata) { - // store the XMPMeta object on the directory in case others wish to use it - directory.setXMPMeta(xmpMeta); + final Collection<XmpDirectory> xmpDirectories = metadata.getDirectoriesOfType(XmpDirectory.class); - // read all the tags and send them to the directory - // I've added some popular tags, feel free to add more tags - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_ADDITIONAL_PROPERTIES, "aux:LensInfo", XmpDirectory.TAG_LENS_INFO, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_ADDITIONAL_PROPERTIES, "aux:Lens", XmpDirectory.TAG_LENS, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_ADDITIONAL_PROPERTIES, "aux:SerialNumber", XmpDirectory.TAG_CAMERA_SERIAL_NUMBER, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_ADDITIONAL_PROPERTIES, "aux:Firmware", XmpDirectory.TAG_FIRMWARE, FMT_STRING); + for (XmpDirectory directory : xmpDirectories) { + final XMPMeta xmpMeta = directory.getXMPMeta(); - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_TIFF_PROPERTIES, "tiff:Make", XmpDirectory.TAG_MAKE, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_TIFF_PROPERTIES, "tiff:Model", XmpDirectory.TAG_MODEL, FMT_STRING); + try { + final XMPIterator itr = xmpMeta.iterator(SCHEMA_XMP_NOTES, null, null); + if (itr == null) + continue; - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:ExposureTime", XmpDirectory.TAG_EXPOSURE_TIME, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:ExposureProgram", XmpDirectory.TAG_EXPOSURE_PROGRAM, FMT_INT); - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:ApertureValue", XmpDirectory.TAG_APERTURE_VALUE, FMT_RATIONAL); - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:FNumber", XmpDirectory.TAG_F_NUMBER, FMT_RATIONAL); - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:FocalLength", XmpDirectory.TAG_FOCAL_LENGTH, FMT_RATIONAL); - processXmpTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:ShutterSpeedValue", XmpDirectory.TAG_SHUTTER_SPEED, FMT_RATIONAL); - - processXmpDateTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:DateTimeOriginal", XmpDirectory.TAG_DATETIME_ORIGINAL); - processXmpDateTag(xmpMeta, directory, SCHEMA_EXIF_SPECIFIC_PROPERTIES, "exif:DateTimeDigitized", XmpDirectory.TAG_DATETIME_DIGITIZED); - - processXmpTag(xmpMeta, directory, SCHEMA_XMP_PROPERTIES, "xmp:Rating", XmpDirectory.TAG_RATING, FMT_DOUBLE); - -/* - // this requires further research - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:title", XmpDirectory.TAG_TITLE, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:subject", XmpDirectory.TAG_SUBJECT, FMT_STRING); - processXmpDateTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:date", XmpDirectory.TAG_DATE); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:type", XmpDirectory.TAG_TYPE, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:description", XmpDirectory.TAG_DESCRIPTION, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:relation", XmpDirectory.TAG_RELATION, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:coverage", XmpDirectory.TAG_COVERAGE, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:creator", XmpDirectory.TAG_CREATOR, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:publisher", XmpDirectory.TAG_PUBLISHER, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:contributor", XmpDirectory.TAG_CONTRIBUTOR, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:rights", XmpDirectory.TAG_RIGHTS, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:format", XmpDirectory.TAG_FORMAT, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:identifier", XmpDirectory.TAG_IDENTIFIER, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:language", XmpDirectory.TAG_LANGUAGE, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:audience", XmpDirectory.TAG_AUDIENCE, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:provenance", XmpDirectory.TAG_PROVENANCE, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:rightsHolder", XmpDirectory.TAG_RIGHTS_HOLDER, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:instructionalMethod", XmpDirectory.TAG_INSTRUCTIONAL_METHOD, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:accrualMethod", XmpDirectory.TAG_ACCRUAL_METHOD, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:accrualPeriodicity", XmpDirectory.TAG_ACCRUAL_PERIODICITY, FMT_STRING); - processXmpTag(xmpMeta, directory, SCHEMA_DUBLIN_CORE_SPECIFIC_PROPERTIES, "dc:accrualPolicy", XmpDirectory.TAG_ACCRUAL_POLICY, FMT_STRING); -*/ - - for (XMPIterator iterator = xmpMeta.iterator(); iterator.hasNext(); ) { - XMPPropertyInfo propInfo = (XMPPropertyInfo) iterator.next(); - String path = propInfo.getPath(); - String value = propInfo.getValue(); - if (path != null && value != null) - directory.addProperty(path, value); + while (itr.hasNext()) { + final XMPPropertyInfo pi = (XMPPropertyInfo) itr.next(); + if (ATTRIBUTE_EXTENDED_XMP.equals(pi.getPath())) { + return pi.getValue(); + } + } + } catch (XMPException e) { + // Fail silently here: we had a reading issue, not a decoding issue. + } } + + return null; } /** - * Reads an property value with given namespace URI and property name. Add property value to directory if exists + * Process an Extended XMP chunk. It will read the bytes from segmentBytes and validates that the GUID the requested one. + * It will progressively fill the buffer with each chunk. + * The format is specified in this document: + * http://www.adobe.com/content/dam/Adobe/en/devnet/xmp/pdfs/XMPSpecificationPart3.pdf + * at page 19 */ - private static void processXmpTag(@NotNull XMPMeta meta, @NotNull XmpDirectory directory, @NotNull String schemaNS, @NotNull String propName, int tagType, int formatCode) throws XMPException + @Nullable + private static byte[] processExtendedXMPChunk(@NotNull Metadata metadata, @NotNull byte[] segmentBytes, @NotNull String extendedXMPGUID, @Nullable byte[] extendedXMPBuffer) { - String property = meta.getPropertyString(schemaNS, propName); - - if (property == null) - return; - - switch (formatCode) { - case FMT_RATIONAL: - String[] rationalParts = property.split("/", 2); - if (rationalParts.length == 2) { - try { - Rational rational = new Rational((long) Float.parseFloat(rationalParts[0]), (long) Float.parseFloat(rationalParts[1])); - directory.setRational(tagType, rational); - } catch (NumberFormatException ex) { - directory.addError(String.format("Unable to parse XMP property %s as a Rational.", propName)); + final int extensionPreambleLength = XMP_EXTENSION_JPEG_PREAMBLE.length(); + final int segmentLength = segmentBytes.length; + final int totalOffset = extensionPreambleLength + EXTENDED_XMP_GUID_LENGTH + EXTENDED_XMP_INT_LENGTH + EXTENDED_XMP_INT_LENGTH; + + if (segmentLength >= totalOffset) { + try { + /* + * The chunk contains: + * - A null-terminated signature string of "http://ns.adobe.com/xmp/extension/". + * - A 128-bit GUID stored as a 32-byte ASCII hex string, capital A-F, no null termination. + * The GUID is a 128-bit MD5 digest of the full ExtendedXMP serialization. + * - The full length of the ExtendedXMP serialization as a 32-bit unsigned integer + * - The offset of this portion as a 32-bit unsigned integer + * - The portion of the ExtendedXMP + */ + final SequentialReader reader = new SequentialByteArrayReader(segmentBytes); + reader.skip(extensionPreambleLength); + final String segmentGUID = reader.getString(EXTENDED_XMP_GUID_LENGTH); + + if (extendedXMPGUID.equals(segmentGUID)) { + final int fullLength = (int)reader.getUInt32(); + final int chunkOffset = (int)reader.getUInt32(); + + if (extendedXMPBuffer == null) + extendedXMPBuffer = new byte[fullLength]; + + if (extendedXMPBuffer.length == fullLength) { + System.arraycopy(segmentBytes, totalOffset, extendedXMPBuffer, chunkOffset, segmentLength - totalOffset); + } else { + XmpDirectory directory = new XmpDirectory(); + directory.addError(String.format("Inconsistent length for the Extended XMP buffer: %d instead of %d", fullLength, extendedXMPBuffer.length)); + metadata.addDirectory(directory); } - } else { - directory.addError("Error in rational format for tag " + tagType); - } - break; - case FMT_INT: - try { - directory.setInt(tagType, Integer.valueOf(property)); - } catch (NumberFormatException ex) { - directory.addError(String.format("Unable to parse XMP property %s as an int.", propName)); - } - break; - case FMT_DOUBLE: - try { - directory.setDouble(tagType, Double.valueOf(property)); - } catch (NumberFormatException ex) { - directory.addError(String.format("Unable to parse XMP property %s as an double.", propName)); } - break; - case FMT_STRING: - directory.setString(tagType, property); - break; - default: - directory.addError(String.format("Unknown format code %d for tag %d", formatCode, tagType)); + } catch (IOException ex) { + XmpDirectory directory = new XmpDirectory(); + directory.addError(ex.getMessage()); + metadata.addDirectory(directory); + } } - } - - @SuppressWarnings({"SameParameterValue"}) - private static void processXmpDateTag(@NotNull XMPMeta meta, @NotNull XmpDirectory directory, @NotNull String schemaNS, @NotNull String propName, int tagType) throws XMPException - { - Calendar cal = meta.getPropertyCalendar(schemaNS, propName); - if (cal != null) { - directory.setDate(tagType, cal.getTime()); - } + return extendedXMPBuffer; } } diff --git a/Source/com/drew/metadata/xmp/XmpWriter.java b/Source/com/drew/metadata/xmp/XmpWriter.java new file mode 100644 index 0000000..462b076 --- /dev/null +++ b/Source/com/drew/metadata/xmp/XmpWriter.java @@ -0,0 +1,37 @@ +package com.drew.metadata.xmp; + +import java.io.OutputStream; + +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.XMPMetaFactory; +import com.adobe.xmp.options.SerializeOptions; +import com.drew.metadata.Metadata; + +public class XmpWriter +{ + /** + * Serializes the XmpDirectory component of <code>Metadata</code> into an <code>OutputStream</code> + * @param os Destination for the xmp data + * @param data populated metadata + * @return serialize success + */ + public static boolean write(OutputStream os, Metadata data) + { + XmpDirectory dir = data.getFirstDirectoryOfType(XmpDirectory.class); + if (dir == null) + return false; + XMPMeta meta = dir.getXMPMeta(); + try + { + SerializeOptions so = new SerializeOptions().setOmitPacketWrapper(true); + XMPMetaFactory.serialize(meta, os, so); + } + catch (XMPException e) + { + e.printStackTrace(); + return false; + } + return true; + } +} diff --git a/Source/com/drew/metadata/xmp/package-info.java b/Source/com/drew/metadata/xmp/package-info.java new file mode 100644 index 0000000..38eb9fe --- /dev/null +++ b/Source/com/drew/metadata/xmp/package-info.java @@ -0,0 +1,4 @@ +/** + * Contains classes for the extraction and modelling of Adobe's XMP metadata. + */ +package com.drew.metadata.xmp; diff --git a/Source/com/drew/metadata/xmp/package.html b/Source/com/drew/metadata/xmp/package.html deleted file mode 100644 index f62e225..0000000 --- a/Source/com/drew/metadata/xmp/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes for the extraction and modelling of Adobe's XMP metadata. - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Source/com/drew/tools/ExtractJpegSegmentTool.java b/Source/com/drew/tools/ExtractJpegSegmentTool.java index f02a550..e62c876 100644 --- a/Source/com/drew/tools/ExtractJpegSegmentTool.java +++ b/Source/com/drew/tools/ExtractJpegSegmentTool.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ import java.util.Set; /** * Extracts JPEG segments and writes them to individual files. - * <p/> + * <p> * Extracting only the required segment(s) for use in unit testing has several benefits: * <ul> * <li>Helps reduce the repository size. For example a small JPEG image may still be 20kB+ in size, yet its diff --git a/Source/com/drew/tools/FileUtil.java b/Source/com/drew/tools/FileUtil.java index 6ea94fb..3c8deba 100644 --- a/Source/com/drew/tools/FileUtil.java +++ b/Source/com/drew/tools/FileUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Source/com/drew/tools/ProcessAllImagesInFolderUtility.java b/Source/com/drew/tools/ProcessAllImagesInFolderUtility.java index 06fe97a..e2fd26e 100644 --- a/Source/com/drew/tools/ProcessAllImagesInFolderUtility.java +++ b/Source/com/drew/tools/ProcessAllImagesInFolderUtility.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,8 +21,13 @@ package com.drew.tools; +import com.adobe.xmp.XMPException; +import com.adobe.xmp.XMPIterator; +import com.adobe.xmp.XMPMeta; +import com.adobe.xmp.properties.XMPPropertyInfo; +import com.drew.imaging.FileType; +import com.drew.imaging.FileTypeDetector; import com.drew.imaging.ImageMetadataReader; -import com.drew.imaging.ImageProcessingException; import com.drew.imaging.jpeg.JpegProcessingException; import com.drew.lang.StringUtil; import com.drew.lang.annotations.NotNull; @@ -33,6 +38,8 @@ import com.drew.metadata.Tag; import com.drew.metadata.exif.ExifIFD0Directory; import com.drew.metadata.exif.ExifSubIFDDirectory; import com.drew.metadata.exif.ExifThumbnailDirectory; +import com.drew.metadata.file.FileMetadataDirectory; +import com.drew.metadata.xmp.XmpDirectory; import java.io.*; import java.util.*; @@ -44,48 +51,70 @@ public class ProcessAllImagesInFolderUtility { public static void main(String[] args) throws IOException, JpegProcessingException { - if (args.length == 0) { - System.err.println("Expects one or more directories as arguments."); - System.exit(1); - } - List<String> directories = new ArrayList<String>(); FileHandler handler = null; + PrintStream log = System.out; - for (String arg : args) { - if (arg.equalsIgnoreCase("-text")) { - // If "-text" is specified, write the discovered metadata into a sub-folder relative to the image + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if (arg.equalsIgnoreCase("--text")) { + // If "--text" is specified, write the discovered metadata into a sub-folder relative to the image handler = new TextFileOutputHandler(); - } else if (arg.equalsIgnoreCase("-markdown")) { - // If "-markdown" is specified, write a summary table in markdown format to standard out + } else if (arg.equalsIgnoreCase("--markdown")) { + // If "--markdown" is specified, write a summary table in markdown format to standard out handler = new MarkdownTableOutputHandler(); + } else if (arg.equalsIgnoreCase("--unknown")) { + // If "--unknown" is specified, write CSV tallying unknown tag counts + handler = new UnknownTagHandler(); + } else if (arg.equalsIgnoreCase("--log-file")) { + if (i == args.length - 1) { + printUsage(); + System.exit(1); + } + log = new PrintStream(new FileOutputStream(args[++i], false), true); } else { // Treat this argument as a directory directories.add(arg); } } + if (directories.isEmpty()) { + System.err.println("Expects one or more directories as arguments."); + printUsage(); + System.exit(1); + } + if (handler == null) { handler = new BasicFileHandler(); } long start = System.nanoTime(); - // Order alphabetically so that output is stable across invocations - Collections.sort(directories); - for (String directory : directories) { - processDirectory(new File(directory), handler, ""); + processDirectory(new File(directory), handler, "", log); } - handler.onCompleted(); + handler.onScanCompleted(log); System.out.println(String.format("Completed in %d ms", (System.nanoTime() - start) / 1000000)); + + if (log != System.out) { + log.close(); + } + } + + private static void printUsage() + { + System.out.println("Usage:"); + System.out.println(); + System.out.println(" java com.drew.tools.ProcessAllImagesInFolderUtility [--text|--markdown|--unknown] [--log-file <file-name>]"); } - private static void processDirectory(@NotNull File path, @NotNull FileHandler handler, @NotNull String relativePath) + private static void processDirectory(@NotNull File path, @NotNull FileHandler handler, @NotNull String relativePath, PrintStream log) { + handler.onStartingDirectory(path); + String[] pathItems = path.list(); if (pathItems == null) { @@ -99,40 +128,51 @@ public class ProcessAllImagesInFolderUtility File file = new File(path, pathItem); if (file.isDirectory()) { - processDirectory(file, handler, relativePath.length() == 0 ? pathItem : relativePath + "/" + pathItem); + processDirectory(file, handler, relativePath.length() == 0 ? pathItem : relativePath + "/" + pathItem, log); } else if (handler.shouldProcess(file)) { - handler.onProcessingStarting(file); + handler.onBeforeExtraction(file, log, relativePath); // Read metadata final Metadata metadata; try { metadata = ImageMetadataReader.readMetadata(file); } catch (Throwable t) { - handler.onException(file, t); + handler.onExtractionError(file, t, log); continue; } - handler.onExtracted(file, metadata, relativePath); + handler.onExtractionSuccess(file, metadata, relativePath, log); } } } interface FileHandler { + /** Called when the scan is about to start processing files in directory <code>path</code>. */ + void onStartingDirectory(@NotNull File directoryPath); + + /** Called to determine whether the implementation should process <code>filePath</code>. */ boolean shouldProcess(@NotNull File file); - void onException(@NotNull File file, @NotNull Throwable throwable); - void onExtracted(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath); - void onCompleted(); - void onProcessingStarting(@NotNull File file); + /** Called before extraction is performed on <code>filePath</code>. */ + void onBeforeExtraction(@NotNull File file, @NotNull PrintStream log, @NotNull String relativePath); + + /** Called when extraction on <code>filePath</code> completed without an exception. */ + void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log); + + /** Called when extraction on <code>filePath</code> resulted in an exception. */ + void onExtractionError(@NotNull File file, @NotNull Throwable throwable, @NotNull PrintStream log); + + /** Called when all files have been processed. */ + void onScanCompleted(@NotNull PrintStream log); } abstract static class FileHandlerBase implements FileHandler { private final Set<String> _supportedExtensions = new HashSet<String>( Arrays.asList( - "jpg", "jpeg", "png", "gif", "bmp", "ico", + "jpg", "jpeg", "png", "gif", "bmp", "ico", "webp", "pcx", "ai", "eps", "nef", "crw", "cr2", "orf", "arw", "raf", "srw", "x3f", "rw2", "rwl", "tif", "tiff", "psd", "dng")); @@ -141,53 +181,48 @@ public class ProcessAllImagesInFolderUtility private int _errorCount = 0; private long _processedByteCount = 0; + public void onStartingDirectory(@NotNull File directoryPath) + {} + public boolean shouldProcess(@NotNull File file) { String extension = getExtension(file); return extension != null && _supportedExtensions.contains(extension.toLowerCase()); } - public void onProcessingStarting(@NotNull File file) + public void onBeforeExtraction(@NotNull File file, @NotNull PrintStream log, @NotNull String relativePath) { _processedFileCount++; _processedByteCount += file.length(); } - public void onException(@NotNull File file, @NotNull Throwable throwable) + public void onExtractionError(@NotNull File file, @NotNull Throwable throwable, @NotNull PrintStream log) { _exceptionCount++; - - if (throwable instanceof ImageProcessingException) { - // this is an error in the Jpeg segment structure. we're looking for bad handling of - // metadata segments. in this case, we didn't even get a segment. - System.err.printf("%s: %s [Error Extracting Metadata]\n\t%s%n", throwable.getClass().getName(), file, throwable.getMessage()); - } else { - // general, uncaught exception during processing of jpeg segments - System.err.printf("%s: %s [Error Extracting Metadata]%n", throwable.getClass().getName(), file); - throwable.printStackTrace(System.err); - } + log.printf("\t[%s] %s\n", throwable.getClass().getName(), throwable.getMessage()); } - public void onExtracted(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath) + public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log) { if (metadata.hasErrors()) { - System.err.println(file); + log.print(file); + log.print('\n'); for (Directory directory : metadata.getDirectories()) { if (!directory.hasErrors()) continue; for (String error : directory.getErrors()) { - System.err.printf("\t[%s] %s%n", directory.getName(), error); + log.printf("\t[%s] %s\n", directory.getName(), error); _errorCount++; } } } } - public void onCompleted() + public void onScanCompleted(@NotNull PrintStream log) { if (_processedFileCount > 0) { - System.out.println(String.format( - "Processed %,d files (%,d bytes) with %,d exceptions and %,d file errors", + log.print(String.format( + "Processed %,d files (%,d bytes) with %,d exceptions and %,d file errors\n", _processedFileCount, _processedByteCount, _exceptionCount, _errorCount )); } @@ -211,10 +246,53 @@ public class ProcessAllImagesInFolderUtility */ static class TextFileOutputHandler extends FileHandlerBase { + /** Standardise line ending so that generated files can be more easily diffed. */ + private static final String NEW_LINE = "\n"; + + @Override + public void onStartingDirectory(@NotNull File directoryPath) + { + super.onStartingDirectory(directoryPath); + + // Delete any existing 'metadata' folder + File metadataDirectory = new File(directoryPath + "/metadata"); + if (metadataDirectory.exists()) + deleteRecursively(metadataDirectory); + } + + private static void deleteRecursively(@NotNull File directory) + { + if (!directory.isDirectory()) + throw new IllegalArgumentException("Must be a directory."); + + if (directory.exists()) { + String[] list = directory.list(); + if (list != null) { + for (String item : list) { + File file = new File(item); + if (file.isDirectory()) + deleteRecursively(file); + else + file.delete(); + } + } + } + + directory.delete(); + } + + @Override + public void onBeforeExtraction(@NotNull File file, @NotNull PrintStream log, @NotNull String relativePath) + { + super.onBeforeExtraction(file, log, relativePath); + log.print(file.getAbsoluteFile()); + log.print(NEW_LINE); + } + @Override - public void onExtracted(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath) + public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log) { - super.onExtracted(file, metadata, relativePath); + super.onExtractionSuccess(file, metadata, relativePath, log); try { PrintWriter writer = null; @@ -227,25 +305,67 @@ public class ProcessAllImagesInFolderUtility for (Directory directory : metadata.getDirectories()) { if (!directory.hasErrors()) continue; - for (String error : directory.getErrors()) { - writer.format("[ERROR: %s] %s\n", directory.getName(), error); - } + for (String error : directory.getErrors()) + writer.format("[ERROR: %s] %s%s", directory.getName(), error, NEW_LINE); } - writer.write("\n"); + writer.write(NEW_LINE); } - // Iterate through all values + // Write tag values for each directory for (Directory directory : metadata.getDirectories()) { String directoryName = directory.getName(); + // Write the directory's tags for (Tag tag : directory.getTags()) { String tagName = tag.getTagName(); String description = tag.getDescription(); - writer.format("[%s - %s] %s = %s%n", directoryName, tag.getTagTypeHex(), tagName, description); + if (description == null) + description = ""; + // Skip the file write-time as this changes based on the time at which the regression test image repository was cloned + if (directory instanceof FileMetadataDirectory && tag.getTagType() == FileMetadataDirectory.TAG_FILE_MODIFIED_DATE) + description = "<omitted for regression testing as checkout dependent>"; + writer.format("[%s - %s] %s = %s%s", directoryName, tag.getTagTypeHex(), tagName, description, NEW_LINE); } - if (directory.getTagCount() != 0) { - writer.write('\n'); + if (directory.getTagCount() != 0) + writer.write(NEW_LINE); + // Special handling for XMP directory data + if (directory instanceof XmpDirectory) { + boolean wrote = false; + XmpDirectory xmpDirectory = (XmpDirectory)directory; + XMPMeta xmpMeta = xmpDirectory.getXMPMeta(); + try { + XMPIterator iterator = xmpMeta.iterator(); + while (iterator.hasNext()) { + XMPPropertyInfo prop = (XMPPropertyInfo)iterator.next(); + String ns = prop.getNamespace(); + String path = prop.getPath(); + String value = prop.getValue(); + + if (ns == null) + ns = ""; + if (path == null) + path = ""; + + final int MAX_XMP_VALUE_LENGTH = 512; + if (value == null) + value = ""; + else if (value.length() > MAX_XMP_VALUE_LENGTH) + value = String.format("%s <truncated from %d characters>", value.substring(0, MAX_XMP_VALUE_LENGTH), value.length()); + + writer.format("[XMPMeta - %s] %s = %s%s", ns, path, value, NEW_LINE); + wrote = true; + } + } catch (XMPException e) { + e.printStackTrace(); + } + if (wrote) + writer.write(NEW_LINE); } } + + // Write file structure + writeHierarchyLevel(metadata, writer, null, 0); + + writer.write(NEW_LINE); } finally { closeWriter(writer); } @@ -254,22 +374,44 @@ public class ProcessAllImagesInFolderUtility } } + private static void writeHierarchyLevel(@NotNull Metadata metadata, @NotNull PrintWriter writer, @Nullable Directory parent, int level) + { + final int indent = 4; + + for (Directory child : metadata.getDirectories()) { + if (parent == null) { + if (child.getParent() != null) + continue; + } else if (!parent.equals(child.getParent())) { + continue; + } + + for (int i = 0; i < level*indent; i++) { + writer.write(' '); + } + writer.write("- "); + writer.write(child.getName()); + writer.write(NEW_LINE); + writeHierarchyLevel(metadata, writer, child, level + 1); + } + } + @Override - public void onException(@NotNull File file, @NotNull Throwable throwable) + public void onExtractionError(@NotNull File file, @NotNull Throwable throwable, @NotNull PrintStream log) { - super.onException(file, throwable); + super.onExtractionError(file, throwable, log); try { PrintWriter writer = null; try { writer = openWriter(file); - throwable.printStackTrace(writer); - writer.write('\n'); + writer.write("EXCEPTION: " + throwable.getMessage() + NEW_LINE); + writer.write(NEW_LINE); } finally { closeWriter(writer); } } catch (IOException e) { - System.err.printf("IO exception writing metadata file: %s%n", e.getMessage()); + log.printf("IO exception writing metadata file: %s%s", e.getMessage(), NEW_LINE); } } @@ -281,10 +423,25 @@ public class ProcessAllImagesInFolderUtility if (!metadataDir.exists()) metadataDir.mkdir(); - String outputPath = String.format("%s/metadata/%s.txt", file.getParent(), file.getName().toLowerCase()); - FileWriter writer = new FileWriter(outputPath, false); - writer.write("FILE: " + file.getName() + "\n"); - writer.write('\n'); + String outputPath = String.format("%s/metadata/%s.txt", file.getParent(), file.getName()); + Writer writer = new OutputStreamWriter( + new FileOutputStream(outputPath), + "UTF-8" + ); + writer.write("FILE: " + file.getName() + NEW_LINE); + + // Detect file type + BufferedInputStream stream = null; + try { + stream = new BufferedInputStream(new FileInputStream(file)); + FileType fileType = FileTypeDetector.detectFileType(stream); + writer.write(String.format("TYPE: %s" + NEW_LINE, fileType.toString().toUpperCase())); + writer.write(NEW_LINE); + } finally { + if (stream != null) { + stream.close(); + } + } return new PrintWriter(writer); } @@ -292,8 +449,8 @@ public class ProcessAllImagesInFolderUtility private static void closeWriter(@Nullable Writer writer) throws IOException { if (writer != null) { - writer.write("Generated using metadata-extractor\n"); - writer.write("https://drewnoakes.com/code/exif/\n"); + writer.write("Generated using metadata-extractor" + NEW_LINE); + writer.write("https://drewnoakes.com/code/exif/" + NEW_LINE); writer.flush(); writer.close(); } @@ -308,7 +465,7 @@ public class ProcessAllImagesInFolderUtility private final Map<String, String> _extensionEquivalence = new HashMap<String, String>(); private final Map<String, List<Row>> _rowListByExtension = new HashMap<String, List<Row>>(); - class Row + static class Row { final File file; final Metadata metadata; @@ -325,9 +482,9 @@ public class ProcessAllImagesInFolderUtility this.metadata = metadata; this.relativePath = relativePath; - ExifIFD0Directory ifd0Dir = metadata.getDirectory(ExifIFD0Directory.class); - ExifSubIFDDirectory subIfdDir = metadata.getDirectory(ExifSubIFDDirectory.class); - ExifThumbnailDirectory thumbDir = metadata.getDirectory(ExifThumbnailDirectory.class); + ExifIFD0Directory ifd0Dir = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + ExifSubIFDDirectory subIfdDir = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); + ExifThumbnailDirectory thumbDir = metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class); if (ifd0Dir != null) { manufacturer = ifd0Dir.getDescription(ExifIFD0Directory.TAG_MAKE); model = ifd0Dir.getDescription(ExifIFD0Directory.TAG_MODEL); @@ -338,8 +495,8 @@ public class ProcessAllImagesInFolderUtility hasMakernoteData = subIfdDir.containsTag(ExifSubIFDDirectory.TAG_MAKERNOTE); } if (thumbDir != null) { - Integer width = thumbDir.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_WIDTH); - Integer height = thumbDir.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT); + Integer width = thumbDir.getInteger(ExifThumbnailDirectory.TAG_IMAGE_WIDTH); + Integer height = thumbDir.getInteger(ExifThumbnailDirectory.TAG_IMAGE_HEIGHT); thumbnail = width != null && height != null ? String.format("Yes (%s x %s)", width, height) : "Yes"; @@ -347,6 +504,7 @@ public class ProcessAllImagesInFolderUtility for (Directory directory : metadata.getDirectories()) { if (directory.getClass().getName().contains("Makernote")) { makernote = directory.getName().replace("Makernote", "").trim(); + break; } } if (makernote == null) { @@ -361,9 +519,9 @@ public class ProcessAllImagesInFolderUtility } @Override - public void onExtracted(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath) + public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log) { - super.onExtracted(file, metadata, relativePath); + super.onExtractionSuccess(file, metadata, relativePath, log); String extension = getExtension(file); @@ -374,7 +532,7 @@ public class ProcessAllImagesInFolderUtility // Sanitise the extension extension = extension.toLowerCase(); if (_extensionEquivalence.containsKey(extension)) - extension =_extensionEquivalence.get(extension); + extension = _extensionEquivalence.get(extension); List<Row> list = _rowListByExtension.get(extension); if (list == null) { @@ -385,9 +543,9 @@ public class ProcessAllImagesInFolderUtility } @Override - public void onCompleted() + public void onScanCompleted(@NotNull PrintStream log) { - super.onCompleted(); + super.onScanCompleted(log); OutputStream outputStream = null; PrintStream stream = null; @@ -415,12 +573,14 @@ public class ProcessAllImagesInFolderUtility Writer writer = new OutputStreamWriter(stream); writer.write("# Image Database Summary\n\n"); - for (String extension : _rowListByExtension.keySet()) { + for (Map.Entry<String, List<Row>> entry : _rowListByExtension.entrySet()) { + String extension = entry.getKey(); writer.write("## " + extension.toUpperCase() + " Files\n\n"); writer.write("File|Manufacturer|Model|Dir Count|Exif?|Makernote|Thumbnail|All Data\n"); writer.write("----|------------|-----|---------|-----|---------|---------|--------\n"); - List<Row> rows = _rowListByExtension.get(extension); + + List<Row> rows = entry.getValue(); // Order by manufacturer, then model Collections.sort(rows, new Comparator<Row>() { @@ -432,7 +592,7 @@ public class ProcessAllImagesInFolderUtility }); for (Row row : rows) { - writer.write(String.format("[%s](https://raw.githubusercontent.com/drewnoakes/metadata-extractor-images/master/%s/%s)|%s|%s|%d|%s|%s|%s|[metadata](https://raw.githubusercontent.com/drewnoakes/metadata-extractor-images/master/%s/metadata/%s.txt)%n", + writer.write(String.format("[%s](https://raw.githubusercontent.com/drewnoakes/metadata-extractor-images/master/%s/%s)|%s|%s|%d|%s|%s|%s|[metadata](https://raw.githubusercontent.com/drewnoakes/metadata-extractor-images/master/%s/metadata/%s.txt)\n", row.file.getName(), row.relativePath, StringUtil.urlEncode(row.file.getName()), @@ -453,6 +613,66 @@ public class ProcessAllImagesInFolderUtility } } + /** + * Keeps track of unknown tags. + */ + static class UnknownTagHandler extends FileHandlerBase + { + private HashMap<String, HashMap<Integer, Integer>> _occurrenceCountByTagByDirectory = new HashMap<String, HashMap<Integer, Integer>>(); + + @Override + public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log) + { + super.onExtractionSuccess(file, metadata, relativePath, log); + + for (Directory directory : metadata.getDirectories()) { + for (Tag tag : directory.getTags()) { + + // Only interested in unknown tags (those without names) + if (tag.hasTagName()) + continue; + + HashMap<Integer, Integer> occurrenceCountByTag = _occurrenceCountByTagByDirectory.get(directory.getName()); + if (occurrenceCountByTag == null) { + occurrenceCountByTag = new HashMap<Integer, Integer>(); + _occurrenceCountByTagByDirectory.put(directory.getName(), occurrenceCountByTag); + } + + Integer count = occurrenceCountByTag.get(tag.getTagType()); + if (count == null) { + count = 0; + occurrenceCountByTag.put(tag.getTagType(), 0); + } + + occurrenceCountByTag.put(tag.getTagType(), count + 1); + } + } + } + + @Override + public void onScanCompleted(@NotNull PrintStream log) + { + super.onScanCompleted(log); + + for (Map.Entry<String, HashMap<Integer, Integer>> pair1 : _occurrenceCountByTagByDirectory.entrySet()) { + String directoryName = pair1.getKey(); + List<Map.Entry<Integer, Integer>> counts = new ArrayList<Map.Entry<Integer, Integer>>(pair1.getValue().entrySet()); + Collections.sort(counts, new Comparator<Map.Entry<Integer, Integer>>() + { + public int compare(Map.Entry<Integer, Integer> o1, Map.Entry<Integer, Integer> o2) + { + return o2.getValue().compareTo(o1.getValue()); + } + }); + for (Map.Entry<Integer, Integer> pair2 : counts) { + Integer tagType = pair2.getKey(); + Integer count = pair2.getValue(); + log.format("%s, 0x%04X, %d\n", directoryName, tagType, count); + } + } + } + } + /** * Does nothing with the output except enumerate it in memory and format descriptions. This is useful in order to * flush out any potential exceptions raised during the formatting of extracted value descriptions. @@ -460,9 +680,9 @@ public class ProcessAllImagesInFolderUtility static class BasicFileHandler extends FileHandlerBase { @Override - public void onExtracted(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath) + public void onExtractionSuccess(@NotNull File file, @NotNull Metadata metadata, @NotNull String relativePath, @NotNull PrintStream log) { - super.onExtracted(file, metadata, relativePath); + super.onExtractionSuccess(file, metadata, relativePath, log); // Iterate through all values, calling toString to flush out any formatting exceptions for (Directory directory : metadata.getDirectories()) { diff --git a/Source/com/drew/tools/ProcessUrlUtility.java b/Source/com/drew/tools/ProcessUrlUtility.java index 680c9da..8308e7a 100644 --- a/Source/com/drew/tools/ProcessUrlUtility.java +++ b/Source/com/drew/tools/ProcessUrlUtility.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,7 +67,7 @@ public class ProcessUrlUtility } catch (ImageProcessingException e) { // this is an error in the Jpeg segment structure. we're looking for bad handling of // metadata segments. in this case, we didn't even get a segment. - System.err.printf("%s: %s [Error Extracting Metadata]\n\t%s%n", e.getClass().getName(), url, e.getMessage()); return; + System.err.printf("%s: %s [Error Extracting Metadata]%n\t%s%n", e.getClass().getName(), url, e.getMessage()); return; } catch (Throwable t) { // general, uncaught exception during processing of jpeg segments System.err.printf("%s: %s [Error Extracting Metadata]%n", t.getClass().getName(), url); diff --git a/Source/com/drew/tools/package-info.java b/Source/com/drew/tools/package-info.java new file mode 100644 index 0000000..54d0649 --- /dev/null +++ b/Source/com/drew/tools/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains classes used internally by the library, that should not be used in client code and is not included in + * distributions. + */ +package com.drew.tools; diff --git a/Source/com/drew/tools/package.html b/Source/com/drew/tools/package.html deleted file mode 100644 index 8e713fd..0000000 --- a/Source/com/drew/tools/package.html +++ /dev/null @@ -1,33 +0,0 @@ -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> -<html> -<head> -</head> -<body bgcolor="white"> - -Contains classes used internally by the library, that should not be used in client code (not included in distributions). - -<!-- Put @see and @since tags down here. --> - -</body> -</html> diff --git a/Tests/Data/withTypicalHuffman.jpg b/Tests/Data/withTypicalHuffman.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a8de094e9eb9bf8a842ba3bd3d85e32412899dc1 GIT binary patch literal 1570 zcmbW!dpOg390%~<ZniPBY_*BBS=v-9Gb|yw%_UD~A{5D`u}&p-ahy=Zn&d9!ahoAj zh;pfe7_p<rB~s)vp^HxLSvt<tdCqyB^Vd1&{e8ZlKR(~j^L@Ur=ezb|Z4}sGZDC~r zfWQC%k}hEF6<`V=WMtqnFa#V9M<NljD2zNxP7a0NsHBKdRaV=os;r{2jYQqCjkrr) zMTM+K*|k@TPN%Ew*k{O~8Bn$9wDn6sNF)*^hr-Fr<7fmG0_{I%tr0*Y00Y1a0wMxn zGzfwQt+fDZ(s#l@-vay@5Euf5$-ogvSvl!I^#%Y8f<VAf2n+^=N~a^F=KvH9+epwl zD5J!3hZFrUw5X&!gt|#Z6V|T#6G_{{KN=~!X)_M5yj^1lnW9P8(bZ$@(>FCUw_sXY zSs$@?IO^!+e9Y796x-W}b2=a}C^#haLfECtF|k+T;<?EwscGpMnK!cX3;2bCqT-U< zl~wnvYaTqTt!sYL(kg6gf7;R0+xNVG;Kks}v2pRlo5`u^x9?`><`)*1mOrmZ)?FX~ z@|`8E-(ml7p`|V`6bgaD*IgiRh;&2HP#8f=X5&E?+}%%!NQ**XOp@{{nvm++cAu~w z{@t>hNc6Gov+J~PvVRAQ{$H}cV1K(_0w@Sb`gssEZ~$1yIF*1Oj?U+$dq%=bZujeF z#|rNB>s=qZ%(zIIzyGD*m-pM*Gpl>TRa;xmKN7LYrfYyw@?O{ljfC>|$|s6;i-;Y~ zc2||QKN)kI#G?#D-@AVFyT+Sf`Fg(Avf@siG4S*ZK+!s^nVRr<V9Y{4=UfAKx*$m- z-Yel!0jgBt@)29ZsMhk9Rt+)-W7Jfe86>Nnc(;(j^}w0ax!1WI42s9)lpq92mWXyi z)_NnB#tkNy;IE47{fyG^LGcAw1B*mFBU$;z$nYtZ5IlB#+|a_3>Xmc&Mq*~Mcc8&0 zLzyrIrrT+WlFL0;Q|okXbXv-C363$;sG=yWW%pbh&$bWPl|FDh+1_P8<=iiNv%m%T z-NV*h<whLmY>c2luDtW-t<`!ptQYUob#3Wra%6r&6CAhL-o|otQIuO*4%vL|<&U~k zv}7SI6bMu7n|k|$d0us4O!bgs%CJjI)QNH9ymq)@J-a65gQP24=TYCm?kgFZ=%#1; z)I<?GZ8(BSs_%qRSLivj%Eng(dj`Iip%RP7%RCCIs<^`EC*vv{CaP{#so8~RWpb;- zPqUZRiCN~B<^)V*9G8P7V0tJZJf=s}gu=JOiC8vV3aYzo%T0N+(Dco`mAXR}*kk~_ zLc93v(DFiKoA~&`D5ZmvoXrbWcGaIfd2=%MuvjP)t~SjS+t%GrvA#ay+-BH;LhkN` zHK#mf2}kuMyz@JcPWHT(4A0yVkDww;0ZYar)L-7*dnq-pH6Vp#d(Vc4G@gF4x>#1L zQ?+P5ViQw7{AjV#<yc2$vskn9b)lfJL93Hg(GoYOQco6&-*JRBjx~IFdh9KqIllV- zEn6Sv)x+Wv2&LejU<SHYLgk$3?Qr>($o%lJ^miQvzN1|#{!p*2<@})8h>nP<YTW6) zV!ylGNr-T=Nt(_JSbeAJ(2dD$ZN-8Hat_p2B>J8e4d!Q5M_g&Izo}VxP17Ac<B<2z zU+<?}NetGVe;zqxwX6~U+{YTykkO^v`&jrPtXPsIs_UZ?S6I=!V62I)$(y=%ThkK4 z94XL^{UFgOsORpz;)MP*^k{>iHlsdu+3m~3kz>xd8EQ)yJ9|=+IP9vBR^1ue_VH}= zSLSC2{Hm#6SL5x70yWWOh1`P@!9-RXwvXzP<McQ)dw@2&Bp)Sl&<zS6)>tx}8}{Mr n95q(g_k0u2tR-b7hNET}&vJ`4Py~EWE7C+2n@i4BTpRoYHBYC? literal 0 HcmV?d00001 diff --git a/Tests/Data/withXmp.jpg b/Tests/Data/withXmp.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9aa65df4f6e4fe933a2308a32d6fd5d707bf661e GIT binary patch literal 12666 zcmeHNe^e7k9-nN25CjQmTa-nXfR!pFBp^T11Pg&c&<K(!$hGQ{EMZGX!tMqFdVNo& zr(VS>R;xahYD;T>*{3a4^w}!d(w;3nYESV*tF?+++iTSmZLQqwf*~O7^ZxAX4oTRV z@6Y+ncV_qP`}B47oduFyon8k70s+W@f1vMsM3P_TU>tOr!-*xx;z3G=-XIbI*aSCF zK`{`9P0B_E03bkwBDKIrYn1B?Q6w4oE8`W@;^Gx?N+pU|fjD!nA~kWEQjvfnK|o+c z5q}_n*97oQpJOx?=+jYT4)8NF6lu2-6pG}7AZ<>8-e}6xVMbHBHYW>3Xdq12<)g@x zKmbsr82Hc7n~F2$WaVVdL=iXWNO#fD9-mBO2ApBaNSwW>At(&?2)l*Gm@)#ZAnZ`V z;;6Qlkv1E~P&jFaF)>FeC)VkWSh~$g+6l~9PO^eR2W7KhT9*?e?br;GsV#94WrQsj z8weMRIqX<2Zg=5S4W?AaV(iRiSbu~0V);3FraVlJ4V%xft{ke$vk8W&!Soflm7r0? z2Z*O7B&DV*QWVorWE>D0XXsJH0#TS_LJ>QV+;2jHm<L4xMVJvVUH}AfC=$aVoX-9F z4xr90a^Jbgq%1dC3V^`?rob}r146+VxcmTo0e~cJISJV6fE5uu<_ZdY;aC9dE8!QQ zSKyC?z2X~>0FMBV0FMBV0FMBV0FMBV0FMBV0FMBV0FS_*0f7g7N%6;cRRdQ{F!;73 z7-LtEf7`*Lz&9QN9swQ!9swQ!9swQ!9swQ!9swQ!9swSKKNA9d=fufHhJ2us%k8ub zw>U}&nb}bxcjHdELKZIv>gjGLZZ0Pntc0+Vc8#>{oz?@bA4)OOqLCI(GsGL5TB3~1 zsig>G?JSeIw%nX*kxoxjyH)NAX9dCFn7hJer&Vr^lyk3w*X*!dig7ATxkd^zz_bpE zz!GH&nL?VTrYxnZf=u0jDeP&aWenqlSyoq9%c>J(4$3N5q^73I<CSuyG7f6Q(KU7k zcgNZ3r~wC=1Z}2BCqqIW4C@dtakv<b6vpKea^52ld3+B+z$4~xxM!qyGUh}$F*aL| z9gJb-GNKua+*^Y)<)X2(<)U&-ntmcpwq9rQ_<%lTR+eQ$awHpW5v*h>GbFmj?9q3+ zC>s~kVwMv&0<wn~8tN)Mff%xM5VavQV<kh}5|B9KaXdtol?+jJ+FZ1^s*5JrAubb} zrBn{5F+|^5L)sj6$UIVdtVWtC)Uoud4On9y_*I!WLue@i{=$kIWN(?`<5k=To67x@ zdYS0$kZ|QPvnvm?^_HZ&m@)@N=R0VUm1?B-m|aqA*vhE1cm+Y>I?_hi;T7v<xJDjD z!YzT@26Yk>Qc|X+D3l9~S?RK;o!0wpxTP<^?Xc<-q?pC5Vo1gYy?Y2%-q~s$E;~cj zXru#NG%4m?U8(`G)?tATAot4K)aY<gW_DPY?>(GD;1(MskwL)&lG297oar#*{m6N{ z)shU%a*+1M&8&d5mN5|jVT3YHW*OX@51z^&<E85mTqwdq!ZT+$A#R8ctZb`;U1g*9 zu!+L$bOlM%uuKbxQi3Av?0UxCUsC;rYQW101FQtw?^SC5K4N#2iDYX!J{~e5xzF!y zCoRq$qJ<R65ENWbmP<KO8aA8GL2(7Hk?L?8O^l?(ZYm=I>5#Tru8I=Y-w0Zc3a4j> z-Y?dXZpK9sdJ8=C$Wn4-A6afQJebHTLhGo4A+2z>{W+xt`uB4n)$GZo;_@}RoK72b z#C7zRG!H9OE&pw$K|lBIBr47icNpU~ZU4HG1nr54VceZ91Gjn&4Gh}IG)SXV%h@{> zyuRlgO1U9#8<^CCMpVNShIW-aO_&*PmuwK|5tDlc)$$QE)x%G`e1f7K@Wg}L-D5d8 zsUhwj#FWj>%pUCyYPdTiQRy`_h|#da<Tvs1w&&(HG)}*r*AoI4tbY_fc)*@o?s<Yq z8+-_YbX|?K8b06jodR0mH)@pcC?7vxUth7<&p%*XV8H0n0pml)28|1!5HT@)0*YeM zvdNe<HVQ>!k{^qWS0pATModoCq$t(0ghVClL?9N614ajg1_p*IBhg6Z|K0iyfnYzN z1{R?}3XoudFj&xc1fY->iwGfM+S3pr@DYec`T9YrLIMOL0V482e0=&<1OO3=f_*|F z73s4_jg{h+<90`7EZN@qsIg?};UA$%C}dx=1nj}anLtToFbpLSLI0j6K!PKMA&T^| zv!#_w`u+_9gitwH7!0O^8!xu(-u=a*WksD0Jt%m5{oC2b?XNfg@G=z^xjg7v<JQ+s z&%M?0eepAo90-3dxb+vs#_d6OzKCAv^X=xOy>F4VTi(aV?aVFe+Hy3gZNZiCW5RFI z^Lta;1FpsFJ^Y{0qSl@=_s8`c@eKv@(6@rFgR`Bht*06#`(wVfbSC9=PizlpZd)rh zq=`S;H4``ct8~k+hHW|bS)w&UvsSVO)I~okse9IMkK~0`$(p4PXM}9b2$5hh&-ur$ zoN<2tH-DX?ym{o(H>V%F9u;(WVYi>}uFAHZpUjs<ePvnt*}BWuQ*PJeTd%4|KOebG zyQ1gn2X!Z6lJmc8T6Kh=r$4lIS$zjM`^=jgs@DCgI<7uCsr)aSriLwVY3lxU!A9Yc ziHi@&YWFW{nt6jX=pJrKx}!0jZVC^l>L(jS2Fb&T_5f7zbrjlq+CA}+>)&_n>m_$p z^=#5tuG$91wDqRm%6U%f%6;hVkElg+OBR-2&6zOqiPX!-rd)2!d+YMC!e+@LRNYdP zE!>tGQTgNkxr$Fe`uwi%EVpeXvE<at@}9dz<0GP$cC|jGs+|2smsA$#2<h3<(%z!U ztsMRIcaxJp*&|uAG2^f}GBYwVzAhSjc1>L!glLJ-KlWUvxMZj8pI>FK#1~sX|B_ho zehvt{X1RN@_U57XEAMoIO<4(#b><11j_pbkAI$&s_D@T;Ex#N5^Y-Qf!xU}p7W(AE zLk$Pw=TFEB>1=bNzpQflAOAeJ_stKQ_w}}6aZb^))n`{@O~#)$WR|?UE_zniq>n`} zQO-WleIjntYd7o9#ICP6ad6&-DgQW$yN^D3=JlQP=7(NrDEh`65qY$`v%bA0vygDD z1M8Qrc>O|-uIlmdA78z+b9T|xg{H0TNOI%fE)f4!zj~1zAMx&)<9%RiP{Gu9a|4dW z<eGb$#vYtol5%m>Q;mCOG{2!(8YZp@-yGJszwKX7Mdb!`j4S@(#MZ{F(3>~9pUhox z?L+PEMY7Fbp07I>e5v`nj^3Ycymvk|P~5Tp?AV69mY5BDcNF8d<sC^oeoo$Zz3lds zJ;z@ziE3UQ7<%cU#hdG2q}RMq6MZXgWq8D~!spXmukP)d6jp4?Ub47})_wKP#V7he z{)XGHs7H5a+upeNOT^zVUEaE@aMd$`3%_1_CvN;5t$FpK<BetQ-4_ZTInbhQm|L~$ zXk|;<?%I;-c`y3DRONg1z3!U_PrY*H{0Z5`9f!q-L+O*x&ooT@wxOVRpOyT0ktkCX SjipDgYtW}%iP_=Tck+Ms4%t)y literal 0 HcmV?d00001 diff --git a/Tests/com/drew/imaging/jpeg/JpegMetadataReaderTest.java b/Tests/com/drew/imaging/jpeg/JpegMetadataReaderTest.java index 17e5119..cda6d54 100644 --- a/Tests/com/drew/imaging/jpeg/JpegMetadataReaderTest.java +++ b/Tests/com/drew/imaging/jpeg/JpegMetadataReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,18 @@ package com.drew.imaging.jpeg; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.exif.ExifSubIFDDirectory; +import com.drew.metadata.jpeg.HuffmanTablesDirectory; +import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable; +import com.drew.metadata.xmp.XmpDirectory; import org.junit.Test; import java.io.File; import java.io.FileInputStream; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; /** * @author Drew Noakes https://drewnoakes.com @@ -48,10 +53,39 @@ public class JpegMetadataReaderTest validate(JpegMetadataReader.readMetadata(new FileInputStream((new File("Tests/Data/withExif.jpg"))))); } + @Test + public void testExtractXmpMetadata() throws Exception + { + Metadata metadata = JpegMetadataReader.readMetadata(new File("Tests/Data/withXmp.jpg")); + Directory directory = metadata.getFirstDirectoryOfType(XmpDirectory.class); + assertNotNull(directory); + directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class); + assertNotNull(directory); + assertTrue(((HuffmanTablesDirectory) directory).isOptimized()); + } + + @Test + public void testTypicalHuffman() throws Exception + { + Metadata metadata = JpegMetadataReader.readMetadata(new File("Tests/Data/withTypicalHuffman.jpg")); + Directory directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class); + assertNotNull(directory); + assertTrue(((HuffmanTablesDirectory) directory).isTypical()); + assertFalse(((HuffmanTablesDirectory) directory).isOptimized()); + for (int i = 0; i < ((HuffmanTablesDirectory) directory).getNumberOfTables(); i++) { + HuffmanTable table = ((HuffmanTablesDirectory) directory).getTable(i); + assertTrue(table.isTypical()); + assertFalse(table.isOptimized()); + } + } + private void validate(Metadata metadata) { - Directory directory = metadata.getDirectory(ExifSubIFDDirectory.class); + Directory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); assertNotNull(directory); assertEquals("80", directory.getString(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT)); + directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class); + assertNotNull(directory); + assertTrue(((HuffmanTablesDirectory) directory).isOptimized()); } } diff --git a/Tests/com/drew/imaging/jpeg/JpegSegmentDataTest.java b/Tests/com/drew/imaging/jpeg/JpegSegmentDataTest.java index d270343..7474b2b 100644 --- a/Tests/com/drew/imaging/jpeg/JpegSegmentDataTest.java +++ b/Tests/com/drew/imaging/jpeg/JpegSegmentDataTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/imaging/jpeg/JpegSegmentReaderTest.java b/Tests/com/drew/imaging/jpeg/JpegSegmentReaderTest.java index 7d02a17..edea9ab 100644 --- a/Tests/com/drew/imaging/jpeg/JpegSegmentReaderTest.java +++ b/Tests/com/drew/imaging/jpeg/JpegSegmentReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.junit.Test; import java.io.File; import java.util.Arrays; +import java.util.Collections; import static org.junit.Assert.*; @@ -85,6 +86,7 @@ public class JpegSegmentReaderTest assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPC)); assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPF)); assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.COM)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.DAC)); assertEquals(4, segmentData.getSegmentCount(JpegSegmentType.DHT)); assertEquals(2, segmentData.getSegmentCount(JpegSegmentType.DQT)); assertEquals(1, segmentData.getSegmentCount(JpegSegmentType.SOF0)); @@ -127,6 +129,34 @@ public class JpegSegmentReaderTest segmentData.getSegment(JpegSegmentType.APP2)); } + @Test + public void testReadDhtSegment() throws Exception + { + JpegSegmentData segmentData = JpegSegmentReader.readSegments( + new File("Tests/Data/withExifAndIptc.jpg"), + Collections.singletonList(JpegSegmentType.DHT)); + + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP0)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP1)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP2)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPD)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPE)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP3)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP4)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP5)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP6)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP7)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP8)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APP9)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPA)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPB)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPC)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.APPF)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.COM)); + assertEquals(4, segmentData.getSegmentCount(JpegSegmentType.DHT)); + assertEquals(0, segmentData.getSegmentCount(JpegSegmentType.SOF0)); + } + @Test public void testLoadJpegWithoutExifDataReturnsNull() throws Exception { diff --git a/Tests/com/drew/imaging/png/PngChunkReaderTest.java b/Tests/com/drew/imaging/png/PngChunkReaderTest.java index c0af9cb..d9ba01b 100644 --- a/Tests/com/drew/imaging/png/PngChunkReaderTest.java +++ b/Tests/com/drew/imaging/png/PngChunkReaderTest.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.lang.Iterables; diff --git a/Tests/com/drew/imaging/png/PngChunkTypeTest.java b/Tests/com/drew/imaging/png/PngChunkTypeTest.java index 12b0a0e..af8893c 100644 --- a/Tests/com/drew/imaging/png/PngChunkTypeTest.java +++ b/Tests/com/drew/imaging/png/PngChunkTypeTest.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import org.junit.Test; @@ -15,7 +35,7 @@ public class PngChunkTypeTest try { new PngChunkType("TooLong"); fail("Expecting exception"); - } catch (IllegalArgumentException ex) { + } catch (PngProcessingException ex) { assertEquals("PNG chunk type identifier must be four bytes in length", ex.getMessage()); } } @@ -26,7 +46,7 @@ public class PngChunkTypeTest try { new PngChunkType("foo"); fail("Expecting exception"); - } catch (IllegalArgumentException ex) { + } catch (PngProcessingException ex) { assertEquals("PNG chunk type identifier must be four bytes in length", ex.getMessage()); } } @@ -40,7 +60,7 @@ public class PngChunkTypeTest try { new PngChunkType(invalidString); fail("Expecting exception"); - } catch (IllegalArgumentException ex) { + } catch (PngProcessingException ex) { assertEquals("PNG chunk type identifier may only contain alphabet characters", ex.getMessage()); } } diff --git a/Tests/com/drew/imaging/png/PngMetadataReaderTest.java b/Tests/com/drew/imaging/png/PngMetadataReaderTest.java index 0abeeae..6bf8809 100644 --- a/Tests/com/drew/imaging/png/PngMetadataReaderTest.java +++ b/Tests/com/drew/imaging/png/PngMetadataReaderTest.java @@ -1,15 +1,37 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.png; import com.drew.lang.KeyValuePair; import com.drew.lang.annotations.NotNull; -import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.png.PngDirectory; import org.junit.Test; import java.io.FileInputStream; import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Collection; import java.util.List; +import java.util.Locale; import java.util.TimeZone; import static org.junit.Assert.*; @@ -19,14 +41,6 @@ import static org.junit.Assert.*; */ public class PngMetadataReaderTest { - @NotNull - public static <T extends Directory> T processFile(@NotNull String filePath, @NotNull Class<T> directoryClass) throws IOException, PngProcessingException - { - T directory = processFile(filePath).getDirectory(directoryClass); - assertNotNull(directory); - return directory; - } - @NotNull private static Metadata processFile(@NotNull String filePath) throws PngProcessingException, IOException { @@ -49,25 +63,53 @@ public class PngMetadataReaderTest try { TimeZone.setDefault(TimeZone.getTimeZone("GMT")); - PngDirectory directory = processFile("Tests/Data/gimp-8x12-greyscale-alpha-time-background.png", PngDirectory.class); - - assertEquals(8, directory.getInt(PngDirectory.TAG_IMAGE_WIDTH)); - assertEquals(12, directory.getInt(PngDirectory.TAG_IMAGE_HEIGHT)); - assertEquals(8, directory.getInt(PngDirectory.TAG_BITS_PER_SAMPLE)); - assertEquals(4, directory.getInt(PngDirectory.TAG_COLOR_TYPE)); - assertEquals(0, directory.getInt(PngDirectory.TAG_COMPRESSION_TYPE)); - assertEquals(0, directory.getInt(PngDirectory.TAG_FILTER_METHOD)); - assertEquals(0, directory.getInt(PngDirectory.TAG_INTERLACE_METHOD)); - assertEquals(0.45455, directory.getDouble(PngDirectory.TAG_GAMMA), 0.00001); - assertArrayEquals(new byte[]{0, 52}, directory.getByteArray(PngDirectory.TAG_BACKGROUND_COLOR)); + Metadata metadata = processFile("Tests/Data/gimp-8x12-greyscale-alpha-time-background.png"); + Collection<PngDirectory> directories = metadata.getDirectoriesOfType(PngDirectory.class); + + assertNotNull(directories); + assertEquals(6, directories.size()); + + PngDirectory[] dirs = new PngDirectory[directories.size()]; + directories.toArray(dirs); + + assertEquals(PngChunkType.IHDR, dirs[0].getPngChunkType()); + assertEquals(8, dirs[0].getInt(PngDirectory.TAG_IMAGE_WIDTH)); + assertEquals(12, dirs[0].getInt(PngDirectory.TAG_IMAGE_HEIGHT)); + assertEquals(8, dirs[0].getInt(PngDirectory.TAG_BITS_PER_SAMPLE)); + assertEquals(4, dirs[0].getInt(PngDirectory.TAG_COLOR_TYPE)); + assertEquals(0, dirs[0].getInt(PngDirectory.TAG_COMPRESSION_TYPE)); + assertEquals(0, dirs[0].getInt(PngDirectory.TAG_FILTER_METHOD)); + assertEquals(0, dirs[0].getInt(PngDirectory.TAG_INTERLACE_METHOD)); + + assertEquals(PngChunkType.gAMA, dirs[1].getPngChunkType()); + assertEquals(0.45455, dirs[1].getDouble(PngDirectory.TAG_GAMMA), 0.00001); + + assertEquals(PngChunkType.bKGD, dirs[2].getPngChunkType()); + assertArrayEquals(new byte[]{0, 52}, dirs[2].getByteArray(PngDirectory.TAG_BACKGROUND_COLOR)); + //noinspection ConstantConditions - assertEquals("Tue Jan 01 04:08:30 GMT 2013", directory.getDate(PngDirectory.TAG_LAST_MODIFICATION_TIME).toString()); + assertEquals(PngChunkType.pHYs, dirs[3].getPngChunkType()); + assertEquals(1, dirs[3].getInt(PngDirectory.TAG_UNIT_SPECIFIER)); + assertEquals(2835, dirs[3].getInt(PngDirectory.TAG_PIXELS_PER_UNIT_X)); + assertEquals(2835, dirs[3].getInt(PngDirectory.TAG_PIXELS_PER_UNIT_Y)); + + assertEquals(PngChunkType.tIME, dirs[4].getPngChunkType()); + assertEquals("2013:01:01 04:08:30", dirs[4].getString(PngDirectory.TAG_LAST_MODIFICATION_TIME)); + + java.util.Date modTime = dirs[4].getDate(PngDirectory.TAG_LAST_MODIFICATION_TIME); + SimpleDateFormat formatter = new SimpleDateFormat("EE MMM DD HH:mm:ss z yyyy", Locale.US); + formatter.setTimeZone(TimeZone.getTimeZone("GMT")); + assertEquals("Tue Jan 01 04:08:30 GMT 2013", formatter.format(modTime)); + assertNotNull(modTime); + assertEquals(1357013310000L, modTime.getTime()); + + assertEquals(PngChunkType.iTXt, dirs[5].getPngChunkType()); @SuppressWarnings("unchecked") - List<KeyValuePair> pairs = (List<KeyValuePair>)directory.getObject(PngDirectory.TAG_TEXTUAL_DATA); + List<KeyValuePair> pairs = (List<KeyValuePair>)dirs[5].getObject(PngDirectory.TAG_TEXTUAL_DATA); assertNotNull(pairs); assertEquals(1, pairs.size()); - assertEquals("Comment", pairs.get(0).getKey()); - assertEquals("Created with GIMP", pairs.get(0).getValue()); + assertEquals("Comment", pairs.get(0).getKey().toString()); + assertEquals("Created with GIMP", pairs.get(0).getValue().toString()); } finally { TimeZone.setDefault(timeZone); } diff --git a/Tests/com/drew/lang/ByteArrayReaderTest.java b/Tests/com/drew/lang/ByteArrayReaderTest.java index 858f2d7..01403c9 100644 --- a/Tests/com/drew/lang/ByteArrayReaderTest.java +++ b/Tests/com/drew/lang/ByteArrayReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/lang/ByteConvertTest.java b/Tests/com/drew/lang/ByteConvertTest.java new file mode 100644 index 0000000..dcf9f77 --- /dev/null +++ b/Tests/com/drew/lang/ByteConvertTest.java @@ -0,0 +1,25 @@ +package com.drew.lang; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Drew Noakes http://drewnoakes.com + */ +public class ByteConvertTest +{ + @Test + public void toInt32BigEndian() + { + assertEquals(0x01020304, ByteConvert.toInt32BigEndian(new byte[]{1, 2, 3, 4})); + assertEquals(0x01020304, ByteConvert.toInt32BigEndian(new byte[]{1, 2, 3, 4, 5})); + } + + @Test + public void toInt32LittleEndian() + { + assertEquals(0x04030201, ByteConvert.toInt32LittleEndian(new byte[]{1, 2, 3, 4})); + assertEquals(0x04030201, ByteConvert.toInt32LittleEndian(new byte[]{1, 2, 3, 4, 5})); + } +} diff --git a/Tests/com/drew/lang/ByteTrieTest.java b/Tests/com/drew/lang/ByteTrieTest.java index 8b6cd32..701f360 100644 --- a/Tests/com/drew/lang/ByteTrieTest.java +++ b/Tests/com/drew/lang/ByteTrieTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/lang/CompoundExceptionTest.java b/Tests/com/drew/lang/CompoundExceptionTest.java index eb553c0..9b82de8 100644 --- a/Tests/com/drew/lang/CompoundExceptionTest.java +++ b/Tests/com/drew/lang/CompoundExceptionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/lang/GeoLocationTest.java b/Tests/com/drew/lang/GeoLocationTest.java index 7a0807c..e51ea8c 100644 --- a/Tests/com/drew/lang/GeoLocationTest.java +++ b/Tests/com/drew/lang/GeoLocationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/lang/NullOutputStreamTest.java b/Tests/com/drew/lang/NullOutputStreamTest.java index fdf88d7..d707388 100644 --- a/Tests/com/drew/lang/NullOutputStreamTest.java +++ b/Tests/com/drew/lang/NullOutputStreamTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/lang/RandomAccessFileReaderTest.java b/Tests/com/drew/lang/RandomAccessFileReaderTest.java index 1bc75c8..2b4ac15 100644 --- a/Tests/com/drew/lang/RandomAccessFileReaderTest.java +++ b/Tests/com/drew/lang/RandomAccessFileReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ package com.drew.lang; +import com.drew.tools.FileUtil; import org.junit.After; import org.junit.Test; @@ -48,9 +49,7 @@ public class RandomAccessFileReaderTest extends RandomAccessTestBase deleteTempFile(); _tempFile = File.createTempFile("metadata-extractor-test-", ".tmp"); - FileOutputStream stream = new FileOutputStream(_tempFile); - stream.write(bytes); - stream.close(); + FileUtil.saveBytes(_tempFile, bytes); _randomAccessFile = new RandomAccessFile(_tempFile, "r"); return new RandomAccessFileReader(_randomAccessFile); } catch (IOException e) { diff --git a/Tests/com/drew/lang/RandomAccessStreamReaderTest.java b/Tests/com/drew/lang/RandomAccessStreamReaderTest.java index 1f45da8..8295965 100644 --- a/Tests/com/drew/lang/RandomAccessStreamReaderTest.java +++ b/Tests/com/drew/lang/RandomAccessStreamReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/lang/RandomAccessTestBase.java b/Tests/com/drew/lang/RandomAccessTestBase.java index 9d93621..809c2e2 100644 --- a/Tests/com/drew/lang/RandomAccessTestBase.java +++ b/Tests/com/drew/lang/RandomAccessTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -246,19 +246,19 @@ public abstract class RandomAccessTestBase byte[] bytes = new byte[]{0x41, 0x42, 0x43, 0x44, 0x00, 0x45, 0x46, 0x47}; RandomAccessReader reader = createReader(bytes); - assertEquals("", reader.getNullTerminatedString(0, 0)); - assertEquals("A", reader.getNullTerminatedString(0, 1)); - assertEquals("AB", reader.getNullTerminatedString(0, 2)); - assertEquals("ABC", reader.getNullTerminatedString(0, 3)); - assertEquals("ABCD", reader.getNullTerminatedString(0, 4)); - assertEquals("ABCD", reader.getNullTerminatedString(0, 5)); - assertEquals("ABCD", reader.getNullTerminatedString(0, 6)); + assertEquals("", reader.getNullTerminatedString(0, 0, Charsets.UTF_8)); + assertEquals("A", reader.getNullTerminatedString(0, 1, Charsets.UTF_8)); + assertEquals("AB", reader.getNullTerminatedString(0, 2, Charsets.UTF_8)); + assertEquals("ABC", reader.getNullTerminatedString(0, 3, Charsets.UTF_8)); + assertEquals("ABCD", reader.getNullTerminatedString(0, 4, Charsets.UTF_8)); + assertEquals("ABCD", reader.getNullTerminatedString(0, 5, Charsets.UTF_8)); + assertEquals("ABCD", reader.getNullTerminatedString(0, 6, Charsets.UTF_8)); - assertEquals("BCD", reader.getNullTerminatedString(1, 3)); - assertEquals("BCD", reader.getNullTerminatedString(1, 4)); - assertEquals("BCD", reader.getNullTerminatedString(1, 5)); + assertEquals("BCD", reader.getNullTerminatedString(1, 3, Charsets.UTF_8)); + assertEquals("BCD", reader.getNullTerminatedString(1, 4, Charsets.UTF_8)); + assertEquals("BCD", reader.getNullTerminatedString(1, 5, Charsets.UTF_8)); - assertEquals("", reader.getNullTerminatedString(4, 3)); + assertEquals("", reader.getNullTerminatedString(4, 3, Charsets.UTF_8)); } @Test @@ -267,19 +267,19 @@ public abstract class RandomAccessTestBase byte[] bytes = new byte[]{0x41, 0x42, 0x43, 0x44, 0x00, 0x45, 0x46, 0x47}; RandomAccessReader reader = createReader(bytes); - assertEquals("", reader.getString(0, 0)); - assertEquals("A", reader.getString(0, 1)); - assertEquals("AB", reader.getString(0, 2)); - assertEquals("ABC", reader.getString(0, 3)); - assertEquals("ABCD", reader.getString(0, 4)); - assertEquals("ABCD\0", reader.getString(0, 5)); - assertEquals("ABCD\0E", reader.getString(0, 6)); + assertEquals("", reader.getString(0, 0, Charsets.UTF_8)); + assertEquals("A", reader.getString(0, 1, Charsets.UTF_8)); + assertEquals("AB", reader.getString(0, 2, Charsets.UTF_8)); + assertEquals("ABC", reader.getString(0, 3, Charsets.UTF_8)); + assertEquals("ABCD", reader.getString(0, 4, Charsets.UTF_8)); + assertEquals("ABCD\0", reader.getString(0, 5, Charsets.UTF_8)); + assertEquals("ABCD\0E", reader.getString(0, 6, Charsets.UTF_8)); - assertEquals("BCD", reader.getString(1, 3)); - assertEquals("BCD\0", reader.getString(1, 4)); - assertEquals("BCD\0E", reader.getString(1, 5)); + assertEquals("BCD", reader.getString(1, 3, Charsets.UTF_8)); + assertEquals("BCD\0", reader.getString(1, 4, Charsets.UTF_8)); + assertEquals("BCD\0E", reader.getString(1, 5, Charsets.UTF_8)); - assertEquals("\0EF", reader.getString(4, 3)); + assertEquals("\0EF", reader.getString(4, 3, Charsets.UTF_8)); } @Test diff --git a/Tests/com/drew/lang/RationalTest.java b/Tests/com/drew/lang/RationalTest.java index ad68290..c92da46 100644 --- a/Tests/com/drew/lang/RationalTest.java +++ b/Tests/com/drew/lang/RationalTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,20 @@ import static org.junit.Assert.assertTrue; */ public class RationalTest { + @Test + public void testCompare() throws Exception + { + Rational third1 = new Rational(1, 3); + Rational third2 = new Rational(2, 6); + assertEquals(0, third1.compareTo(third2)); + + Rational half = new Rational(1, 2); + assertEquals(-1, third1.compareTo(half)); + + Rational negForth = new Rational(-1, 4); + assertEquals(1, third1.compareTo(negForth)); + } + @Test public void testCreateRational() throws Exception { @@ -105,4 +119,126 @@ public class RationalTest assertEquals(0L, new Rational(0, 0).longValue()); assertTrue(new Rational(0, 0).isInteger()); } + + private static final int[] _primes = + { + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, + 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, + 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, + 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, + 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, + 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, + 967, 971, 977, 983, 991, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, 1091, 1093, 1097, 1103, + 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, 1279, + 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447, + 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, + 1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733, 1741, 1747, 1753, + 1759, 1777, 1783, 1787, 1789, 1801, 1811, 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933, + 1949, 1951, 1973, 1979, 1987, 1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, 2081, 2083, 2087, 2089, 2099, + 2111, 2113, 2129, 2131, 2137, 2141, 2143, 2153, 2161, 2179, 2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, 2269, 2273, 2281, + 2287, 2293, 2297, 2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399, 2411, 2417, 2423, 2437, + 2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531, 2539, 2543, 2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617, 2621, 2633, 2647, + 2657, 2659, 2663, 2671, 2677, 2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741, 2749, 2753, 2767, 2777, 2789, + 2791, 2797, 2801, 2803, 2819, 2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903, 2909, 2917, 2927, 2939, 2953, 2957, 2963, + 2969, 2971, 2999, 3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079, 3083, 3089, 3109, 3119, 3121, 3137, 3163, 3167, 3169, + 3181, 3187, 3191, 3203, 3209, 3217, 3221, 3229, 3251, 3253, 3257, 3259, 3271, 3299, 3301, 3307, 3313, 3319, 3323, 3329, 3331, 3343, + 3347, 3359, 3361, 3371, 3373, 3389, 3391, 3407, 3413, 3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, 3499, 3511, 3517, 3527, 3529, + 3533, 3539, 3541, 3547, 3557, 3559, 3571, 3581, 3583, 3593, 3607, 3613, 3617, 3623, 3631, 3637, 3643, 3659, 3671, 3673, 3677, 3691, + 3697, 3701, 3709, 3719, 3727, 3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797, 3803, 3821, 3823, 3833, 3847, 3851, 3853, 3863, 3877, + 3881, 3889, 3907, 3911, 3917, 3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003, 4007, 4013, 4019, 4021, 4027, 4049, 4051, + 4057, 4073, 4079, 4091, 4093, 4099, 4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, 4201, 4211, 4217, 4219, 4229, 4231, 4241, + 4243, 4253, 4259, 4261, 4271, 4273, 4283, 4289, 4297, 4327, 4337, 4339, 4349, 4357, 4363, 4373, 4391, 4397, 4409, 4421, 4423, 4441, + 4447, 4451, 4457, 4463, 4481, 4483, 4493, 4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583, 4591, 4597, 4603, 4621, 4637, + 4639, 4643, 4649, 4651, 4657, 4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751, 4759, 4783, 4787, 4789, 4793, 4799, 4801, + 4813, 4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937, 4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999, + 5003, 5009, 5011, 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087, 5099, 5101, 5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179, 5189, + 5197, 5209, 5227, 5231, 5233, 5237, 5261, 5273, 5279, 5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351, 5381, 5387, 5393, 5399, 5407, + 5413, 5417, 5419, 5431, 5437, 5441, 5443, 5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519, 5521, 5527, 5531, 5557, 5563, 5569, + 5573, 5581, 5591, 5623, 5639, 5641, 5647, 5651, 5653, 5657, 5659, 5669, 5683, 5689, 5693, 5701, 5711, 5717, 5737, 5741, 5743, 5749, + 5779, 5783, 5791, 5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869, 5879, 5881, 5897, 5903, 5923, 5927, + 5939, 5953, 5981, 5987, 6007, 6011, 6029, 6037, 6043, 6047, 6053, 6067, 6073, 6079, 6089, 6091, 6101, 6113, 6121, 6131, 6133, 6143, + 6151, 6163, 6173, 6197, 6199, 6203, 6211, 6217, 6221, 6229, 6247, 6257, 6263, 6269, 6271, 6277, 6287, 6299, 6301, 6311, 6317, 6323, + 6329, 6337, 6343, 6353, 6359, 6361, 6367, 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469, 6473, 6481, 6491, 6521, 6529, 6547, + 6551, 6553, 6563, 6569, 6571, 6577, 6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673, 6679, 6689, 6691, 6701, 6703, 6709, 6719, + 6733, 6737, 6761, 6763, 6779, 6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833, 6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907, 6911, + 6917, 6947, 6949, 6959, 6961, 6967, 6971, 6977, 6983, 6991, 6997, 7001, 7013, 7019, 7027, 7039, 7043, 7057, 7069, 7079, 7103, 7109, + 7121, 7127, 7129, 7151, 7159, 7177, 7187, 7193, 7207, 7211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, 7283, 7297, 7307, 7309, 7321, + 7331, 7333, 7349, 7351, 7369, 7393, 7411, 7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, 7489, 7499, 7507, 7517, 7523, 7529, 7537, + 7541, 7547, 7549, 7559, 7561, 7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643, 7649, 7669, 7673, 7681, 7687, 7691, 7699, + 7703, 7717, 7723, 7727, 7741, 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907, + 7919, 7927, 7933, 7937, 7949, 7951, 7963, 7993, 8009, 8011, 8017, 8039, 8053, 8059, 8069, 8081, 8087, 8089, 8093, 8101, 8111, 8117, + 8123, 8147, 8161, 8167, 8171, 8179, 8191, 8209, 8219, 8221, 8231, 8233, 8237, 8243, 8263, 8269, 8273, 8287, 8291, 8293, 8297, 8311, + 8317, 8329, 8353, 8363, 8369, 8377, 8387, 8389, 8419, 8423, 8429, 8431, 8443, 8447, 8461, 8467, 8501, 8513, 8521, 8527, 8537, 8539, + 8543, 8563, 8573, 8581, 8597, 8599, 8609, 8623, 8627, 8629, 8641, 8647, 8663, 8669, 8677, 8681, 8689, 8693, 8699, 8707, 8713, 8719, + 8731, 8737, 8741, 8747, 8753, 8761, 8779, 8783, 8803, 8807, 8819, 8821, 8831, 8837, 8839, 8849, 8861, 8863, 8867, 8887, 8893, 8923, + 8929, 8933, 8941, 8951, 8963, 8969, 8971, 8999, 9001, 9007, 9011, 9013, 9029, 9041, 9043, 9049, 9059, 9067, 9091, 9103, 9109, 9127, + 9133, 9137, 9151, 9157, 9161, 9173, 9181, 9187, 9199, 9203, 9209, 9221, 9227, 9239, 9241, 9257, 9277, 9281, 9283, 9293, 9311, 9319, + 9323, 9337, 9341, 9343, 9349, 9371, 9377, 9391, 9397, 9403, 9413, 9419, 9421, 9431, 9433, 9437, 9439, 9461, 9463, 9467, 9473, 9479, + 9491, 9497, 9511, 9521, 9533, 9539, 9547, 9551, 9587, 9601, 9613, 9619, 9623, 9629, 9631, 9643, 9649, 9661, 9677, 9679, 9689, 9697, + 9719, 9721, 9733, 9739, 9743, 9749, 9767, 9769, 9781, 9787, 9791, 9803, 9811, 9817, 9829, 9833, 9839, 9851, 9857, 9859, 9871, 9883, + 9887, 9901, 9907, 9923, 9929, 9931, 9941, 9949, 9967, 9973 + }; + + @Test + public void simplifiedInstances() + { + Rational simple = new Rational(1, 2); + + for (int prime : _primes) + { + Rational complex = new Rational(prime, 2 * prime); + Rational actualSimple = complex.getSimplifiedInstance(); + + assertTrue(simple.equalsExact(actualSimple)); + assertEquals(actualSimple.doubleValue(), complex.doubleValue(), 0.0001); + } + + simple = new Rational(2, 1); + + for (int prime : _primes) + { + Rational complex = new Rational(2 * prime, prime); + Rational actualSimple = complex.getSimplifiedInstance(); + + assertTrue(simple.equalsExact(actualSimple)); + assertEquals(actualSimple.doubleValue(), complex.doubleValue(), 0.0001); + } + + simple = new Rational(-1, 2); + + for (int prime : _primes) + { + Rational complex = new Rational(-prime, 2 * prime); + Rational actualSimple = complex.getSimplifiedInstance(); + + assertTrue(simple.equalsExact(actualSimple)); + assertEquals(actualSimple.doubleValue(), complex.doubleValue(), 0.0001); + } + + simple = new Rational(1, -2); + + for (int prime : _primes) + { + Rational complex = new Rational(prime, -2 * prime); + Rational actualSimple = complex.getSimplifiedInstance(); + + assertTrue(simple.equalsExact(actualSimple)); + assertEquals(actualSimple.doubleValue(), complex.doubleValue(), 0.0001); + } + + simple = new Rational(-1, -2); + + for (int prime : _primes) + { + Rational complex = new Rational(-prime, -2 * prime); + Rational actualSimple = complex.getSimplifiedInstance(); + + assertTrue(simple.equalsExact(actualSimple)); + assertEquals(actualSimple.doubleValue(), complex.doubleValue(), 0.0001); + } + + assertEquals(new Rational(-32768, 65535), new Rational(-32768, 65535).getSimplifiedInstance()); + assertEquals(new Rational(-32768, 32767), new Rational(-32768, 32767).getSimplifiedInstance()); + } + } diff --git a/Tests/com/drew/lang/SequentialAccessTestBase.java b/Tests/com/drew/lang/SequentialAccessTestBase.java index ba37e32..57e4b5e 100644 --- a/Tests/com/drew/lang/SequentialAccessTestBase.java +++ b/Tests/com/drew/lang/SequentialAccessTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -231,13 +231,13 @@ public abstract class SequentialAccessTestBase // Test max length for (int i = 0; i < bytes.length; i++) { - assertEquals("ABCDEFG".substring(0, i), createReader(bytes).getNullTerminatedString(i)); + assertEquals("ABCDEFG".substring(0, i), createReader(bytes).getNullTerminatedString(i, Charsets.UTF_8)); } - assertEquals("", createReader(new byte[]{0}).getNullTerminatedString(10)); - assertEquals("A", createReader(new byte[]{0x41, 0}).getNullTerminatedString(10)); - assertEquals("AB", createReader(new byte[]{0x41, 0x42, 0}).getNullTerminatedString(10)); - assertEquals("AB", createReader(new byte[]{0x41, 0x42, 0, 0x43}).getNullTerminatedString(10)); + assertEquals("", createReader(new byte[]{0}).getNullTerminatedString(10, Charsets.UTF_8)); + assertEquals("A", createReader(new byte[]{0x41, 0}).getNullTerminatedString(10, Charsets.UTF_8)); + assertEquals("AB", createReader(new byte[]{0x41, 0x42, 0}).getNullTerminatedString(10, Charsets.UTF_8)); + assertEquals("AB", createReader(new byte[]{0x41, 0x42, 0, 0x43}).getNullTerminatedString(10, Charsets.UTF_8)); } @Test diff --git a/Tests/com/drew/lang/SequentialByteArrayReaderTest.java b/Tests/com/drew/lang/SequentialByteArrayReaderTest.java index 66b8a2c..44ab504 100644 --- a/Tests/com/drew/lang/SequentialByteArrayReaderTest.java +++ b/Tests/com/drew/lang/SequentialByteArrayReaderTest.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.lang; import org.junit.Test; diff --git a/Tests/com/drew/lang/StreamReaderTest.java b/Tests/com/drew/lang/StreamReaderTest.java index 52e18e5..537a888 100644 --- a/Tests/com/drew/lang/StreamReaderTest.java +++ b/Tests/com/drew/lang/StreamReaderTest.java @@ -1,3 +1,23 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.lang; import org.junit.Test; diff --git a/Tests/com/drew/lang/StringUtilTest.java b/Tests/com/drew/lang/StringUtilTest.java index 3bd3870..bd91cc4 100644 --- a/Tests/com/drew/lang/StringUtilTest.java +++ b/Tests/com/drew/lang/StringUtilTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/metadata/AgeTest.java b/Tests/com/drew/metadata/AgeTest.java index 22fcb7c..7cd49c1 100644 --- a/Tests/com/drew/metadata/AgeTest.java +++ b/Tests/com/drew/metadata/AgeTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/metadata/DirectoryTest.java b/Tests/com/drew/metadata/DirectoryTest.java index a4d35b8..b638dc0 100644 --- a/Tests/com/drew/metadata/DirectoryTest.java +++ b/Tests/com/drew/metadata/DirectoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,9 @@ import com.drew.metadata.exif.ExifSubIFDDirectory; import org.junit.Before; import org.junit.Test; +import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.TimeZone; import static org.junit.Assert.*; @@ -105,19 +107,88 @@ public class DirectoryTest @Test public void testSetStringAndGetDate() throws Exception { - String date1 = "2002:01:30 24:59:59"; - String date2 = "2002:01:30 24:59"; - String date3 = "2002-01-30 24:59:59"; - String date4 = "2002-01-30 24:59"; + String date1 = "2002:01:30 23:59:59"; + String date2 = "2002:01:30 23:59"; + String date3 = "2002-01-30 23:59:59"; + String date4 = "2002-01-30 23:59"; + String date5 = "2002-01-30T23:59:59.099-08:00"; + String date6 = "2002-01-30T23:59:59.099"; + String date7 = "2002-01-30T23:59:59-08:00"; + String date8 = "2002-01-30T23:59:59"; + String date9 = "2002-01-30T23:59-08:00"; + String date10 = "2002-01-30T23:59"; + String date11 = "2002-01-30"; + String date12 = "2002-01"; + String date13 = "2002"; _directory.setString(1, date1); _directory.setString(2, date2); _directory.setString(3, date3); _directory.setString(4, date4); + _directory.setString(5, date5); + _directory.setString(6, date6); + _directory.setString(7, date7); + _directory.setString(8, date8); + _directory.setString(9, date9); + _directory.setString(10, date10); + _directory.setString(11, date11); + _directory.setString(12, date12); + _directory.setString(13, date13); assertEquals(date1, _directory.getString(1)); - assertEquals(new GregorianCalendar(2002, GregorianCalendar.JANUARY, 30, 24, 59, 59).getTime(), _directory.getDate(1)); - assertEquals(new GregorianCalendar(2002, GregorianCalendar.JANUARY, 30, 24, 59, 0).getTime(), _directory.getDate(2)); - assertEquals(new GregorianCalendar(2002, GregorianCalendar.JANUARY, 30, 24, 59, 59).getTime(), _directory.getDate(3)); - assertEquals(new GregorianCalendar(2002, GregorianCalendar.JANUARY, 30, 24, 59, 0).getTime(), _directory.getDate(4)); + + // Don't use default timezone + TimeZone gmt = TimeZone.getTimeZone("GMT"); + GregorianCalendar gc = new GregorianCalendar(gmt); + // clear millis to 0 or test will fail + gc.setTimeInMillis(0); + gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 59); + assertEquals(gc.getTime(), _directory.getDate(1, null)); + + gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 0); + assertEquals(gc.getTime(), _directory.getDate(2, null)); + + // Use specific timezone + TimeZone pst = TimeZone.getTimeZone("PST"); + gc = new GregorianCalendar(pst); + gc.setTimeInMillis(0); + + gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 59); + assertEquals(gc.getTime(), _directory.getDate(3, pst)); + + gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 0); + assertEquals(gc.getTime(), _directory.getDate(4, pst)); + + gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 59); + gc.set(Calendar.MILLISECOND, 99); + assertEquals(gc.getTime(), _directory.getDate(5, null)); + assertEquals(gc.getTime(), _directory.getDate(5, gmt)); + assertEquals(gc.getTime(), _directory.getDate(6, pst)); + + assertEquals(gc.getTime(), _directory.getDate(5, "011", null)); + assertEquals(gc.getTime(), _directory.getDate(6, "011", pst)); + assertEquals(gc.getTime(), _directory.getDate(7, "099", null)); + assertEquals(gc.getTime(), _directory.getDate(8, "099", pst)); + + gc.set(Calendar.MILLISECOND, 0); + assertEquals(gc.getTime(), _directory.getDate(7, null)); + assertEquals(gc.getTime(), _directory.getDate(7, gmt)); + assertEquals(gc.getTime(), _directory.getDate(8, pst)); + + gc.set(2002, GregorianCalendar.JANUARY, 30, 23, 59, 0); + assertEquals(gc.getTime(), _directory.getDate(9, null)); + assertEquals(gc.getTime(), _directory.getDate(9, gmt)); + assertEquals(gc.getTime(), _directory.getDate(10, pst)); + + gc = new GregorianCalendar(gmt); + gc.setTimeInMillis(0); + + gc.set(2002, GregorianCalendar.JANUARY, 30, 0, 0, 0); + assertEquals(gc.getTime(), _directory.getDate(11, null)); + + gc.set(2002, GregorianCalendar.JANUARY, 1, 0, 0, 0); + assertEquals(gc.getTime(), _directory.getDate(12, null)); + + gc.set(2002, GregorianCalendar.JANUARY, 1, 0, 0, 0); + assertEquals(gc.getTime(), _directory.getDate(13, null)); } @Test diff --git a/Tests/com/drew/metadata/MetadataTest.java b/Tests/com/drew/metadata/MetadataTest.java index 0cc98d7..2535641 100644 --- a/Tests/com/drew/metadata/MetadataTest.java +++ b/Tests/com/drew/metadata/MetadataTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,13 @@ package com.drew.metadata; import com.drew.metadata.exif.ExifIFD0Directory; import com.drew.metadata.exif.ExifSubIFDDirectory; -import com.drew.metadata.iptc.IptcDirectory; +import com.drew.metadata.exif.ExifThumbnailDirectory; import org.junit.Test; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + import static org.junit.Assert.*; /** @@ -34,59 +38,79 @@ import static org.junit.Assert.*; */ public class MetadataTest { - @Test public void testGetDirectoryWhenNotExists() + @Test + public void testGetDirectoryWhenNotExists() { - assertNull(new Metadata().getDirectory(ExifSubIFDDirectory.class)); + assertNull(new Metadata().getFirstDirectoryOfType(ExifSubIFDDirectory.class)); } - @Test public void testGetOrCreateDirectoryWhenNotExists() + @Test + public void testHasErrors() throws Exception { - assertNotNull(new Metadata().getOrCreateDirectory(ExifSubIFDDirectory.class)); - } + ExifSubIFDDirectory directory = new ExifSubIFDDirectory(); + directory.addError("Test Error 1"); - @Test public void testGetDirectoryReturnsSameInstance() - { Metadata metadata = new Metadata(); - Directory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class); - assertSame(directory, metadata.getDirectory(ExifSubIFDDirectory.class)); - } + assertFalse(metadata.hasErrors()); - @Test public void testGetOrCreateDirectoryReturnsSameInstance() - { - Metadata metadata = new Metadata(); - Directory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class); - assertSame(directory, metadata.getOrCreateDirectory(ExifSubIFDDirectory.class)); - assertNotSame(directory, metadata.getOrCreateDirectory(IptcDirectory.class)); + metadata.addDirectory(directory); + assertTrue(metadata.hasErrors()); } @Test - public void testHasErrors() throws Exception + public void testToString() { Metadata metadata = new Metadata(); - assertFalse(metadata.hasErrors()); - final ExifSubIFDDirectory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class); - directory.addError("Test Error 1"); - assertTrue(metadata.hasErrors()); + assertEquals("Metadata (0 directories)", metadata.toString()); + + metadata.addDirectory(new ExifIFD0Directory()); + assertEquals("Metadata (1 directory)", metadata.toString()); + + metadata.addDirectory(new ExifSubIFDDirectory()); + assertEquals("Metadata (2 directories)", metadata.toString()); } @Test - public void testGetErrors() throws Exception + public void testOrderOfSameType() { Metadata metadata = new Metadata(); - assertFalse(metadata.hasErrors()); - final ExifSubIFDDirectory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class); - directory.addError("Test Error 1"); - assertTrue(metadata.hasErrors()); + Directory directory2 = new ExifSubIFDDirectory(); + Directory directory3 = new ExifSubIFDDirectory(); + Directory directory1 = new ExifSubIFDDirectory(); + + metadata.addDirectory(directory1); + metadata.addDirectory(directory2); + metadata.addDirectory(directory3); + + Collection<ExifSubIFDDirectory> directories = metadata.getDirectoriesOfType(ExifSubIFDDirectory.class); + + assertNotNull(directories); + assertEquals(3, directories.size()); + assertSame(directory1, directories.toArray()[0]); + assertSame(directory2, directories.toArray()[1]); + assertSame(directory3, directories.toArray()[2]); } @Test - public void testToString() + public void testOrderOfDifferentTypes() { Metadata metadata = new Metadata(); - assertEquals("Metadata (0 directories)", metadata.toString()); - metadata.getOrCreateDirectory(ExifIFD0Directory.class); - assertEquals("Metadata (1 directory)", metadata.toString()); - metadata.getOrCreateDirectory(ExifSubIFDDirectory.class); - assertEquals("Metadata (2 directories)", metadata.toString()); + Directory directory1 = new ExifSubIFDDirectory(); + Directory directory2 = new ExifThumbnailDirectory(); + Directory directory3 = new ExifIFD0Directory(); + + metadata.addDirectory(directory1); + metadata.addDirectory(directory2); + metadata.addDirectory(directory3); + + List<Directory> directories = new ArrayList<Directory>(); + for (Directory directory : metadata.getDirectories()) { + directories.add(directory); + } + + assertEquals(3, directories.size()); + assertSame(directory1, directories.toArray()[0]); + assertSame(directory2, directories.toArray()[1]); + assertSame(directory3, directories.toArray()[2]); } } diff --git a/Tests/com/drew/metadata/MockDirectory.java b/Tests/com/drew/metadata/MockDirectory.java index eb88b70..61541dd 100644 --- a/Tests/com/drew/metadata/MockDirectory.java +++ b/Tests/com/drew/metadata/MockDirectory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/metadata/adobe/AdobeJpegReaderTest.java b/Tests/com/drew/metadata/adobe/AdobeJpegReaderTest.java index 58cd670..d3b1dfe 100644 --- a/Tests/com/drew/metadata/adobe/AdobeJpegReaderTest.java +++ b/Tests/com/drew/metadata/adobe/AdobeJpegReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ public class AdobeJpegReaderTest Metadata metadata = new Metadata(); new AdobeJpegReader().extract(new SequentialByteArrayReader(FileUtil.readBytes(filePath)), metadata); - AdobeJpegDirectory directory = metadata.getDirectory(AdobeJpegDirectory.class); + AdobeJpegDirectory directory = metadata.getFirstDirectoryOfType(AdobeJpegDirectory.class); assertNotNull(directory); return directory; } diff --git a/Tests/com/drew/metadata/bmp/BmpReaderTest.java b/Tests/com/drew/metadata/bmp/BmpReaderTest.java index 5478045..10fc40e 100644 --- a/Tests/com/drew/metadata/bmp/BmpReaderTest.java +++ b/Tests/com/drew/metadata/bmp/BmpReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ public class BmpReaderTest new BmpReader().extract(new StreamReader(stream), metadata); stream.close(); - BmpHeaderDirectory directory = metadata.getDirectory(BmpHeaderDirectory.class); + BmpHeaderDirectory directory = metadata.getFirstDirectoryOfType(BmpHeaderDirectory.class); assertNotNull(directory); return directory; } diff --git a/Tests/com/drew/metadata/exif/CanonMakernoteDescriptorTest.java b/Tests/com/drew/metadata/exif/CanonMakernoteDescriptorTest.java index f447ddb..c81fb57 100644 --- a/Tests/com/drew/metadata/exif/CanonMakernoteDescriptorTest.java +++ b/Tests/com/drew/metadata/exif/CanonMakernoteDescriptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import com.drew.metadata.exif.makernotes.CanonMakernoteDescriptor; import com.drew.metadata.exif.makernotes.CanonMakernoteDirectory; import org.junit.Test; +import static com.drew.metadata.exif.makernotes.CanonMakernoteDirectory.*; import static org.junit.Assert.assertEquals; /** @@ -39,34 +40,34 @@ public class CanonMakernoteDescriptorTest // set and check values - directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0xFFC0); - assertEquals("-2.0 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS)); + directory.setInt(FocalLength.TAG_FLASH_BIAS, 0xFFC0); + assertEquals("-2.0 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS)); - directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0xffd4); - assertEquals("-1.375 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS)); + directory.setInt(FocalLength.TAG_FLASH_BIAS, 0xffd4); + assertEquals("-1.375 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS)); - directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0000); - assertEquals("0.0 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS)); + directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0000); + assertEquals("0.0 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS)); - directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x000c); - assertEquals("0.375 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS)); + directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x000c); + assertEquals("0.375 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS)); - directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0010); - assertEquals("0.5 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS)); + directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0010); + assertEquals("0.5 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS)); - directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0014); - assertEquals("0.625 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS)); + directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0014); + assertEquals("0.625 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS)); - directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0020); - assertEquals("1.0 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS)); + directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0020); + assertEquals("1.0 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS)); - directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0030); - assertEquals("1.5 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS)); + directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0030); + assertEquals("1.5 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS)); - directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0034); - assertEquals("1.625 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS)); + directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0034); + assertEquals("1.625 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS)); - directory.setInt(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS, 0x0040); - assertEquals("2.0 EV", descriptor.getDescription(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS)); + directory.setInt(FocalLength.TAG_FLASH_BIAS, 0x0040); + assertEquals("2.0 EV", descriptor.getDescription(FocalLength.TAG_FLASH_BIAS)); } } diff --git a/Tests/com/drew/metadata/exif/ExifDirectoryTest.java b/Tests/com/drew/metadata/exif/ExifDirectoryTest.java index f70fbea..5c44662 100644 --- a/Tests/com/drew/metadata/exif/ExifDirectoryTest.java +++ b/Tests/com/drew/metadata/exif/ExifDirectoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,23 +21,24 @@ package com.drew.metadata.exif; import com.drew.imaging.jpeg.JpegProcessingException; -import com.drew.imaging.jpeg.JpegSegmentReader; -import com.drew.lang.SequentialByteArrayReader; +import com.drew.lang.GeoLocation; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import com.drew.metadata.MetadataException; import org.junit.Test; -import java.io.File; import java.io.IOException; +import java.util.TimeZone; import static org.junit.Assert.*; /** - * Unit tests for {@link ExifSubIFDDirectory}, {@link ExifIFD0Directory}, {@link ExifThumbnailDirectory}. + * Unit tests for {@link ExifSubIFDDirectory}, {@link ExifIFD0Directory}, {@link ExifThumbnailDirectory} and + * {@link GpsDirectory}. * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("ConstantConditions") public class ExifDirectoryTest { @Test @@ -46,73 +47,88 @@ public class ExifDirectoryTest Directory subIFDDirectory = new ExifSubIFDDirectory(); Directory ifd0Directory = new ExifIFD0Directory(); Directory thumbDirectory = new ExifThumbnailDirectory(); + Directory gpsDirectory = new GpsDirectory(); assertFalse(subIFDDirectory.hasErrors()); assertFalse(ifd0Directory.hasErrors()); assertFalse(thumbDirectory.hasErrors()); + assertFalse(gpsDirectory.hasErrors()); assertEquals("Exif IFD0", ifd0Directory.getName()); assertEquals("Exif SubIFD", subIFDDirectory.getName()); assertEquals("Exif Thumbnail", thumbDirectory.getName()); + assertEquals("GPS", gpsDirectory.getName()); } @Test - public void testGetThumbnailData() throws Exception + public void testDateTime() throws JpegProcessingException, IOException, MetadataException { - ExifThumbnailDirectory directory = ExifReaderTest.processBytes("Tests/Data/withExif.jpg.app1", ExifThumbnailDirectory.class); - - byte[] thumbData = directory.getThumbnailData(); - assertNotNull(thumbData); - try { - // attempt to read the thumbnail -- it should be a legal Jpeg file - JpegSegmentReader.readSegments(new SequentialByteArrayReader(thumbData), null); - } catch (JpegProcessingException e) { - fail("Unable to construct JpegSegmentReader from thumbnail data"); - } - } + Metadata metadata = ExifReaderTest.processBytes("Tests/Data/nikonMakernoteType2a.jpg.app1"); - @Test - public void testWriteThumbnail() throws Exception - { - ExifThumbnailDirectory directory = ExifReaderTest.processBytes("Tests/Data/manuallyAddedThumbnail.jpg.app1", ExifThumbnailDirectory.class); - - assertTrue(directory.hasThumbnailData()); - - File thumbnailFile = File.createTempFile("thumbnail", ".jpg"); - try { - directory.writeThumbnail(thumbnailFile.getAbsolutePath()); - File file = new File(thumbnailFile.getAbsolutePath()); - assertEquals(2970, file.length()); - assertTrue(file.exists()); - } finally { - if (!thumbnailFile.delete()) - fail("Unable to delete temp thumbnail file."); - } - } + ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + ExifSubIFDDirectory exifSubIFDDirectory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); -// @Test -// public void testContainsThumbnail() -// { -// ExifSubIFDDirectory exifDirectory = new ExifSubIFDDirectory(); -// -// assertTrue(!exifDirectory.hasThumbnailData()); -// -// exifDirectory.setObject(ExifSubIFDDirectory.TAG_THUMBNAIL_DATA, "foo"); -// -// assertTrue(exifDirectory.hasThumbnailData()); -// } + assertNotNull(exifIFD0Directory); + assertNotNull(exifSubIFDDirectory); + + assertEquals("2003:10:15 10:37:08", exifIFD0Directory.getString(ExifIFD0Directory.TAG_DATETIME)); + assertEquals("80", exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME)); + assertEquals("2003:10:15 10:37:08", exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL)); + assertEquals("80", exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME_ORIGINAL)); + assertEquals("2003:10:15 10:37:08", exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED)); + assertEquals("80", exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME_DIGITIZED)); + + assertEquals(1066214228800L, exifIFD0Directory.getDate( + ExifIFD0Directory.TAG_DATETIME, + exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME), + null + ).getTime()); + assertEquals(1066210628800L, exifIFD0Directory.getDate( + ExifIFD0Directory.TAG_DATETIME, + exifSubIFDDirectory.getString(ExifSubIFDDirectory.TAG_SUBSECOND_TIME), + TimeZone.getTimeZone("GMT+0100") + ).getTime()); + assertEquals(1066214228800L, exifSubIFDDirectory.getDateOriginal().getTime()); + assertEquals(1066210628800L, exifSubIFDDirectory.getDateOriginal(TimeZone.getTimeZone("GMT+0100")).getTime()); + assertEquals(1066214228800L, exifSubIFDDirectory.getDateDigitized().getTime()); + assertEquals(1066210628800L, exifSubIFDDirectory.getDateDigitized(TimeZone.getTimeZone("GMT+0100")).getTime()); + } @Test public void testResolution() throws JpegProcessingException, IOException, MetadataException { Metadata metadata = ExifReaderTest.processBytes("Tests/Data/withUncompressedRGBThumbnail.jpg.app1"); - ExifThumbnailDirectory thumbnailDirectory = metadata.getDirectory(ExifThumbnailDirectory.class); + ExifThumbnailDirectory thumbnailDirectory = metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class); assertNotNull(thumbnailDirectory); assertEquals(72, thumbnailDirectory.getInt(ExifThumbnailDirectory.TAG_X_RESOLUTION)); - ExifIFD0Directory exifIFD0Directory = metadata.getDirectory(ExifIFD0Directory.class); + ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); assertNotNull(exifIFD0Directory); assertEquals(216, exifIFD0Directory.getInt(ExifIFD0Directory.TAG_X_RESOLUTION)); } + + @Test + public void testGeoLocation() throws IOException, MetadataException + { + Metadata metadata = ExifReaderTest.processBytes("Tests/Data/withExifAndIptc.jpg.app1.0"); + + GpsDirectory gpsDirectory = metadata.getFirstDirectoryOfType(GpsDirectory.class); + assertNotNull(gpsDirectory); + GeoLocation geoLocation = gpsDirectory.getGeoLocation(); + assertEquals(54.989666666666665, geoLocation.getLatitude(), 0.001); + assertEquals(-1.9141666666666666, geoLocation.getLongitude(), 0.001); + } + + @Test + public void testGpsDate() throws IOException, MetadataException + { + Metadata metadata = ExifReaderTest.processBytes("Tests/Data/withPanasonicFaces.jpg.app1"); + + GpsDirectory gpsDirectory = metadata.getFirstDirectoryOfType(GpsDirectory.class); + assertNotNull(gpsDirectory); + assertEquals("2010:06:24", gpsDirectory.getString(GpsDirectory.TAG_DATE_STAMP)); + assertEquals("10/1 17/1 21/1", gpsDirectory.getString(GpsDirectory.TAG_TIME_STAMP)); + assertEquals(1277374641000L, gpsDirectory.getGpsDate().getTime()); + } } diff --git a/Tests/com/drew/metadata/exif/ExifIFD0DescriptorTest.java b/Tests/com/drew/metadata/exif/ExifIFD0DescriptorTest.java index 1a9101e..8b1ca9a 100644 --- a/Tests/com/drew/metadata/exif/ExifIFD0DescriptorTest.java +++ b/Tests/com/drew/metadata/exif/ExifIFD0DescriptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ package com.drew.metadata.exif; import com.drew.lang.Rational; import org.junit.Test; +import static com.drew.metadata.exif.ExifIFD0Directory.*; import static org.junit.Assert.assertEquals; /** @@ -37,22 +38,22 @@ public class ExifIFD0DescriptorTest public void testXResolutionDescription() throws Exception { ExifIFD0Directory directory = new ExifIFD0Directory(); - directory.setRational(ExifIFD0Directory.TAG_X_RESOLUTION, new Rational(72, 1)); + directory.setRational(TAG_X_RESOLUTION, new Rational(72, 1)); // 2 is for 'Inch' - directory.setInt(ExifIFD0Directory.TAG_RESOLUTION_UNIT, 2); + directory.setInt(TAG_RESOLUTION_UNIT, 2); ExifIFD0Descriptor descriptor = new ExifIFD0Descriptor(directory); - assertEquals("72 dots per inch", descriptor.getDescription(ExifIFD0Directory.TAG_X_RESOLUTION)); + assertEquals("72 dots per inch", descriptor.getDescription(TAG_X_RESOLUTION)); } @Test public void testYResolutionDescription() throws Exception { ExifIFD0Directory directory = new ExifIFD0Directory(); - directory.setRational(ExifIFD0Directory.TAG_Y_RESOLUTION, new Rational(50, 1)); + directory.setRational(TAG_Y_RESOLUTION, new Rational(50, 1)); // 3 is for 'cm' - directory.setInt(ExifIFD0Directory.TAG_RESOLUTION_UNIT, 3); + directory.setInt(TAG_RESOLUTION_UNIT, 3); ExifIFD0Descriptor descriptor = new ExifIFD0Descriptor(directory); - assertEquals("50 dots per cm", descriptor.getDescription(ExifIFD0Directory.TAG_Y_RESOLUTION)); + assertEquals("50 dots per cm", descriptor.getDescription(TAG_Y_RESOLUTION)); } @Test @@ -60,17 +61,17 @@ public class ExifIFD0DescriptorTest { ExifIFD0Directory directory = ExifReaderTest.processBytes("Tests/Data/windowsXpFields.jpg.app1", ExifIFD0Directory.class); - assertEquals("Testing artist\0", directory.getString(ExifIFD0Directory.TAG_WIN_AUTHOR, "UTF-16LE")); - assertEquals("Testing comments\0", directory.getString(ExifIFD0Directory.TAG_WIN_COMMENT, "UTF-16LE")); - assertEquals("Testing keywords\0", directory.getString(ExifIFD0Directory.TAG_WIN_KEYWORDS, "UTF-16LE")); - assertEquals("Testing subject\0", directory.getString(ExifIFD0Directory.TAG_WIN_SUBJECT, "UTF-16LE")); - assertEquals("Testing title\0", directory.getString(ExifIFD0Directory.TAG_WIN_TITLE, "UTF-16LE")); + assertEquals("Testing artist\0", directory.getString(TAG_WIN_AUTHOR, "UTF-16LE")); + assertEquals("Testing comments\0", directory.getString(TAG_WIN_COMMENT, "UTF-16LE")); + assertEquals("Testing keywords\0", directory.getString(TAG_WIN_KEYWORDS, "UTF-16LE")); + assertEquals("Testing subject\0", directory.getString(TAG_WIN_SUBJECT, "UTF-16LE")); + assertEquals("Testing title\0", directory.getString(TAG_WIN_TITLE, "UTF-16LE")); ExifIFD0Descriptor descriptor = new ExifIFD0Descriptor(directory); - assertEquals("Testing artist", descriptor.getDescription(ExifIFD0Directory.TAG_WIN_AUTHOR)); - assertEquals("Testing comments", descriptor.getDescription(ExifIFD0Directory.TAG_WIN_COMMENT)); - assertEquals("Testing keywords", descriptor.getDescription(ExifIFD0Directory.TAG_WIN_KEYWORDS)); - assertEquals("Testing subject", descriptor.getDescription(ExifIFD0Directory.TAG_WIN_SUBJECT)); - assertEquals("Testing title", descriptor.getDescription(ExifIFD0Directory.TAG_WIN_TITLE)); + assertEquals("Testing artist", descriptor.getDescription(TAG_WIN_AUTHOR)); + assertEquals("Testing comments", descriptor.getDescription(TAG_WIN_COMMENT)); + assertEquals("Testing keywords", descriptor.getDescription(TAG_WIN_KEYWORDS)); + assertEquals("Testing subject", descriptor.getDescription(TAG_WIN_SUBJECT)); + assertEquals("Testing title", descriptor.getDescription(TAG_WIN_TITLE)); } } diff --git a/Tests/com/drew/metadata/exif/ExifInteropDescriptorTest.java b/Tests/com/drew/metadata/exif/ExifInteropDescriptorTest.java index 797f8fd..8bb07fb 100644 --- a/Tests/com/drew/metadata/exif/ExifInteropDescriptorTest.java +++ b/Tests/com/drew/metadata/exif/ExifInteropDescriptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ package com.drew.metadata.exif; import org.junit.Test; +import static com.drew.metadata.exif.ExifInteropDirectory.*; import static org.junit.Assert.assertEquals; /** @@ -36,9 +37,9 @@ public class ExifInteropDescriptorTest public void testGetInteropVersionDescription() throws Exception { ExifInteropDirectory directory = new ExifInteropDirectory(); - directory.setIntArray(ExifInteropDirectory.TAG_INTEROP_VERSION, new int[]{0, 1, 0, 0}); + directory.setIntArray(TAG_INTEROP_VERSION, new int[]{0, 1, 0, 0}); ExifInteropDescriptor descriptor = new ExifInteropDescriptor(directory); - assertEquals("1.00", descriptor.getDescription(ExifInteropDirectory.TAG_INTEROP_VERSION)); + assertEquals("1.00", descriptor.getDescription(TAG_INTEROP_VERSION)); assertEquals("1.00", descriptor.getInteropVersionDescription()); } @@ -46,9 +47,9 @@ public class ExifInteropDescriptorTest public void testGetInteropIndexDescription() throws Exception { ExifInteropDirectory directory = new ExifInteropDirectory(); - directory.setString(ExifInteropDirectory.TAG_INTEROP_INDEX, "R98"); + directory.setString(TAG_INTEROP_INDEX, "R98"); ExifInteropDescriptor descriptor = new ExifInteropDescriptor(directory); - assertEquals("Recommended Exif Interoperability Rules (ExifR98)", descriptor.getDescription(ExifInteropDirectory.TAG_INTEROP_INDEX)); + assertEquals("Recommended Exif Interoperability Rules (ExifR98)", descriptor.getDescription(TAG_INTEROP_INDEX)); assertEquals("Recommended Exif Interoperability Rules (ExifR98)", descriptor.getInteropIndexDescription()); } } diff --git a/Tests/com/drew/metadata/exif/ExifReaderTest.java b/Tests/com/drew/metadata/exif/ExifReaderTest.java index 2a129dd..1bf4ceb 100644 --- a/Tests/com/drew/metadata/exif/ExifReaderTest.java +++ b/Tests/com/drew/metadata/exif/ExifReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ package com.drew.metadata.exif; import com.drew.imaging.jpeg.JpegSegmentType; +import com.drew.lang.ByteArrayReader; import com.drew.lang.Rational; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Directory; @@ -29,6 +30,7 @@ import com.drew.tools.FileUtil; import org.junit.Test; import java.io.IOException; +import java.util.ArrayList; import static org.junit.Assert.*; @@ -43,34 +45,25 @@ public class ExifReaderTest public static Metadata processBytes(@NotNull String filePath) throws IOException { Metadata metadata = new Metadata(); - new ExifReader().extract(FileUtil.readBytes(filePath), metadata, JpegSegmentType.APP1); + byte[] bytes = FileUtil.readBytes(filePath); + new ExifReader().extract(new ByteArrayReader(bytes), metadata, ExifReader.JPEG_SEGMENT_PREAMBLE.length(), null); return metadata; } @NotNull public static <T extends Directory> T processBytes(@NotNull String filePath, @NotNull Class<T> directoryClass) throws IOException { - T directory = processBytes(filePath).getDirectory(directoryClass); + T directory = processBytes(filePath).getFirstDirectoryOfType(directoryClass); assertNotNull(directory); return directory; } + @SuppressWarnings("ConstantConditions") @Test public void testExtractWithNullDataThrows() throws Exception { try{ - new ExifReader().extract(null, new Metadata(), JpegSegmentType.APP1); - fail("Exception expected"); - } catch (NullPointerException npe) { - // passed - } - } - - @Test - public void testExtractWithNullMetadataThrows() throws Exception - { - try{ - new ExifReader().extract(new byte[10], null, JpegSegmentType.APP1); + new ExifReader().readJpegSegments(null, new Metadata(), JpegSegmentType.APP1); fail("Exception expected"); } catch (NullPointerException npe) { // passed @@ -90,12 +83,15 @@ public class ExifReaderTest } @Test - public void testLoadJpegWithNoExifData() throws Exception + public void testReadJpegSegmentWithNoExifData() throws Exception { byte[] badExifData = new byte[]{ 1,2,3,4,5,6,7,8,9,10 }; Metadata metadata = new Metadata(); - new ExifReader().extract(badExifData, metadata, JpegSegmentType.APP1); + ArrayList<byte[]> segments = new ArrayList<byte[]>(); + segments.add(badExifData); + new ExifReader().readJpegSegments(segments, metadata, JpegSegmentType.APP1); assertEquals(0, metadata.getDirectoryCount()); + assertFalse(metadata.hasErrors()); } @Test @@ -156,21 +152,12 @@ public class ExifReaderTest } @Test - public void testThumbnailData() throws Exception - { - ExifThumbnailDirectory directory = ExifReaderTest.processBytes("Tests/Data/manuallyAddedThumbnail.jpg.app1", ExifThumbnailDirectory.class); - byte[] thumbnailData = directory.getThumbnailData(); - assertNotNull(thumbnailData); - assertEquals(2970, thumbnailData.length); - } - - @Test - public void testThumbnailCompression() throws Exception + public void testCompression() throws Exception { ExifThumbnailDirectory directory = ExifReaderTest.processBytes("Tests/Data/manuallyAddedThumbnail.jpg.app1", ExifThumbnailDirectory.class); // 6 means JPEG compression - assertEquals(6, directory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)); + assertEquals(6, directory.getInt(ExifThumbnailDirectory.TAG_COMPRESSION)); } @Test @@ -194,8 +181,8 @@ public class ExifReaderTest // These values used to be merged into a single directory, causing errors. // This unit test demonstrates correct behaviour. Metadata metadata = processBytes("Tests/Data/repeatedOrientationTagWithDifferentValues.jpg.app1"); - ExifIFD0Directory ifd0Directory = metadata.getDirectory(ExifIFD0Directory.class); - ExifThumbnailDirectory thumbnailDirectory = metadata.getDirectory(ExifThumbnailDirectory.class); + ExifIFD0Directory ifd0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + ExifThumbnailDirectory thumbnailDirectory = metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class); assertNotNull(ifd0Directory); assertNotNull(thumbnailDirectory); diff --git a/Tests/com/drew/metadata/exif/ExifSubIFDDescriptorTest.java b/Tests/com/drew/metadata/exif/ExifSubIFDDescriptorTest.java index fa9ed5f..d394eca 100644 --- a/Tests/com/drew/metadata/exif/ExifSubIFDDescriptorTest.java +++ b/Tests/com/drew/metadata/exif/ExifSubIFDDescriptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ package com.drew.metadata.exif; import org.junit.Test; +import static com.drew.metadata.exif.ExifSubIFDDirectory.*; import static org.junit.Assert.assertEquals; /** @@ -36,9 +37,9 @@ public class ExifSubIFDDescriptorTest { byte[] commentBytes = "\0\0\0\0\0\0\0\0This is a comment".getBytes(); ExifSubIFDDirectory directory = new ExifSubIFDDirectory(); - directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes); + directory.setByteArray(TAG_USER_COMMENT, commentBytes); ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory); - assertEquals("This is a comment", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)); + assertEquals("This is a comment", descriptor.getDescription(TAG_USER_COMMENT)); } @Test @@ -46,9 +47,9 @@ public class ExifSubIFDDescriptorTest { byte[] commentBytes = "ASCII\0\0This is a comment".getBytes(); ExifSubIFDDirectory directory = new ExifSubIFDDirectory(); - directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes); + directory.setByteArray(TAG_USER_COMMENT, commentBytes); ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory); - assertEquals("This is a comment", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)); + assertEquals("This is a comment", descriptor.getDescription(TAG_USER_COMMENT)); } @Test @@ -56,9 +57,9 @@ public class ExifSubIFDDescriptorTest { byte[] commentBytes = "ASCII\0\0\0 ".getBytes(); ExifSubIFDDirectory directory = new ExifSubIFDDirectory(); - directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes); + directory.setByteArray(TAG_USER_COMMENT, commentBytes); ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory); - assertEquals("", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)); + assertEquals("", descriptor.getDescription(TAG_USER_COMMENT)); } @Test @@ -67,9 +68,9 @@ public class ExifSubIFDDescriptorTest // the 10-byte encoding region is only partially full byte[] commentBytes = "ASCII\0\0\0".getBytes(); ExifSubIFDDirectory directory = new ExifSubIFDDirectory(); - directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes); + directory.setByteArray(TAG_USER_COMMENT, commentBytes); ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory); - assertEquals("ASCII", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)); + assertEquals("ASCII", descriptor.getDescription(TAG_USER_COMMENT)); } @Test @@ -78,9 +79,9 @@ public class ExifSubIFDDescriptorTest // fill the 10-byte encoding region byte[] commentBytes = "ASCII\0\0\0\0\0".getBytes(); ExifSubIFDDirectory directory = new ExifSubIFDDirectory(); - directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes); + directory.setByteArray(TAG_USER_COMMENT, commentBytes); ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory); - assertEquals("", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)); + assertEquals("", descriptor.getDescription(TAG_USER_COMMENT)); } @Test @@ -88,9 +89,9 @@ public class ExifSubIFDDescriptorTest { byte[] commentBytes = new byte[] { 85, 78, 73, 67, 79, 68, 69, 0, 84, 0, 104, 0, 105, 0, 115, 0, 32, 0, 109, 0, 97, 0, 114, 0, 109, 0, 111, 0, 116, 0, 32, 0, 105, 0, 115, 0, 32, 0, 103, 0, 101, 0, 116, 0, 116, 0, 105, 0, 110, 0, 103, 0, 32, 0, 99, 0, 108, 0, 111, 0, 115, 0, 101, 0, 46, 0, 46, 0, 46, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0, 32, 0 }; ExifSubIFDDirectory directory = new ExifSubIFDDirectory(); - directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes); + directory.setByteArray(TAG_USER_COMMENT, commentBytes); ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory); - assertEquals("This marmot is getting close...", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)); + assertEquals("This marmot is getting close...", descriptor.getDescription(TAG_USER_COMMENT)); } @Test @@ -98,8 +99,8 @@ public class ExifSubIFDDescriptorTest { byte[] commentBytes = new byte[] { 65, 83, 67, 73, 73, 0, 0, 0, 73, 32, 97, 109, 32, 97, 32, 99, 111, 109, 109, 101, 110, 116, 46, 32, 89, 101, 121, 46, 0 }; ExifSubIFDDirectory directory = new ExifSubIFDDirectory(); - directory.setByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT, commentBytes); + directory.setByteArray(TAG_USER_COMMENT, commentBytes); ExifSubIFDDescriptor descriptor = new ExifSubIFDDescriptor(directory); - assertEquals("I am a comment. Yey.", descriptor.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)); + assertEquals("I am a comment. Yey.", descriptor.getDescription(TAG_USER_COMMENT)); } } diff --git a/Tests/com/drew/metadata/exif/ExifThumbnailDescriptorTest.java b/Tests/com/drew/metadata/exif/ExifThumbnailDescriptorTest.java index 8b1bf23..d3c46bd 100644 --- a/Tests/com/drew/metadata/exif/ExifThumbnailDescriptorTest.java +++ b/Tests/com/drew/metadata/exif/ExifThumbnailDescriptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ package com.drew.metadata.exif; import org.junit.Test; +import static com.drew.metadata.exif.ExifThumbnailDirectory.*; import static org.junit.Assert.assertEquals; /** @@ -36,15 +37,15 @@ public class ExifThumbnailDescriptorTest public void testGetYCbCrSubsamplingDescription() throws Exception { ExifThumbnailDirectory directory = new ExifThumbnailDirectory(); - directory.setIntArray(ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING, new int[]{2, 1}); + directory.setIntArray(TAG_YCBCR_SUBSAMPLING, new int[]{2, 1}); ExifThumbnailDescriptor descriptor = new ExifThumbnailDescriptor(directory); - assertEquals("YCbCr4:2:2", descriptor.getDescription(ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING)); + assertEquals("YCbCr4:2:2", descriptor.getDescription(TAG_YCBCR_SUBSAMPLING)); assertEquals("YCbCr4:2:2", descriptor.getYCbCrSubsamplingDescription()); - directory.setIntArray(ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING, new int[]{2, 2}); + directory.setIntArray(TAG_YCBCR_SUBSAMPLING, new int[]{2, 2}); - assertEquals("YCbCr4:2:0", descriptor.getDescription(ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING)); + assertEquals("YCbCr4:2:0", descriptor.getDescription(TAG_YCBCR_SUBSAMPLING)); assertEquals("YCbCr4:2:0", descriptor.getYCbCrSubsamplingDescription()); } } diff --git a/Tests/com/drew/metadata/exif/NikonType1MakernoteTest.java b/Tests/com/drew/metadata/exif/NikonType1MakernoteTest.java index eb20bcd..94e99cd 100644 --- a/Tests/com/drew/metadata/exif/NikonType1MakernoteTest.java +++ b/Tests/com/drew/metadata/exif/NikonType1MakernoteTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,10 +55,10 @@ public class NikonType1MakernoteTest { Metadata metadata = ExifReaderTest.processBytes("Tests/Data/nikonMakernoteType1.jpg.app1"); - _nikonDirectory = metadata.getDirectory(NikonType1MakernoteDirectory.class); - _exifSubIFDDirectory = metadata.getDirectory(ExifSubIFDDirectory.class); - _exifIFD0Directory = metadata.getDirectory(ExifIFD0Directory.class); - _thumbDirectory = metadata.getDirectory(ExifThumbnailDirectory.class); + _nikonDirectory = metadata.getFirstDirectoryOfType(NikonType1MakernoteDirectory.class); + _exifSubIFDDirectory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); + _exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + _thumbDirectory = metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class); } /* @@ -173,7 +173,7 @@ public class NikonType1MakernoteTest assertEquals(3, _exifSubIFDDirectory.getInt(ExifSubIFDDirectory.TAG_FILE_SOURCE)); assertEquals(1, _exifSubIFDDirectory.getInt(ExifSubIFDDirectory.TAG_SCENE_TYPE)); - assertEquals(6, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)); + assertEquals(6, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_COMPRESSION)); assertEquals(2036, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET)); assertEquals(4662, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH)); } diff --git a/Tests/com/drew/metadata/exif/NikonType2MakernoteTest1.java b/Tests/com/drew/metadata/exif/NikonType2MakernoteTest1.java index caf7489..a8685ef 100644 --- a/Tests/com/drew/metadata/exif/NikonType2MakernoteTest1.java +++ b/Tests/com/drew/metadata/exif/NikonType2MakernoteTest1.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/metadata/exif/NikonType2MakernoteTest2.java b/Tests/com/drew/metadata/exif/NikonType2MakernoteTest2.java index 31f1915..a77c399 100644 --- a/Tests/com/drew/metadata/exif/NikonType2MakernoteTest2.java +++ b/Tests/com/drew/metadata/exif/NikonType2MakernoteTest2.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,11 +29,14 @@ import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import java.util.*; + /** * @author Drew Noakes https://drewnoakes.com */ public class NikonType2MakernoteTest2 { + private Metadata _metadata; private NikonType2MakernoteDirectory _nikonDirectory; private ExifIFD0Directory _exifIFD0Directory; private ExifSubIFDDirectory _exifSubIFDDirectory; @@ -42,12 +45,12 @@ public class NikonType2MakernoteTest2 @Before public void setUp() throws Exception { - Metadata metadata = ExifReaderTest.processBytes("Tests/Data/nikonMakernoteType2b.jpg.app1"); + _metadata = ExifReaderTest.processBytes("Tests/Data/nikonMakernoteType2b.jpg.app1"); - _nikonDirectory = metadata.getDirectory(NikonType2MakernoteDirectory.class); - _exifIFD0Directory = metadata.getDirectory(ExifIFD0Directory.class); - _exifSubIFDDirectory = metadata.getDirectory(ExifSubIFDDirectory.class); - _thumbDirectory = metadata.getDirectory(ExifThumbnailDirectory.class); + _nikonDirectory = _metadata.getFirstDirectoryOfType(NikonType2MakernoteDirectory.class); + _exifIFD0Directory = _metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + _exifSubIFDDirectory = _metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); + _thumbDirectory = _metadata.getFirstDirectoryOfType(ExifThumbnailDirectory.class); assertNotNull(_nikonDirectory); assertNotNull(_exifSubIFDDirectory); @@ -97,7 +100,35 @@ public class NikonType2MakernoteTest2 assertEquals(" ", _nikonDirectory.getString(0x008f)); assertEquals(0, _nikonDirectory.getInt(0x0094)); assertEquals("FPNR", _nikonDirectory.getString(0x0095)); - assertEquals("80 114 105 110 116 73 77 0 48 49 48 48 0 0 13 0 1 0 22 0 22 0 2 0 1 0 0 0 3 0 94 0 0 0 7 0 0 0 0 0 8 0 0 0 0 0 9 0 0 0 0 0 10 0 0 0 0 0 11 0 -90 0 0 0 12 0 0 0 0 0 13 0 0 0 0 0 14 0 -66 0 0 0 0 1 5 0 0 0 1 1 1 0 0 0 9 17 0 0 16 39 0 0 11 15 0 0 16 39 0 0 -105 5 0 0 16 39 0 0 -80 8 0 0 16 39 0 0 1 28 0 0 16 39 0 0 94 2 0 0 16 39 0 0 -117 0 0 0 16 39 0 0 -53 3 0 0 16 39 0 0 -27 27 0 0 16 39 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", _nikonDirectory.getString(0x0e00)); + + // PrintIM + HashMap<Integer, String> _expectedData = new HashMap<Integer, String>(); + _expectedData.put(0x0000, "0100"); + _expectedData.put(0x0001, "0x00160016"); + _expectedData.put(0x0002, "0x00000001"); + _expectedData.put(0x0003, "0x0000005e"); + _expectedData.put(0x0007, "0x00000000"); + _expectedData.put(0x0008, "0x00000000"); + _expectedData.put(0x0009, "0x00000000"); + _expectedData.put(0x000A, "0x00000000"); + _expectedData.put(0x000B, "0x000000a6"); + _expectedData.put(0x000C, "0x00000000"); + _expectedData.put(0x000D, "0x00000000"); + _expectedData.put(0x000E, "0x000000be"); + _expectedData.put(0x0100, "0x00000005"); + _expectedData.put(0x0101, "0x00000001"); + + PrintIMDirectory nikonPrintImDirectory = _metadata.getFirstDirectoryOfType(PrintIMDirectory.class); + + assertNotNull(nikonPrintImDirectory); + + assertEquals(_expectedData.size(), nikonPrintImDirectory.getTagCount()); + for (Map.Entry<Integer, String> _expected : _expectedData.entrySet()) + { + assertEquals(_expected.getValue(), nikonPrintImDirectory.getDescription(_expected.getKey())); + } + +// assertEquals("80 114 105 110 116 73 77 0 48 49 48 48 0 0 13 0 1 0 22 0 22 0 2 0 1 0 0 0 3 0 94 0 0 0 7 0 0 0 0 0 8 0 0 0 0 0 9 0 0 0 0 0 10 0 0 0 0 0 11 0 166 0 0 0 12 0 0 0 0 0 13 0 0 0 0 0 14 0 190 0 0 0 0 1 5 0 0 0 1 1 1 0 0 0 9 17 0 0 16 39 0 0 11 15 0 0 16 39 0 0 151 5 0 0 16 39 0 0 176 8 0 0 16 39 0 0 1 28 0 0 16 39 0 0 94 2 0 0 16 39 0 0 139 0 0 0 16 39 0 0 203 3 0 0 16 39 0 0 229 27 0 0 16 39 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0", _nikonDirectory.getString(0x0e00)); // assertEquals("PrintIM", _nikonDirectory.getString(0x0e00)); assertEquals(1394, _nikonDirectory.getInt(0x0e10)); } @@ -181,7 +212,7 @@ public class NikonType2MakernoteTest2 @Test public void testExifThumbnailDirectory_MatchesKnownValues() throws Exception { - assertEquals(6, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)); + assertEquals(6, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_COMPRESSION)); assertEquals(1494, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET)); assertEquals(6077, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH)); assertEquals(1494, _thumbDirectory.getInt(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET)); diff --git a/Tests/com/drew/metadata/exif/PanasonicMakernoteDescriptorTest.java b/Tests/com/drew/metadata/exif/PanasonicMakernoteDescriptorTest.java index ff328bc..e71f941 100644 --- a/Tests/com/drew/metadata/exif/PanasonicMakernoteDescriptorTest.java +++ b/Tests/com/drew/metadata/exif/PanasonicMakernoteDescriptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/metadata/exif/SonyType1MakernoteTest.java b/Tests/com/drew/metadata/exif/SonyType1MakernoteTest.java index abff3f8..38466a9 100644 --- a/Tests/com/drew/metadata/exif/SonyType1MakernoteTest.java +++ b/Tests/com/drew/metadata/exif/SonyType1MakernoteTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/metadata/exif/SonyType6MakernoteTest.java b/Tests/com/drew/metadata/exif/SonyType6MakernoteTest.java index b28fa0e..eca2ee1 100644 --- a/Tests/com/drew/metadata/exif/SonyType6MakernoteTest.java +++ b/Tests/com/drew/metadata/exif/SonyType6MakernoteTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/metadata/gif/GifReaderTest.java b/Tests/com/drew/metadata/gif/GifReaderTest.java index a45b209..08fbd48 100644 --- a/Tests/com/drew/metadata/gif/GifReaderTest.java +++ b/Tests/com/drew/metadata/gif/GifReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ public class GifReaderTest new GifReader().extract(new StreamReader(stream), metadata); stream.close(); - GifHeaderDirectory directory = metadata.getDirectory(GifHeaderDirectory.class); + GifHeaderDirectory directory = metadata.getFirstDirectoryOfType(GifHeaderDirectory.class); assertNotNull(directory); return directory; } @@ -63,7 +63,7 @@ public class GifReaderTest assertFalse(directory.getBoolean(GifHeaderDirectory.TAG_IS_COLOR_TABLE_SORTED)); assertEquals(8, directory.getInt(GifHeaderDirectory.TAG_BITS_PER_PIXEL)); assertTrue(directory.getBoolean(GifHeaderDirectory.TAG_HAS_GLOBAL_COLOR_TABLE)); - assertEquals(0, directory.getInt(GifHeaderDirectory.TAG_TRANSPARENT_COLOR_INDEX)); + assertEquals(0, directory.getInt(GifHeaderDirectory.TAG_BACKGROUND_COLOR_INDEX)); } @Test @@ -80,6 +80,6 @@ public class GifReaderTest assertFalse(directory.getBoolean(GifHeaderDirectory.TAG_IS_COLOR_TABLE_SORTED)); assertEquals(5, directory.getInt(GifHeaderDirectory.TAG_BITS_PER_PIXEL)); assertTrue(directory.getBoolean(GifHeaderDirectory.TAG_HAS_GLOBAL_COLOR_TABLE)); - assertEquals(8, directory.getInt(GifHeaderDirectory.TAG_TRANSPARENT_COLOR_INDEX)); + assertEquals(8, directory.getInt(GifHeaderDirectory.TAG_BACKGROUND_COLOR_INDEX)); } } diff --git a/Tests/com/drew/metadata/icc/IccReaderTest.java b/Tests/com/drew/metadata/icc/IccReaderTest.java index 447d145..04dd6bc 100644 --- a/Tests/com/drew/metadata/icc/IccReaderTest.java +++ b/Tests/com/drew/metadata/icc/IccReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,35 +21,67 @@ package com.drew.metadata.icc; +import com.drew.imaging.jpeg.JpegSegmentType; import com.drew.lang.ByteArrayReader; import com.drew.metadata.Metadata; import com.drew.testing.TestHelper; import com.drew.tools.FileUtil; import org.junit.Test; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +@SuppressWarnings("ConstantConditions") public class IccReaderTest { + // TODO add a test with well-formed ICC data and assert output values are correct + @Test - public void testExtract() throws Exception + public void testExtract_InvalidData() throws Exception { byte[] app2Bytes = FileUtil.readBytes("Tests/Data/iccDataInvalid1.jpg.app2"); - // ICC data starts after a 14-byte preamble + // When in an APP2 segment, ICC data starts after a 14-byte preamble byte[] icc = TestHelper.skipBytes(app2Bytes, 14); Metadata metadata = new Metadata(); new IccReader().extract(new ByteArrayReader(icc), metadata); - IccDirectory directory = metadata.getDirectory(IccDirectory.class); + IccDirectory directory = metadata.getFirstDirectoryOfType(IccDirectory.class); assertNotNull(directory); + assertTrue(directory.hasErrors()); + } + + @Test + public void testReadJpegSegments_InvalidData() throws Exception + { + byte[] app2Bytes = FileUtil.readBytes("Tests/Data/iccDataInvalid1.jpg.app2"); - // TODO validate expected values + Metadata metadata = new Metadata(); + new IccReader().readJpegSegments(Arrays.asList(app2Bytes), metadata, JpegSegmentType.APP2); -// for (Tag tag : directory.getTags()) { -// System.out.println(tag); -// } + IccDirectory directory = metadata.getFirstDirectoryOfType(IccDirectory.class); + + assertNotNull(directory); + assertTrue(directory.hasErrors()); + } + + @Test + public void testExtract_ProfileDateTime() throws Exception + { + byte[] app2Bytes = FileUtil.readBytes("Tests/Data/withExifAndIptc.jpg.app2"); + + Metadata metadata = new Metadata(); + new IccReader().readJpegSegments(Arrays.asList(app2Bytes), metadata, JpegSegmentType.APP2); + + IccDirectory directory = metadata.getFirstDirectoryOfType(IccDirectory.class); + + assertNotNull(directory); + assertEquals("1998:02:09 06:49:00", directory.getString(IccDirectory.TAG_PROFILE_DATETIME)); + assertEquals(887006940000L, directory.getDate(IccDirectory.TAG_PROFILE_DATETIME).getTime()); } } diff --git a/Tests/com/drew/metadata/iptc/IptcDirectoryTest.java b/Tests/com/drew/metadata/iptc/IptcDirectoryTest.java new file mode 100644 index 0000000..95efa9f --- /dev/null +++ b/Tests/com/drew/metadata/iptc/IptcDirectoryTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.iptc; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import static org.junit.Assert.assertEquals; + +@SuppressWarnings("ConstantConditions") +public class IptcDirectoryTest +{ + private IptcDirectory _directory; + + @Before + public void setUp() + { + _directory = new IptcDirectory(); + } + + @Test + public void testGetDateSent() + { + _directory.setString(IptcDirectory.TAG_DATE_SENT, "20101212"); + _directory.setString(IptcDirectory.TAG_TIME_SENT, "124135+0100"); + final Date actual = _directory.getDateSent(); + + Calendar calendar = new GregorianCalendar(2010, 12 - 1, 12, 12, 41, 35); + calendar.setTimeZone(TimeZone.getTimeZone("GMT+1")); + assertEquals(calendar.getTime(), actual); + assertEquals(1292154095000L, actual.getTime()); + } + + @Test + public void testGetReleaseDate() + { + _directory.setString(IptcDirectory.TAG_RELEASE_DATE, "20101212"); + _directory.setString(IptcDirectory.TAG_RELEASE_TIME, "124135+0100"); + final Date actual = _directory.getReleaseDate(); + + Calendar calendar = new GregorianCalendar(2010, 12 - 1, 12, 12, 41, 35); + calendar.setTimeZone(TimeZone.getTimeZone("GMT+1")); + assertEquals(calendar.getTime(), actual); + assertEquals(1292154095000L, actual.getTime()); + } + + @Test + public void testGetExpirationDate() + { + _directory.setString(IptcDirectory.TAG_EXPIRATION_DATE, "20101212"); + _directory.setString(IptcDirectory.TAG_EXPIRATION_TIME, "124135+0100"); + final Date actual = _directory.getExpirationDate(); + + Calendar calendar = new GregorianCalendar(2010, 12 - 1, 12, 12, 41, 35); + calendar.setTimeZone(TimeZone.getTimeZone("GMT+1")); + assertEquals(calendar.getTime(), actual); + assertEquals(1292154095000L, actual.getTime()); + } + + @Test + public void testGetDateCreated() + { + _directory.setString(IptcDirectory.TAG_DATE_CREATED, "20101212"); + _directory.setString(IptcDirectory.TAG_TIME_CREATED, "124135+0100"); + final Date actual = _directory.getDateCreated(); + + Calendar calendar = new GregorianCalendar(2010, 12 - 1, 12, 12, 41, 35); + calendar.setTimeZone(TimeZone.getTimeZone("GMT+1")); + assertEquals(calendar.getTime(), actual); + assertEquals(1292154095000L, actual.getTime()); + } + + @Test + public void testGetDigitalDateCreated() + { + _directory.setString(IptcDirectory.TAG_DIGITAL_DATE_CREATED, "20101212"); + _directory.setString(IptcDirectory.TAG_DIGITAL_TIME_CREATED, "124135+0100"); + final Date actual = _directory.getDigitalDateCreated(); + + Calendar calendar = new GregorianCalendar(2010, 12 - 1, 12, 12, 41, 35); + calendar.setTimeZone(TimeZone.getTimeZone("GMT+1")); + assertEquals(calendar.getTime(), actual); + assertEquals(1292154095000L, actual.getTime()); + } +} diff --git a/Tests/com/drew/metadata/iptc/IptcReaderTest.java b/Tests/com/drew/metadata/iptc/IptcReaderTest.java index 8c37656..23e0581 100644 --- a/Tests/com/drew/metadata/iptc/IptcReaderTest.java +++ b/Tests/com/drew/metadata/iptc/IptcReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import static org.junit.Assert.*; * * @author Drew Noakes https://drewnoakes.com */ +@SuppressWarnings("ConstantConditions") public class IptcReaderTest { @NotNull @@ -44,7 +45,7 @@ public class IptcReaderTest Metadata metadata = new Metadata(); byte[] bytes = FileUtil.readBytes(filePath); new IptcReader().extract(new SequentialByteArrayReader(bytes), metadata, bytes.length); - IptcDirectory directory = metadata.getDirectory(IptcDirectory.class); + IptcDirectory directory = metadata.getFirstDirectoryOfType(IptcDirectory.class); assertNotNull(directory); return directory; } @@ -63,49 +64,49 @@ public class IptcReaderTest assertArrayEquals(new String[] { "Supl. Category2", "Supl. Category1", "Cat" }, directory.getStringArray(tags[0].getTagType())); assertEquals(IptcDirectory.TAG_COPYRIGHT_NOTICE, tags[1].getTagType()); - assertEquals("Copyright", directory.getObject(tags[1].getTagType())); + assertEquals("Copyright", directory.getString(tags[1].getTagType())); assertEquals(IptcDirectory.TAG_SPECIAL_INSTRUCTIONS, tags[2].getTagType()); - assertEquals("Special Instr.", directory.getObject(tags[2].getTagType())); + assertEquals("Special Instr.", directory.getString(tags[2].getTagType())); assertEquals(IptcDirectory.TAG_HEADLINE, tags[3].getTagType()); - assertEquals("Headline", directory.getObject(tags[3].getTagType())); + assertEquals("Headline", directory.getString(tags[3].getTagType())); assertEquals(IptcDirectory.TAG_CAPTION_WRITER, tags[4].getTagType()); - assertEquals("CaptionWriter", directory.getObject(tags[4].getTagType())); + assertEquals("CaptionWriter", directory.getString(tags[4].getTagType())); assertEquals(IptcDirectory.TAG_CAPTION, tags[5].getTagType()); - assertEquals("Caption", directory.getObject(tags[5].getTagType())); + assertEquals("Caption", directory.getString(tags[5].getTagType())); assertEquals(IptcDirectory.TAG_ORIGINAL_TRANSMISSION_REFERENCE, tags[6].getTagType()); - assertEquals("Transmission", directory.getObject(tags[6].getTagType())); + assertEquals("Transmission", directory.getString(tags[6].getTagType())); assertEquals(IptcDirectory.TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME, tags[7].getTagType()); - assertEquals("Country", directory.getObject(tags[7].getTagType())); + assertEquals("Country", directory.getString(tags[7].getTagType())); assertEquals(IptcDirectory.TAG_PROVINCE_OR_STATE, tags[8].getTagType()); - assertEquals("State", directory.getObject(tags[8].getTagType())); + assertEquals("State", directory.getString(tags[8].getTagType())); assertEquals(IptcDirectory.TAG_CITY, tags[9].getTagType()); - assertEquals("City", directory.getObject(tags[9].getTagType())); + assertEquals("City", directory.getString(tags[9].getTagType())); assertEquals(IptcDirectory.TAG_DATE_CREATED, tags[10].getTagType()); - assertEquals(new java.util.GregorianCalendar(2000, 0, 1).getTime(), directory.getObject(tags[10].getTagType())); + assertEquals("20000101", directory.getString(tags[10].getTagType())); assertEquals(IptcDirectory.TAG_OBJECT_NAME, tags[11].getTagType()); - assertEquals("ObjectName", directory.getObject(tags[11].getTagType())); + assertEquals("ObjectName", directory.getString(tags[11].getTagType())); assertEquals(IptcDirectory.TAG_SOURCE, tags[12].getTagType()); - assertEquals("Source", directory.getObject(tags[12].getTagType())); + assertEquals("Source", directory.getString(tags[12].getTagType())); assertEquals(IptcDirectory.TAG_CREDIT, tags[13].getTagType()); - assertEquals("Credits", directory.getObject(tags[13].getTagType())); + assertEquals("Credits", directory.getString(tags[13].getTagType())); assertEquals(IptcDirectory.TAG_BY_LINE_TITLE, tags[14].getTagType()); - assertEquals("BylineTitle", directory.getObject(tags[14].getTagType())); + assertEquals("BylineTitle", directory.getString(tags[14].getTagType())); assertEquals(IptcDirectory.TAG_BY_LINE, tags[15].getTagType()); - assertEquals("Byline", directory.getObject(tags[15].getTagType())); + assertEquals("Byline", directory.getString(tags[15].getTagType())); } @Test @@ -122,52 +123,52 @@ public class IptcReaderTest assertEquals(2, directory.getObject(tags[0].getTagType())); assertEquals(IptcDirectory.TAG_CAPTION, tags[1].getTagType()); - assertEquals("Caption PS6", directory.getObject(tags[1].getTagType())); + assertEquals("Caption PS6", directory.getString(tags[1].getTagType())); assertEquals(IptcDirectory.TAG_CAPTION_WRITER, tags[2].getTagType()); - assertEquals("CaptionWriter", directory.getObject(tags[2].getTagType())); + assertEquals("CaptionWriter", directory.getString(tags[2].getTagType())); assertEquals(IptcDirectory.TAG_HEADLINE, tags[3].getTagType()); - assertEquals("Headline", directory.getObject(tags[3].getTagType())); + assertEquals("Headline", directory.getString(tags[3].getTagType())); assertEquals(IptcDirectory.TAG_SPECIAL_INSTRUCTIONS, tags[4].getTagType()); - assertEquals("Special Instr.", directory.getObject(tags[4].getTagType())); + assertEquals("Special Instr.", directory.getString(tags[4].getTagType())); assertEquals(IptcDirectory.TAG_BY_LINE, tags[5].getTagType()); - assertEquals("Byline", directory.getObject(tags[5].getTagType())); + assertEquals("Byline", directory.getString(tags[5].getTagType())); assertEquals(IptcDirectory.TAG_BY_LINE_TITLE, tags[6].getTagType()); - assertEquals("BylineTitle", directory.getObject(tags[6].getTagType())); + assertEquals("BylineTitle", directory.getString(tags[6].getTagType())); assertEquals(IptcDirectory.TAG_CREDIT, tags[7].getTagType()); - assertEquals("Credits", directory.getObject(tags[7].getTagType())); + assertEquals("Credits", directory.getString(tags[7].getTagType())); assertEquals(IptcDirectory.TAG_SOURCE, tags[8].getTagType()); - assertEquals("Source", directory.getObject(tags[8].getTagType())); + assertEquals("Source", directory.getString(tags[8].getTagType())); assertEquals(IptcDirectory.TAG_OBJECT_NAME, tags[9].getTagType()); - assertEquals("ObjectName", directory.getObject(tags[9].getTagType())); + assertEquals("ObjectName", directory.getString(tags[9].getTagType())); assertEquals(IptcDirectory.TAG_CITY, tags[10].getTagType()); - assertEquals("City", directory.getObject(tags[10].getTagType())); + assertEquals("City", directory.getString(tags[10].getTagType())); assertEquals(IptcDirectory.TAG_PROVINCE_OR_STATE, tags[11].getTagType()); - assertEquals("State", directory.getObject(tags[11].getTagType())); + assertEquals("State", directory.getString(tags[11].getTagType())); assertEquals(IptcDirectory.TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME, tags[12].getTagType()); - assertEquals("Country", directory.getObject(tags[12].getTagType())); + assertEquals("Country", directory.getString(tags[12].getTagType())); assertEquals(IptcDirectory.TAG_ORIGINAL_TRANSMISSION_REFERENCE, tags[13].getTagType()); - assertEquals("Transmission", directory.getObject(tags[13].getTagType())); + assertEquals("Transmission", directory.getString(tags[13].getTagType())); assertEquals(IptcDirectory.TAG_CATEGORY, tags[14].getTagType()); - assertEquals("Cat", directory.getObject(tags[14].getTagType())); + assertEquals("Cat", directory.getString(tags[14].getTagType())); assertEquals(IptcDirectory.TAG_SUPPLEMENTAL_CATEGORIES, tags[15].getTagType()); assertArrayEquals(new String[] { "Supl. Category1", "Supl. Category2" }, directory.getStringArray(tags[15].getTagType())); assertEquals(IptcDirectory.TAG_COPYRIGHT_NOTICE, tags[16].getTagType()); - assertEquals("Copyright", directory.getObject(tags[16].getTagType())); + assertEquals("Copyright", directory.getString(tags[16].getTagType())); } @Test @@ -190,7 +191,7 @@ public class IptcReaderTest assertEquals(2, directory.getObject(tags[2].getTagType())); assertEquals(IptcDirectory.TAG_CAPTION, tags[3].getTagType()); - assertEquals("In diesem Text sind Umlaute enthalten, nämlich öfter als üblich: ÄÖÜäöüß\r", directory.getObject(tags[3].getTagType())); + assertEquals("In diesem Text sind Umlaute enthalten, nämlich öfter als üblich: ÄÖÜäöüß\r", directory.getStringValue(tags[3].getTagType()).toString()); } @Test @@ -210,7 +211,7 @@ public class IptcReaderTest assertEquals(2, directory.getObject(tags[1].getTagType())); assertEquals(IptcDirectory.TAG_CAPTION, tags[2].getTagType()); - assertEquals("In diesem Text sind Umlaute enthalten, nämlich öfter als üblich: ÄÖÜäöüß\r", directory.getObject(tags[2].getTagType())); + assertEquals("In diesem Text sind Umlaute enthalten, nämlich öfter als üblich: ÄÖÜäöüß\r", directory.getStringValue(tags[2].getTagType()).toString()); } @Test @@ -227,7 +228,7 @@ public class IptcReaderTest assertEquals(2, directory.getObject(tags[0].getTagType())); assertEquals(IptcDirectory.TAG_CAPTION, tags[1].getTagType()); - assertEquals("Das Encoding dieser Metadaten ist nicht deklariert und lässt sich nur schwer erkennen.", directory.getObject(tags[1].getTagType())); + assertEquals("Das Encoding dieser Metadaten ist nicht deklariert und lässt sich nur schwer erkennen.", directory.getStringValue(tags[1].getTagType()).toString()); assertEquals(IptcDirectory.TAG_KEYWORDS, tags[2].getTagType()); assertArrayEquals(new String[]{"häufig", "üblich", "Lösung", "Spaß"}, directory.getStringArray(tags[2].getTagType())); diff --git a/Tests/com/drew/metadata/iptc/Iso2022ConverterTest.java b/Tests/com/drew/metadata/iptc/Iso2022ConverterTest.java index 3365898..25dbc8a 100644 --- a/Tests/com/drew/metadata/iptc/Iso2022ConverterTest.java +++ b/Tests/com/drew/metadata/iptc/Iso2022ConverterTest.java @@ -1,9 +1,29 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.iptc; -import static org.junit.Assert.assertEquals; - import org.junit.Test; +import static org.junit.Assert.assertEquals; + public class Iso2022ConverterTest { @Test diff --git a/Tests/com/drew/metadata/jfif/JfifReaderTest.java b/Tests/com/drew/metadata/jfif/JfifReaderTest.java index 6bd84a2..3fb4b49 100644 --- a/Tests/com/drew/metadata/jfif/JfifReaderTest.java +++ b/Tests/com/drew/metadata/jfif/JfifReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,12 +49,12 @@ public class JfifReaderTest reader.extract(new ByteArrayReader(jfifData), metadata); assertEquals(1, metadata.getDirectoryCount()); - JfifDirectory directory = metadata.getDirectory(JfifDirectory.class); + JfifDirectory directory = metadata.getFirstDirectoryOfType(JfifDirectory.class); assertNotNull(directory); assertFalse(directory.getErrors().toString(), directory.hasErrors()); Tag[] tags = directory.getTags().toArray(new Tag[directory.getTagCount()]); - assertEquals(4, tags.length); + assertEquals(6, tags.length); assertEquals(JfifDirectory.TAG_VERSION, tags[0].getTagType()); assertEquals(0x0102, directory.getInt(tags[0].getTagType())); @@ -67,5 +67,11 @@ public class JfifReaderTest assertEquals(JfifDirectory.TAG_RESY, tags[3].getTagType()); assertEquals(108, directory.getInt(tags[3].getTagType())); + + assertEquals(JfifDirectory.TAG_THUMB_WIDTH, tags[4].getTagType()); + assertEquals(0, directory.getInt(tags[4].getTagType())); + + assertEquals(JfifDirectory.TAG_THUMB_HEIGHT, tags[5].getTagType()); + assertEquals(0, directory.getInt(tags[5].getTagType())); } } diff --git a/Tests/com/drew/metadata/jpeg/HuffmanTablesDescriptorTest.java b/Tests/com/drew/metadata/jpeg/HuffmanTablesDescriptorTest.java new file mode 100644 index 0000000..567d438 --- /dev/null +++ b/Tests/com/drew/metadata/jpeg/HuffmanTablesDescriptorTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.jpeg; + +import org.junit.Before; +import org.junit.Test; + +import static com.drew.metadata.jpeg.HuffmanTablesDirectory.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * @author Nadahar + */ +public class HuffmanTablesDescriptorTest +{ + private HuffmanTablesDirectory _directory; + private HuffmanTablesDescriptor _descriptor; + + @Before + public void setUp() throws Exception + { + _directory = new HuffmanTablesDirectory(); + _descriptor = new HuffmanTablesDescriptor(_directory); + } + + @Test + public void testGetNumberOfTablesDescription() throws Exception + { + assertNull(_descriptor.getNumberOfTablesDescription()); + _directory.setInt(TAG_NUMBER_OF_TABLES, 0); + assertEquals("0 Huffman tables", _descriptor.getNumberOfTablesDescription()); + assertEquals("0 Huffman tables", _descriptor.getDescription(TAG_NUMBER_OF_TABLES)); + _directory.setInt(TAG_NUMBER_OF_TABLES, 1); + assertEquals("1 Huffman table", _descriptor.getNumberOfTablesDescription()); + assertEquals("1 Huffman table", _descriptor.getDescription(TAG_NUMBER_OF_TABLES)); + _directory.setInt(TAG_NUMBER_OF_TABLES, 3); + assertEquals("3 Huffman tables", _descriptor.getNumberOfTablesDescription()); + assertEquals("3 Huffman tables", _descriptor.getDescription(TAG_NUMBER_OF_TABLES)); + + } +} diff --git a/Tests/com/drew/metadata/jpeg/HuffmanTablesDirectoryTest.java b/Tests/com/drew/metadata/jpeg/HuffmanTablesDirectoryTest.java new file mode 100644 index 0000000..3f77faf --- /dev/null +++ b/Tests/com/drew/metadata/jpeg/HuffmanTablesDirectoryTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.jpeg; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable; +import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable.HuffmanTableClass; + +/** + * @author Nadahar + */ +public class HuffmanTablesDirectoryTest +{ + private HuffmanTablesDirectory _directory; + + @Before + public void setUp() + { + _directory = new HuffmanTablesDirectory(); + } + + @Test + public void testSetAndGetValue() throws Exception + { + _directory.setInt(32, 8); + assertEquals(8, _directory.getInt(32)); + } + + @Test + public void testGetComponent_NotAdded() + { + try { + _directory.getTable(1); + fail(); + } catch (IndexOutOfBoundsException e) { + // Expected exception + } + } + + @Test + public void testGetNumberOfTables() throws Exception + { + _directory.setInt(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES, 9); + assertEquals(9,_directory.getNumberOfTables()); + assertEquals("9 Huffman tables", _directory.getDescription(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES)); + } + + @Test + public void testIsTypical() throws Exception + { + _directory.tables.add(new HuffmanTable( + HuffmanTableClass.AC, + 0, + HuffmanTablesDirectory.TYPICAL_CHROMINANCE_AC_LENGTHS, + HuffmanTablesDirectory.TYPICAL_CHROMINANCE_AC_VALUES + )); + _directory.tables.add(new HuffmanTable( + HuffmanTableClass.DC, + 0, + HuffmanTablesDirectory.TYPICAL_LUMINANCE_DC_LENGTHS, + HuffmanTablesDirectory.TYPICAL_LUMINANCE_DC_VALUES + )); + + assertTrue(_directory.getTable(0).isTypical()); + assertFalse(_directory.getTable(0).isOptimized()); + assertTrue(_directory.getTable(1).isTypical()); + assertFalse(_directory.getTable(1).isOptimized()); + + assertTrue(_directory.isTypical()); + assertFalse(_directory.isOptimized()); + } +} diff --git a/Tests/com/drew/metadata/jpeg/JpegComponentTest.java b/Tests/com/drew/metadata/jpeg/JpegComponentTest.java index 3ae1fa8..12a7466 100644 --- a/Tests/com/drew/metadata/jpeg/JpegComponentTest.java +++ b/Tests/com/drew/metadata/jpeg/JpegComponentTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/metadata/jpeg/JpegDescriptorTest.java b/Tests/com/drew/metadata/jpeg/JpegDescriptorTest.java index a804df5..efdaf84 100644 --- a/Tests/com/drew/metadata/jpeg/JpegDescriptorTest.java +++ b/Tests/com/drew/metadata/jpeg/JpegDescriptorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import com.drew.metadata.MetadataException; import org.junit.Before; import org.junit.Test; +import static com.drew.metadata.jpeg.JpegDirectory.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -51,33 +52,33 @@ public class JpegDescriptorTest @Test public void testGetImageWidthDescription() throws Exception { - _directory.setInt(JpegDirectory.TAG_IMAGE_WIDTH, 123); + _directory.setInt(TAG_IMAGE_WIDTH, 123); assertEquals("123 pixels", _descriptor.getImageWidthDescription()); - assertEquals("123 pixels", _directory.getDescription(JpegDirectory.TAG_IMAGE_WIDTH)); + assertEquals("123 pixels", _directory.getDescription(TAG_IMAGE_WIDTH)); } @Test public void testGetImageHeightDescription() throws Exception { - _directory.setInt(JpegDirectory.TAG_IMAGE_HEIGHT, 123); + _directory.setInt(TAG_IMAGE_HEIGHT, 123); assertEquals("123 pixels", _descriptor.getImageHeightDescription()); - assertEquals("123 pixels", _directory.getDescription(JpegDirectory.TAG_IMAGE_HEIGHT)); + assertEquals("123 pixels", _directory.getDescription(TAG_IMAGE_HEIGHT)); } @Test public void testGetDataPrecisionDescription() throws Exception { - _directory.setInt(JpegDirectory.TAG_DATA_PRECISION, 8); + _directory.setInt(TAG_DATA_PRECISION, 8); assertEquals("8 bits", _descriptor.getDataPrecisionDescription()); - assertEquals("8 bits", _directory.getDescription(JpegDirectory.TAG_DATA_PRECISION)); + assertEquals("8 bits", _directory.getDescription(TAG_DATA_PRECISION)); } @Test public void testGetComponentDescription() throws MetadataException { JpegComponent component1 = new JpegComponent(1, 0x22, 0); - _directory.setObject(JpegDirectory.TAG_COMPONENT_DATA_1, component1); - assertEquals("Y component: Quantization table 0, Sampling factors 2 horiz/2 vert", _directory.getDescription(JpegDirectory.TAG_COMPONENT_DATA_1)); + _directory.setObject(TAG_COMPONENT_DATA_1, component1); + assertEquals("Y component: Quantization table 0, Sampling factors 2 horiz/2 vert", _directory.getDescription(TAG_COMPONENT_DATA_1)); assertEquals("Y component: Quantization table 0, Sampling factors 2 horiz/2 vert", _descriptor.getComponentDataDescription(0)); } } diff --git a/Tests/com/drew/metadata/jpeg/JpegDhtReaderTest.java b/Tests/com/drew/metadata/jpeg/JpegDhtReaderTest.java new file mode 100644 index 0000000..ba40def --- /dev/null +++ b/Tests/com/drew/metadata/jpeg/JpegDhtReaderTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2017 Drew Noakes + * + * 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.jpeg; + +import com.drew.imaging.jpeg.JpegSegmentData; +import com.drew.imaging.jpeg.JpegSegmentReader; +import com.drew.imaging.jpeg.JpegSegmentType; +import com.drew.lang.SequentialByteArrayReader; +import com.drew.lang.annotations.NotNull; +import com.drew.metadata.Metadata; +import com.drew.metadata.jpeg.HuffmanTablesDirectory.HuffmanTable.HuffmanTableClass; +import org.junit.Before; +import org.junit.Test; +import java.io.File; +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.*; + +/** + * @author Nadahar + */ +public class JpegDhtReaderTest +{ + @NotNull + public static HuffmanTablesDirectory processBytes(String filePath) throws Exception + { + Metadata metadata = new Metadata(); + JpegSegmentData segmentData = JpegSegmentReader.readSegments( + new File(filePath), + Collections.singletonList(JpegSegmentType.DHT)); + + Iterable<byte[]> segments = segmentData.getSegments(JpegSegmentType.DHT); + for (byte[] segment : segments) { + new JpegDhtReader().extract(new SequentialByteArrayReader(segment), metadata); + } + + + HuffmanTablesDirectory directory = metadata.getFirstDirectoryOfType(HuffmanTablesDirectory.class); + assertNotNull(directory); + assertEquals(1, metadata.getDirectoriesOfType(HuffmanTablesDirectory.class).size()); + return directory; + } + + private HuffmanTablesDirectory _directory; + + @Before + public void setUp() throws Exception + { + _directory = processBytes("Tests/Data/withExifAndIptc.jpg"); + } + + @Test + public void testExtract_NumberOfTables() throws Exception + { + assertEquals(4, _directory.getInt(HuffmanTablesDirectory.TAG_NUMBER_OF_TABLES)); + assertEquals(4, _directory.getNumberOfTables()); + } + + @Test + public void testExtract_Tables() throws Exception + { + byte[] l = {0, 1, 4, 1, 2, 3, 3, 8, 5, 9, 6, 4, 6, 2, 3, 0}; + byte[] v = {0, 1, 3, 2, 4, 5}; + + assertArrayEquals(l, _directory.getTable(1).getLengthBytes()); + assertArrayEquals(v, _directory.getTable(2).getValueBytes()); + assertEquals(HuffmanTableClass.DC, _directory.getTable(0).getTableClass()); + assertEquals(HuffmanTableClass.AC, _directory.getTable(1).getTableClass()); + assertEquals(HuffmanTableClass.DC, _directory.getTable(2).getTableClass()); + assertEquals(HuffmanTableClass.AC, _directory.getTable(3).getTableClass()); + assertEquals(0, _directory.getTable(0).getTableDestinationId()); + assertEquals(0, _directory.getTable(1).getTableDestinationId()); + assertEquals(1, _directory.getTable(2).getTableDestinationId()); + assertEquals(1, _directory.getTable(3).getTableDestinationId()); + assertEquals(25, _directory.getTable(0).getTableLength()); + assertEquals(74, _directory.getTable(1).getTableLength()); + assertEquals(23, _directory.getTable(2).getTableLength()); + assertEquals(38, _directory.getTable(3).getTableLength()); + } +} diff --git a/Tests/com/drew/metadata/jpeg/JpegDirectoryTest.java b/Tests/com/drew/metadata/jpeg/JpegDirectoryTest.java index e6b65b3..5e2a565 100644 --- a/Tests/com/drew/metadata/jpeg/JpegDirectoryTest.java +++ b/Tests/com/drew/metadata/jpeg/JpegDirectoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Tests/com/drew/metadata/jpeg/JpegReaderTest.java b/Tests/com/drew/metadata/jpeg/JpegReaderTest.java index b433938..766de5f 100644 --- a/Tests/com/drew/metadata/jpeg/JpegReaderTest.java +++ b/Tests/com/drew/metadata/jpeg/JpegReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ public class JpegReaderTest Metadata metadata = new Metadata(); new JpegReader().extract(FileUtil.readBytes(filePath), metadata, JpegSegmentType.SOF0); - JpegDirectory directory = metadata.getDirectory(JpegDirectory.class); + JpegDirectory directory = metadata.getFirstDirectoryOfType(JpegDirectory.class); assertNotNull(directory); return directory; } diff --git a/Tests/com/drew/metadata/photoshop/PsdReaderTest.java b/Tests/com/drew/metadata/photoshop/PsdReaderTest.java index fdbb4b5..8d77df0 100644 --- a/Tests/com/drew/metadata/photoshop/PsdReaderTest.java +++ b/Tests/com/drew/metadata/photoshop/PsdReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,13 +21,14 @@ package com.drew.metadata.photoshop; -import com.drew.lang.RandomAccessFileReader; +import com.drew.lang.StreamReader; import com.drew.lang.annotations.NotNull; import com.drew.metadata.Metadata; import org.junit.Test; import java.io.File; -import java.io.RandomAccessFile; +import java.io.FileInputStream; +import java.io.InputStream; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -41,11 +42,15 @@ public class PsdReaderTest public static PsdHeaderDirectory processBytes(@NotNull String file) throws Exception { Metadata metadata = new Metadata(); - RandomAccessFile randomAccessFile = new RandomAccessFile(new File(file), "r"); - new PsdReader().extract(new RandomAccessFileReader(randomAccessFile), metadata); - randomAccessFile.close(); + InputStream stream = new FileInputStream(new File(file)); + try { + new PsdReader().extract(new StreamReader(stream), metadata); + } catch (Exception e) { + stream.close(); + throw e; + } - PsdHeaderDirectory directory = metadata.getDirectory(PsdHeaderDirectory.class); + PsdHeaderDirectory directory = metadata.getFirstDirectoryOfType(PsdHeaderDirectory.class); assertNotNull(directory); return directory; } diff --git a/Tests/com/drew/metadata/xmp/XmpReaderTest.java b/Tests/com/drew/metadata/xmp/XmpReaderTest.java index 492da51..e58792a 100644 --- a/Tests/com/drew/metadata/xmp/XmpReaderTest.java +++ b/Tests/com/drew/metadata/xmp/XmpReaderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,14 +21,11 @@ package com.drew.metadata.xmp; import com.drew.imaging.jpeg.JpegSegmentType; -import com.drew.lang.Rational; import com.drew.metadata.Metadata; import com.drew.tools.FileUtil; import org.junit.Before; import org.junit.Test; -import java.io.IOException; -import java.text.SimpleDateFormat; import java.util.*; import static org.junit.Assert.*; @@ -38,51 +35,24 @@ import static org.junit.Assert.*; */ public class XmpReaderTest { - public static XmpDirectory processApp1Bytes(String filePath) throws IOException - { - Metadata metadata = new Metadata(); - new XmpReader().extract(FileUtil.readBytes(filePath), metadata, JpegSegmentType.APP1); - XmpDirectory directory = metadata.getDirectory(XmpDirectory.class); - assertNotNull(directory); - return directory; - } - private XmpDirectory _directory; @Before public void setUp() throws Exception { - _directory = processApp1Bytes("Tests/Data/withXmpAndIptc.jpg.app1.1"); - } + Metadata metadata = new Metadata(); + List<byte[]> jpegSegments = new ArrayList<byte[]>(); + jpegSegments.add(FileUtil.readBytes("Tests/Data/withXmpAndIptc.jpg.app1.1")); + new XmpReader().readJpegSegments(jpegSegments, metadata, JpegSegmentType.APP1); - /* - [Xmp] Lens Information = 24/1 70/1 0/0 0/0 - [Xmp] Lens = EF24-70mm f/2.8L USM - [Xmp] Serial Number = 380319450 - [Xmp] Firmware = 1.2.1 - [Xmp] Make = Canon - [Xmp] Model = Canon EOS 7D - [Xmp] Exposure Time = 1/125 sec - [Xmp] Exposure Program = Manual control - [Xmp] Aperture Value = F11 - [Xmp] F-Number = F11 - [Xmp] Focal Length = 57.0 mm - [Xmp] Shutter Speed Value = 1/124 sec - [Xmp] Date/Time Original = Sun Dec 12 11:41:35 GMT 2010 - [Xmp] Date/Time Digitized = Sun Dec 12 11:41:35 GMT 2010 - */ + Collection<XmpDirectory> xmpDirectories = metadata.getDirectoriesOfType(XmpDirectory.class); - @Test - public void testExtract_LensInformation() throws Exception - { - // Note that this tag really holds a rational array, but XmpReader doesn't parse arrays - assertEquals("24/1 70/1 0/0 0/0", _directory.getString(XmpDirectory.TAG_LENS_INFO)); + assertNotNull(xmpDirectories); + assertEquals(1, xmpDirectories.size()); -// Rational[] info = _directory.getRationalArray(XmpDirectory.TAG_LENS_INFO); -// assertEquals(new Rational(24, 1), info[0]); -// assertEquals(new Rational(70, 1), info[1]); -// assertEquals(new Rational(0, 0), info[2]); -// assertEquals(new Rational(0, 0), info[3]); + _directory = xmpDirectories.iterator().next(); + + assertFalse(_directory.hasErrors()); } @Test @@ -92,123 +62,9 @@ public class XmpReaderTest } @Test - public void testExtract_Lens() throws Exception - { - assertEquals("EF24-70mm f/2.8L USM", _directory.getString(XmpDirectory.TAG_LENS)); - } - -/* - // this requires further research - - @Test - public void testExtract_Format() throws Exception - { - assertEquals("image/tiff", _directory.getString(XmpDirectory.TAG_FORMAT)); - } - - @Test - public void testExtract_Creator() throws Exception - { - assertEquals("", _directory.getString(XmpDirectory.TAG_CREATOR)); - } - - @Test - public void testExtract_Rights() throws Exception - { - assertEquals("", _directory.getString(XmpDirectory.TAG_RIGHTS)); - } - - @Test - public void testExtract_Description() throws Exception - { - assertEquals("", _directory.getString(XmpDirectory.TAG_DESCRIPTION)); - } -*/ - - @Test - public void testExtract_SerialNumber() throws Exception - { - assertEquals("380319450", _directory.getString(XmpDirectory.TAG_CAMERA_SERIAL_NUMBER)); - } - - @Test - public void testExtract_Firmware() throws Exception - { - assertEquals("1.2.1", _directory.getString(XmpDirectory.TAG_FIRMWARE)); - } - - @Test - public void testExtract_Maker() throws Exception + public void testExtract_PropertyCount() throws Exception { - assertEquals("Canon", _directory.getString(XmpDirectory.TAG_MAKE)); - } - - @Test - public void testExtract_Model() throws Exception - { - assertEquals("Canon EOS 7D", _directory.getString(XmpDirectory.TAG_MODEL)); - } - - @Test - public void testExtract_ExposureTime() throws Exception - { - // Note XmpReader doesn't parse this as a rational even though it appears to be... need more examples - assertEquals("1/125", _directory.getString(XmpDirectory.TAG_EXPOSURE_TIME)); -// assertEquals(new Rational(1, 125), _directory.getRational(XmpDirectory.TAG_EXPOSURE_TIME)); - } - - @Test - public void testExtract_ExposureProgram() throws Exception - { - assertEquals(1, _directory.getInt(XmpDirectory.TAG_EXPOSURE_PROGRAM)); - } - - @Test - public void testExtract_FNumber() throws Exception - { - assertEquals(new Rational(11, 1), _directory.getRational(XmpDirectory.TAG_F_NUMBER)); - } - - @Test - public void testExtract_FocalLength() throws Exception - { - assertEquals(new Rational(57, 1), _directory.getRational(XmpDirectory.TAG_FOCAL_LENGTH)); - } - - @Test - public void testExtract_ShutterSpeed() throws Exception - { - assertEquals(new Rational(6965784, 1000000), _directory.getRational(XmpDirectory.TAG_SHUTTER_SPEED)); - } - - @Test - public void testExtract_OriginalDateTime() throws Exception - { - final Date actual = _directory.getDate(XmpDirectory.TAG_DATETIME_ORIGINAL); - - // Underlying string value (in XMP data) is: 2010-12-12T12:41:35.00+01:00 - - assertEquals(new SimpleDateFormat("hh:mm:ss dd MM yyyy Z").parse("11:41:35 12 12 2010 +0000"), actual); -// assertEquals(new SimpleDateFormat("HH:mm:ss dd MMM yyyy Z").parse("12:41:35 12 Dec 2010 +0100"), actual); - - Calendar calendar = new GregorianCalendar(2010, 12-1, 12, 11, 41, 35); - calendar.setTimeZone(TimeZone.getTimeZone("GMT")); - assertEquals(calendar.getTime(), actual); - } - - @Test - public void testExtract_DigitizedDateTime() throws Exception - { - final Date actual = _directory.getDate(XmpDirectory.TAG_DATETIME_DIGITIZED); - - // Underlying string value (in XMP data) is: 2010-12-12T12:41:35.00+01:00 - - assertEquals(new SimpleDateFormat("hh:mm:ss dd MM yyyy Z").parse("11:41:35 12 12 2010 +0000"), actual); -// assertEquals(new SimpleDateFormat("HH:mm:ss dd MMM yyyy Z").parse("12:41:35 12 Dec 2010 +0100"), actual); - - Calendar calendar = new GregorianCalendar(2010, 12-1, 12, 11, 41, 35); - calendar.setTimeZone(TimeZone.getTimeZone("GMT")); - assertEquals(calendar.getTime(), actual); + assertEquals(179, _directory.getInt(XmpDirectory.TAG_XMP_VALUE_COUNT)); } @Test diff --git a/Tests/com/drew/testing/TestHelper.java b/Tests/com/drew/testing/TestHelper.java index 77dce35..d17edd2 100644 --- a/Tests/com/drew/testing/TestHelper.java +++ b/Tests/com/drew/testing/TestHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 Drew Noakes + * Copyright 2002-2017 Drew Noakes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/build.xml b/build.xml deleted file mode 100644 index ae33499..0000000 --- a/build.xml +++ /dev/null @@ -1,192 +0,0 @@ -<?xml version="1.0"?> - -<!-- - ~ Copyright 2002-2015 Drew Noakes - ~ - ~ 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 - --> - -<!--suppress XmlUnboundNsPrefix --> -<project name="metadata-extractor" default="test" basedir="."> - - <description>metadata-extractor build file</description> - - <property name="library-version" value="2.7.2"/> - <property name="java-version" value="1.5"/> - <property name="dist" location="Releases"/> - <property name="src" value="Source"/> - <property name="output" value="Output/Source"/> - <property name="test-src" value="Tests"/> - <property name="test-output" value="Output/Tests"/> - <property name="sample-src" value="Samples"/> - <property name="javadoc" value="../javadoc/${library-version}"/> - <property name="sample-images" value="../sample-images/"/> - <property name="sample-images-output" value="../sample-images/metadata"/> - <property name="lib" value="Libraries"/> - <property name="verbose" value="true"/> - <property name="debug" value="off"/> - <property name="lib-xmp" value="${lib}/xmpcore-5.1.2.jar"/> - <property name="lib-junit" value="${lib}/junit-4.11.jar"/> - <property name="classpath" value="${lib-junit};${lib-xmp}"/> - - <target name="clean" description="deletes and recreates the destination directory"> - <delete verbose="${verbose}" dir="${output}"/> - <mkdir dir="${output}"/> - <delete verbose="${verbose}" dir="${test-output}"/> - <mkdir dir="${test-output}"/> - <mkdir dir="${dist}"/> - </target> - - <target name="compile" description="compile the source"> - <echo message="Using Java version ${ant.java.version}"/> - <javac classpath="${classpath}" - srcdir="${src}" - destdir="${output}" - source="${java-version}" - target="${java-version}" - encoding="UTF-8" - debug="${debug}" - verbose="${verbose}" - includeantruntime="false"/> - <javac classpath="${classpath};${output}" - srcdir="${test-src}" - destdir="${test-output}" - source="${java-version}" - target="${java-version}" - encoding="UTF-8" - debug="${debug}" - verbose="${verbose}" - includeantruntime="false"/> - </target> - - <target name="test" depends="clean, compile" description="run all junit tests"> - <junit printsummary="yes" logfailedtests="true" fork="yes" haltonfailure="yes" forkmode="once"> - <formatter type="plain" usefile="false" /> - <classpath> - <pathelement location="${output}"/> - <pathelement location="${test-output}"/> - <pathelement path="${java.class.path}"/> - <pathelement path="${lib-junit}"/> - <pathelement path="${lib-xmp}"/> - </classpath> - <batchtest> - <fileset dir="${test-src}"> - <include name="**/*Test.java" /> - </fileset> - </batchtest> - </junit> - </target> - - <target name="process-sample-files" depends="compile" description="extract metadata from all sample images, and update output text files"> - <delete verbose="${verbose}" dir="${sample-images-output}"/> - <mkdir dir="${sample-images-output}"/> - <java classname="com.drew.tools.ProcessAllImagesInFolderUtility" - classpath="${output};${lib-xmp}"> - <arg value="${sample-images}"/> - <arg value="-text"/> - </java> - <java classname="com.drew.tools.ProcessAllImagesInFolderUtility" - classpath="${output};${lib-xmp}"> - <arg value="${sample-images}"/> - <arg value="-wiki"/> - </java> - </target> - - <target name="dist-binaries" depends="clean, compile, test" description="generate binary distribution"> - <property name="bin-jar" value="${dist}/metadata-extractor-${library-version}.jar" /> - <property name="bin-zip" value="${dist}/metadata-extractor-${library-version}.zip" /> - <jar destfile="${bin-jar}" update="false"> - <manifest> - <attribute name="Main-Class" value="com.drew.imaging.ImageMetadataReader"/> - <attribute name="Implementation-Title" value="metadata-extractor"/> - <attribute name="Implementation-Vendor" value="Drew Noakes"/> - <attribute name="Implementation-Version" value="${library-version}"/> - </manifest> - <fileset dir="${output}"> - <exclude name="com/drew/tools" /> - <exclude name="com/drew/tools/*.*" /> - </fileset> - <file file="LICENSE-2.0.txt" /> - <file file="README.txt" /> - </jar> - <zip file="${bin-zip}" comment="Metadata Extractor ${library-version} - https://drewnoakes.com/code/exif/"> - <file file="${bin-jar}" /> - <file file="${lib-xmp}" /> - <file file="LICENSE-2.0.txt" /> - <file file="README.txt" /> - </zip> - <delete file="${bin-jar}" /> - </target> - - <target name="dist-source" depends="clean, compile, test" description="generate source distribution"> - <jar destfile="${dist}/metadata-extractor-${library-version}-src.jar" update="false"> - <fileset dir="${src}"> - <include name="**/*.*" /> - <exclude name="com/drew/tools" /> - <exclude name="com/drew/tools/*.*" /> - </fileset> - <fileset dir="."> - <include name="LICENSE-2.0.txt" /> - <include name="README.txt" /> - </fileset> - </jar> - </target> - - <target name="javadoc" description="generate javadoc documentation"> - <delete verbose="${verbose}" dir="${javadoc}" /> - <javadoc - destdir="${javadoc}" - defaultexcludes="yes" - author="true" - version="true" - use="true" - access="protected" - windowtitle="metadata-extractor - Javadoc - Extracts Exif, IPTC, XMP, ICC and other metadata from image files" - failonerror="true"> - <arg value="-notimestamp" /> - <!-- be sure to only use single quotes in the CDATA sections below --> - <!-- TODO include <link rel='shortcut icon' href='https://raw.githubusercontent.com/drewnoakes/metadata-extractor/master/Resources/metadata-extractor.ico' /> --> - <header><![CDATA[<a href='https://drewnoakes.com/code/exif/' title='Go to the project home page.'><img src='https://raw.githubusercontent.com/drewnoakes/metadata-extractor/master/Resources/metadata-extractor-logo-131x30.png' border="0" alt='Metadata Extractor Logo'></a>]]></header> - <bottom><![CDATA[<i>Copyright © 2002-2015 Drew Noakes. All Rights Reserved.</i> -<script src='http://www.google-analytics.com/urchin.js' type='text/javascript'></script> -<script type='text/javascript'> -_uacct = 'UA-936661-1'; -urchinTracker(); -</script>]]></bottom> - - <!-- Only build Java --> - <packageset dir="${src}" defaultexcludes="yes"> - <include name="com/**"/> - <exclude name="com/drew/tools/**"/> - </packageset> - - <classpath> - <fileset dir="."> - <include name="${lib-xmp}"/> - </fileset> - </classpath> - - </javadoc> - <copy file="Resources/javadoc-stylesheet.css" tofile="${javadoc}/stylesheet.css" overwrite="yes" /> - </target> - - <target name="all" depends="dist-all, javadoc, process-sample-files" description="prepare source and binary distributions, and javadoc"/> - - <target name="dist-all" depends="dist-source, dist-binaries" description="prepare source and binary distributions"/> - -</project> diff --git a/pom.xml b/pom.xml index bb89f0e..da678c6 100644 --- a/pom.xml +++ b/pom.xml @@ -14,19 +14,19 @@ <groupId>com.drewnoakes</groupId> <artifactId>metadata-extractor</artifactId> - <version>2.7.2</version> + <version>2.10.1</version> <packaging>jar</packaging> <name>${project.groupId}:${project.artifactId}</name> <description>Java library for extracting EXIF, IPTC, XMP, ICC and other metadata from image files.</description> - + <url>https://drewnoakes.com/code/exif/</url> - + <issueManagement> <system>GitHub Issues</system> <url>https://github.com/drewnoakes/metadata-extractor/issues</url> </issueManagement> - + <mailingLists> <mailingList> <name>Announce mailing list</name> @@ -36,10 +36,6 @@ <name>Development mailing list</name> <archive>http://groups.google.com/group/metadata-extractor-dev</archive> </mailingList> - <mailingList> - <name>Changes mailing list</name> - <archive>http://groups.google.com/group/metadata-extractor-changes</archive> - </mailingList> </mailingLists> <licenses> @@ -72,16 +68,28 @@ <dependency> <groupId>com.adobe.xmp</groupId> <artifactId>xmpcore</artifactId> - <version>5.1.2</version> + <version>5.1.3</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> - <version>4.11</version> + <version>4.12</version> <scope>test</scope> </dependency> </dependencies> + <profiles> + <profile> + <id>java8-doclint-disabled</id> + <activation> + <jdk>[1.8,)</jdk> + </activation> + <properties> + <javadoc.opts>-Xdoclint:none</javadoc.opts> + </properties> + </profile> + </profiles> + <build> <directory>Output/maven</directory> <outputDirectory>Output/maven/classes</outputDirectory> @@ -96,7 +104,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> - <version>2.4</version> + <version>3.0.1</version> <executions> <execution> <id>attach-sources</id> @@ -109,20 +117,32 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> - <version>3.2</version> + <version>3.6.1</version> + <configuration> + <source>1.6</source> + <target>1.6</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>2.19.1</version> <configuration> - <source>1.5</source> - <target>1.5</target> + <includes> + <include>**/*Test*.java</include> + </includes> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> - <version>2.4</version> + <version>3.0.2</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> + <mainClass>com.drew.imaging.ImageMetadataReader</mainClass> + <addDefaultImplementationEntries>true</addDefaultImplementationEntries> </manifest> <manifestEntries> <Implementation-Title>metadata-extractor</Implementation-Title> @@ -135,15 +155,18 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-javadoc-plugin</artifactId> - <version>2.10.1</version> + <version>2.10.4</version> <configuration> - <!--<additionalparam>-Xdoclint:none</additionalparam>--> + <additionalparam>${javadoc.opts}</additionalparam> <stylesheetfile>${basedir}/src/main/javadoc/stylesheet.css</stylesheetfile> <show>public</show> - <windowtitle>metadata-extractor - Javadoc - Extracts Exif, IPTC, XMP, ICC and other metadata from image files</windowtitle> + <windowtitle>metadata-extractor - Javadoc - Extracts Exif, IPTC, XMP, ICC and other metadata from + image files + </windowtitle> <notimestamp>true</notimestamp> - <header><![CDATA[<a href='https://drewnoakes.com/code/exif/' title='Go to the project home page.'><img src='https://raw.githubusercontent.com/drewnoakes/metadata-extractor/master/Resources/metadata-extractor-logo-131x30.png' border="0" alt='Metadata Extractor Logo'></a>]]></header> - <bottom><![CDATA[<i>Copyright © 2002-2015 Drew Noakes. All Rights Reserved.</i> + <header> + <![CDATA[<a href='https://drewnoakes.com/code/exif/' title='Go to the project home page.'><img src='https://raw.githubusercontent.com/drewnoakes/metadata-extractor/master/Resources/metadata-extractor-logo-131x30.png' border="0" alt='Metadata Extractor Logo'></a>]]></header> + <bottom><![CDATA[<i>Copyright © 2002-2017 Drew Noakes. All Rights Reserved.</i> <script src='http://www.google-analytics.com/urchin.js' type='text/javascript'></script> <script type='text/javascript'> _uacct = 'UA-936661-1'; @@ -164,7 +187,7 @@ urchinTracker(); <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-gpg-plugin</artifactId> - <version>1.5</version> + <version>1.6</version> <executions> <execution> <id>sign-artifacts</id> @@ -178,7 +201,7 @@ urchinTracker(); <plugin> <groupId>org.sonatype.plugins</groupId> <artifactId>nexus-staging-maven-plugin</artifactId> - <version>1.6.3</version> + <version>1.6.7</version> <extensions>true</extensions> <configuration> <serverId>ossrh</serverId> -- GitLab