| 1 | /*
|
|---|
| 2 | * Copyright 2002-2019 Drew Noakes and contributors
|
|---|
| 3 | *
|
|---|
| 4 | * Licensed under the Apache License, Version 2.0 (the "License");
|
|---|
| 5 | * you may not use this file except in compliance with the License.
|
|---|
| 6 | * You may obtain a copy of the License at
|
|---|
| 7 | *
|
|---|
| 8 | * http://www.apache.org/licenses/LICENSE-2.0
|
|---|
| 9 | *
|
|---|
| 10 | * Unless required by applicable law or agreed to in writing, software
|
|---|
| 11 | * distributed under the License is distributed on an "AS IS" BASIS,
|
|---|
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|---|
| 13 | * See the License for the specific language governing permissions and
|
|---|
| 14 | * limitations under the License.
|
|---|
| 15 | *
|
|---|
| 16 | * More information about this project is available at:
|
|---|
| 17 | *
|
|---|
| 18 | * https://drewnoakes.com/code/exif/
|
|---|
| 19 | * https://github.com/drewnoakes/metadata-extractor
|
|---|
| 20 | */
|
|---|
| 21 | package com.drew.metadata.icc;
|
|---|
| 22 |
|
|---|
| 23 | import com.drew.imaging.jpeg.JpegSegmentMetadataReader;
|
|---|
| 24 | import com.drew.imaging.jpeg.JpegSegmentType;
|
|---|
| 25 | import com.drew.lang.ByteArrayReader;
|
|---|
| 26 | import com.drew.lang.DateUtil;
|
|---|
| 27 | import com.drew.lang.RandomAccessReader;
|
|---|
| 28 | import com.drew.lang.annotations.NotNull;
|
|---|
| 29 | import com.drew.lang.annotations.Nullable;
|
|---|
| 30 | import com.drew.metadata.Directory;
|
|---|
| 31 | import com.drew.metadata.Metadata;
|
|---|
| 32 | import com.drew.metadata.MetadataReader;
|
|---|
| 33 |
|
|---|
| 34 | import java.io.IOException;
|
|---|
| 35 | import java.util.Collections;
|
|---|
| 36 |
|
|---|
| 37 | /**
|
|---|
| 38 | * Reads an ICC profile.
|
|---|
| 39 | * <p>
|
|---|
| 40 | * More information about ICC:
|
|---|
| 41 | * <ul>
|
|---|
| 42 | * <li>http://en.wikipedia.org/wiki/ICC_profile</li>
|
|---|
| 43 | * <li>http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/ICC_Profile.html</li>
|
|---|
| 44 | * <li>https://developer.apple.com/library/mac/samplecode/ImageApp/Listings/ICC_h.html</li>
|
|---|
| 45 | * </ul>
|
|---|
| 46 | *
|
|---|
| 47 | * @author Yuri Binev
|
|---|
| 48 | * @author Drew Noakes https://drewnoakes.com
|
|---|
| 49 | */
|
|---|
| 50 | public class IccReader implements JpegSegmentMetadataReader, MetadataReader
|
|---|
| 51 | {
|
|---|
| 52 | public static final String JPEG_SEGMENT_PREAMBLE = "ICC_PROFILE";
|
|---|
| 53 |
|
|---|
| 54 | @NotNull
|
|---|
| 55 | public Iterable<JpegSegmentType> getSegmentTypes()
|
|---|
| 56 | {
|
|---|
| 57 | return Collections.singletonList(JpegSegmentType.APP2);
|
|---|
| 58 | }
|
|---|
| 59 |
|
|---|
| 60 | public void readJpegSegments(@NotNull Iterable<byte[]> segments, @NotNull Metadata metadata, @NotNull JpegSegmentType segmentType)
|
|---|
| 61 | {
|
|---|
| 62 | final int preambleLength = JPEG_SEGMENT_PREAMBLE.length();
|
|---|
| 63 |
|
|---|
| 64 | // ICC data can be spread across multiple JPEG segments.
|
|---|
| 65 | // We concat them together in this buffer for later processing.
|
|---|
| 66 | byte[] buffer = null;
|
|---|
| 67 |
|
|---|
| 68 | for (byte[] segmentBytes : segments) {
|
|---|
| 69 | // Skip any segments that do not contain the required preamble
|
|---|
| 70 | if (segmentBytes.length < preambleLength || !JPEG_SEGMENT_PREAMBLE.equalsIgnoreCase(new String(segmentBytes, 0, preambleLength)))
|
|---|
| 71 | continue;
|
|---|
| 72 |
|
|---|
| 73 | // NOTE we ignore three bytes here -- are they useful for anything?
|
|---|
| 74 |
|
|---|
| 75 | // Grow the buffer
|
|---|
| 76 | if (buffer == null) {
|
|---|
| 77 | buffer = new byte[segmentBytes.length - 14];
|
|---|
| 78 | // skip the first 14 bytes
|
|---|
| 79 | System.arraycopy(segmentBytes, 14, buffer, 0, segmentBytes.length - 14);
|
|---|
| 80 | } else {
|
|---|
| 81 | byte[] newBuffer = new byte[buffer.length + segmentBytes.length - 14];
|
|---|
| 82 | System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
|
|---|
| 83 | System.arraycopy(segmentBytes, 14, newBuffer, buffer.length, segmentBytes.length - 14);
|
|---|
| 84 | buffer = newBuffer;
|
|---|
| 85 | }
|
|---|
| 86 | }
|
|---|
| 87 |
|
|---|
| 88 | if (buffer != null)
|
|---|
| 89 | extract(new ByteArrayReader(buffer), metadata);
|
|---|
| 90 | }
|
|---|
| 91 |
|
|---|
| 92 | public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata)
|
|---|
| 93 | {
|
|---|
| 94 | extract(reader, metadata, null);
|
|---|
| 95 | }
|
|---|
| 96 |
|
|---|
| 97 | public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata, @Nullable Directory parentDirectory)
|
|---|
| 98 | {
|
|---|
| 99 | // TODO review whether the 'tagPtr' values below really do require RandomAccessReader or whether SequentialReader may be used instead
|
|---|
| 100 |
|
|---|
| 101 | IccDirectory directory = new IccDirectory();
|
|---|
| 102 |
|
|---|
| 103 | if (parentDirectory != null)
|
|---|
| 104 | directory.setParent(parentDirectory);
|
|---|
| 105 |
|
|---|
| 106 | try {
|
|---|
| 107 | int profileByteCount = reader.getInt32(IccDirectory.TAG_PROFILE_BYTE_COUNT);
|
|---|
| 108 | directory.setInt(IccDirectory.TAG_PROFILE_BYTE_COUNT, profileByteCount);
|
|---|
| 109 |
|
|---|
| 110 | // For these tags, the int value of the tag is in fact it's offset within the buffer.
|
|---|
| 111 | set4ByteString(directory, IccDirectory.TAG_CMM_TYPE, reader);
|
|---|
| 112 | setInt32(directory, IccDirectory.TAG_PROFILE_VERSION, reader);
|
|---|
| 113 | set4ByteString(directory, IccDirectory.TAG_PROFILE_CLASS, reader);
|
|---|
| 114 | set4ByteString(directory, IccDirectory.TAG_COLOR_SPACE, reader);
|
|---|
| 115 | set4ByteString(directory, IccDirectory.TAG_PROFILE_CONNECTION_SPACE, reader);
|
|---|
| 116 | setDate(directory, IccDirectory.TAG_PROFILE_DATETIME, reader);
|
|---|
| 117 | set4ByteString(directory, IccDirectory.TAG_SIGNATURE, reader);
|
|---|
| 118 | set4ByteString(directory, IccDirectory.TAG_PLATFORM, reader);
|
|---|
| 119 | setInt32(directory, IccDirectory.TAG_CMM_FLAGS, reader);
|
|---|
| 120 | set4ByteString(directory, IccDirectory.TAG_DEVICE_MAKE, reader);
|
|---|
| 121 |
|
|---|
| 122 | int temp = reader.getInt32(IccDirectory.TAG_DEVICE_MODEL);
|
|---|
| 123 | if (temp != 0) {
|
|---|
| 124 | if (temp <= 0x20202020) {
|
|---|
| 125 | directory.setInt(IccDirectory.TAG_DEVICE_MODEL, temp);
|
|---|
| 126 | } else {
|
|---|
| 127 | directory.setString(IccDirectory.TAG_DEVICE_MODEL, getStringFromInt32(temp));
|
|---|
| 128 | }
|
|---|
| 129 | }
|
|---|
| 130 |
|
|---|
| 131 | setInt32(directory, IccDirectory.TAG_RENDERING_INTENT, reader);
|
|---|
| 132 | setInt64(directory, IccDirectory.TAG_DEVICE_ATTR, reader);
|
|---|
| 133 |
|
|---|
| 134 | float[] xyz = new float[]{
|
|---|
| 135 | reader.getS15Fixed16(IccDirectory.TAG_XYZ_VALUES),
|
|---|
| 136 | reader.getS15Fixed16(IccDirectory.TAG_XYZ_VALUES + 4),
|
|---|
| 137 | reader.getS15Fixed16(IccDirectory.TAG_XYZ_VALUES + 8)
|
|---|
| 138 | };
|
|---|
| 139 | directory.setObject(IccDirectory.TAG_XYZ_VALUES, xyz);
|
|---|
| 140 |
|
|---|
| 141 | // Process 'ICC tags'
|
|---|
| 142 | int tagCount = reader.getInt32(IccDirectory.TAG_TAG_COUNT);
|
|---|
| 143 | directory.setInt(IccDirectory.TAG_TAG_COUNT, tagCount);
|
|---|
| 144 |
|
|---|
| 145 | for (int i = 0; i < tagCount; i++) {
|
|---|
| 146 | int pos = IccDirectory.TAG_TAG_COUNT + 4 + i * 12;
|
|---|
| 147 | int tagType = reader.getInt32(pos);
|
|---|
| 148 | int tagPtr = reader.getInt32(pos + 4);
|
|---|
| 149 | int tagLen = reader.getInt32(pos + 8);
|
|---|
| 150 | byte[] b = reader.getBytes(tagPtr, tagLen);
|
|---|
| 151 | directory.setByteArray(tagType, b);
|
|---|
| 152 | }
|
|---|
| 153 | } catch (IOException ex) {
|
|---|
| 154 | directory.addError("Exception reading ICC profile: " + ex.getMessage());
|
|---|
| 155 | }
|
|---|
| 156 |
|
|---|
| 157 | metadata.addDirectory(directory);
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | private void set4ByteString(@NotNull Directory directory, int tagType, @NotNull RandomAccessReader reader) throws IOException
|
|---|
| 161 | {
|
|---|
| 162 | int i = reader.getInt32(tagType);
|
|---|
| 163 | if (i != 0)
|
|---|
| 164 | directory.setString(tagType, getStringFromInt32(i));
|
|---|
| 165 | }
|
|---|
| 166 |
|
|---|
| 167 | private void setInt32(@NotNull Directory directory, int tagType, @NotNull RandomAccessReader reader) throws IOException
|
|---|
| 168 | {
|
|---|
| 169 | int i = reader.getInt32(tagType);
|
|---|
| 170 | if (i != 0)
|
|---|
| 171 | directory.setInt(tagType, i);
|
|---|
| 172 | }
|
|---|
| 173 |
|
|---|
| 174 | @SuppressWarnings({"SameParameterValue"})
|
|---|
| 175 | private void setInt64(@NotNull Directory directory, int tagType, @NotNull RandomAccessReader reader) throws IOException
|
|---|
| 176 | {
|
|---|
| 177 | long l = reader.getInt64(tagType);
|
|---|
| 178 | if (l != 0)
|
|---|
| 179 | directory.setLong(tagType, l);
|
|---|
| 180 | }
|
|---|
| 181 |
|
|---|
| 182 | @SuppressWarnings({"SameParameterValue", "MagicConstant"})
|
|---|
| 183 | private void setDate(@NotNull final IccDirectory directory, final int tagType, @NotNull RandomAccessReader reader) throws IOException
|
|---|
| 184 | {
|
|---|
| 185 | final int y = reader.getUInt16(tagType);
|
|---|
| 186 | final int m = reader.getUInt16(tagType + 2);
|
|---|
| 187 | final int d = reader.getUInt16(tagType + 4);
|
|---|
| 188 | final int h = reader.getUInt16(tagType + 6);
|
|---|
| 189 | final int M = reader.getUInt16(tagType + 8);
|
|---|
| 190 | final int s = reader.getUInt16(tagType + 10);
|
|---|
| 191 |
|
|---|
| 192 | if (DateUtil.isValidDate(y, m - 1, d) && DateUtil.isValidTime(h, M, s))
|
|---|
| 193 | {
|
|---|
| 194 | String dateString = String.format("%04d:%02d:%02d %02d:%02d:%02d", y, m, d, h, M, s);
|
|---|
| 195 | directory.setString(tagType, dateString);
|
|---|
| 196 | }
|
|---|
| 197 | else
|
|---|
| 198 | {
|
|---|
| 199 | directory.addError(String.format(
|
|---|
| 200 | "ICC data describes an invalid date/time: year=%d month=%d day=%d hour=%d minute=%d second=%d",
|
|---|
| 201 | y, m, d, h, M, s));
|
|---|
| 202 | }
|
|---|
| 203 | }
|
|---|
| 204 |
|
|---|
| 205 | @NotNull
|
|---|
| 206 | public static String getStringFromInt32(int d)
|
|---|
| 207 | {
|
|---|
| 208 | // MSB
|
|---|
| 209 | byte[] b = new byte[] {
|
|---|
| 210 | (byte) ((d & 0xFF000000) >> 24),
|
|---|
| 211 | (byte) ((d & 0x00FF0000) >> 16),
|
|---|
| 212 | (byte) ((d & 0x0000FF00) >> 8),
|
|---|
| 213 | (byte) ((d & 0x000000FF))
|
|---|
| 214 | };
|
|---|
| 215 | return new String(b);
|
|---|
| 216 | }
|
|---|
| 217 | }
|
|---|