| 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 |
|
|---|
| 22 | package com.drew.metadata.icc;
|
|---|
| 23 |
|
|---|
| 24 | import com.drew.lang.ByteArrayReader;
|
|---|
| 25 | import com.drew.lang.RandomAccessReader;
|
|---|
| 26 | import com.drew.lang.annotations.NotNull;
|
|---|
| 27 | import com.drew.lang.annotations.Nullable;
|
|---|
| 28 | import com.drew.metadata.TagDescriptor;
|
|---|
| 29 |
|
|---|
| 30 | import java.io.IOException;
|
|---|
| 31 | import java.io.UnsupportedEncodingException;
|
|---|
| 32 | import java.text.DecimalFormat;
|
|---|
| 33 |
|
|---|
| 34 | import static com.drew.metadata.icc.IccDirectory.*;
|
|---|
| 35 |
|
|---|
| 36 | /**
|
|---|
| 37 | * @author Yuri Binev
|
|---|
| 38 | * @author Drew Noakes https://drewnoakes.com
|
|---|
| 39 | */
|
|---|
| 40 | @SuppressWarnings("WeakerAccess")
|
|---|
| 41 | public class IccDescriptor extends TagDescriptor<IccDirectory>
|
|---|
| 42 | {
|
|---|
| 43 | public IccDescriptor(@NotNull IccDirectory directory)
|
|---|
| 44 | {
|
|---|
| 45 | super(directory);
|
|---|
| 46 | }
|
|---|
| 47 |
|
|---|
| 48 | @Override
|
|---|
| 49 | public String getDescription(int tagType)
|
|---|
| 50 | {
|
|---|
| 51 | switch (tagType) {
|
|---|
| 52 | case TAG_PROFILE_VERSION:
|
|---|
| 53 | return getProfileVersionDescription();
|
|---|
| 54 | case TAG_PROFILE_CLASS:
|
|---|
| 55 | return getProfileClassDescription();
|
|---|
| 56 | case TAG_PLATFORM:
|
|---|
| 57 | return getPlatformDescription();
|
|---|
| 58 | case TAG_RENDERING_INTENT:
|
|---|
| 59 | return getRenderingIntentDescription();
|
|---|
| 60 | }
|
|---|
| 61 |
|
|---|
| 62 | if (tagType > 0x20202020 && tagType < 0x7a7a7a7a)
|
|---|
| 63 | return getTagDataString(tagType);
|
|---|
| 64 |
|
|---|
| 65 | return super.getDescription(tagType);
|
|---|
| 66 | }
|
|---|
| 67 |
|
|---|
| 68 | private static final int ICC_TAG_TYPE_TEXT = 0x74657874;
|
|---|
| 69 | private static final int ICC_TAG_TYPE_DESC = 0x64657363;
|
|---|
| 70 | private static final int ICC_TAG_TYPE_SIG = 0x73696720;
|
|---|
| 71 | private static final int ICC_TAG_TYPE_MEAS = 0x6D656173;
|
|---|
| 72 | private static final int ICC_TAG_TYPE_XYZ_ARRAY = 0x58595A20;
|
|---|
| 73 | private static final int ICC_TAG_TYPE_MLUC = 0x6d6c7563;
|
|---|
| 74 | private static final int ICC_TAG_TYPE_CURV = 0x63757276;
|
|---|
| 75 |
|
|---|
| 76 | @Nullable
|
|---|
| 77 | private String getTagDataString(int tagType)
|
|---|
| 78 | {
|
|---|
| 79 | try {
|
|---|
| 80 | byte[] bytes = _directory.getByteArray(tagType);
|
|---|
| 81 | if (bytes == null)
|
|---|
| 82 | return _directory.getString(tagType);
|
|---|
| 83 | RandomAccessReader reader = new ByteArrayReader(bytes);
|
|---|
| 84 | int iccTagType = reader.getInt32(0);
|
|---|
| 85 | switch (iccTagType) {
|
|---|
| 86 | case ICC_TAG_TYPE_TEXT:
|
|---|
| 87 | try {
|
|---|
| 88 | return new String(bytes, 8, bytes.length - 8 - 1, "ASCII");
|
|---|
| 89 | } catch (UnsupportedEncodingException ex) {
|
|---|
| 90 | return new String(bytes, 8, bytes.length - 8 - 1);
|
|---|
| 91 | }
|
|---|
| 92 | case ICC_TAG_TYPE_DESC:
|
|---|
| 93 | int stringLength = reader.getInt32(8);
|
|---|
| 94 | return new String(bytes, 12, stringLength - 1);
|
|---|
| 95 | case ICC_TAG_TYPE_SIG:
|
|---|
| 96 | return IccReader.getStringFromInt32(reader.getInt32(8));
|
|---|
| 97 | case ICC_TAG_TYPE_MEAS: {
|
|---|
| 98 | int observerType = reader.getInt32(8);
|
|---|
| 99 | float x = reader.getS15Fixed16(12);
|
|---|
| 100 | float y = reader.getS15Fixed16(16);
|
|---|
| 101 | float z = reader.getS15Fixed16(20);
|
|---|
| 102 | int geometryType = reader.getInt32(24);
|
|---|
| 103 | float flare = reader.getS15Fixed16(28);
|
|---|
| 104 | int illuminantType = reader.getInt32(32);
|
|---|
| 105 | String observerString;
|
|---|
| 106 | switch (observerType) {
|
|---|
| 107 | case 0:
|
|---|
| 108 | observerString = "Unknown";
|
|---|
| 109 | break;
|
|---|
| 110 | case 1:
|
|---|
| 111 | observerString = "1931 2\u00B0";
|
|---|
| 112 | break;
|
|---|
| 113 | case 2:
|
|---|
| 114 | observerString = "1964 10\u00B0";
|
|---|
| 115 | break;
|
|---|
| 116 | default:
|
|---|
| 117 | observerString = String.format("Unknown %d", observerType);
|
|---|
| 118 | }
|
|---|
| 119 | String geometryString;
|
|---|
| 120 | switch (geometryType) {
|
|---|
| 121 | case 0:
|
|---|
| 122 | geometryString = "Unknown";
|
|---|
| 123 | break;
|
|---|
| 124 | case 1:
|
|---|
| 125 | geometryString = "0/45 or 45/0";
|
|---|
| 126 | break;
|
|---|
| 127 | case 2:
|
|---|
| 128 | geometryString = "0/d or d/0";
|
|---|
| 129 | break;
|
|---|
| 130 | default:
|
|---|
| 131 | geometryString = String.format("Unknown %d", observerType);
|
|---|
| 132 | }
|
|---|
| 133 | String illuminantString;
|
|---|
| 134 | switch (illuminantType) {
|
|---|
| 135 | case 0:
|
|---|
| 136 | illuminantString = "unknown";
|
|---|
| 137 | break;
|
|---|
| 138 | case 1:
|
|---|
| 139 | illuminantString = "D50";
|
|---|
| 140 | break;
|
|---|
| 141 | case 2:
|
|---|
| 142 | illuminantString = "D65";
|
|---|
| 143 | break;
|
|---|
| 144 | case 3:
|
|---|
| 145 | illuminantString = "D93";
|
|---|
| 146 | break;
|
|---|
| 147 | case 4:
|
|---|
| 148 | illuminantString = "F2";
|
|---|
| 149 | break;
|
|---|
| 150 | case 5:
|
|---|
| 151 | illuminantString = "D55";
|
|---|
| 152 | break;
|
|---|
| 153 | case 6:
|
|---|
| 154 | illuminantString = "A";
|
|---|
| 155 | break;
|
|---|
| 156 | case 7:
|
|---|
| 157 | illuminantString = "Equi-Power (E)";
|
|---|
| 158 | break;
|
|---|
| 159 | case 8:
|
|---|
| 160 | illuminantString = "F8";
|
|---|
| 161 | break;
|
|---|
| 162 | default:
|
|---|
| 163 | illuminantString = String.format("Unknown %d", illuminantType);
|
|---|
| 164 | break;
|
|---|
| 165 | }
|
|---|
| 166 | DecimalFormat format = new DecimalFormat("0.###");
|
|---|
| 167 | return String.format("%s Observer, Backing (%s, %s, %s), Geometry %s, Flare %d%%, Illuminant %s",
|
|---|
| 168 | observerString, format.format(x), format.format(y), format.format(z), geometryString, Math.round(flare * 100), illuminantString);
|
|---|
| 169 | }
|
|---|
| 170 | case ICC_TAG_TYPE_XYZ_ARRAY: {
|
|---|
| 171 | StringBuilder res = new StringBuilder();
|
|---|
| 172 | DecimalFormat format = new DecimalFormat("0.####");
|
|---|
| 173 | int count = (bytes.length - 8) / 12;
|
|---|
| 174 | for (int i = 0; i < count; i++) {
|
|---|
| 175 | float x = reader.getS15Fixed16(8 + i * 12);
|
|---|
| 176 | float y = reader.getS15Fixed16(8 + i * 12 + 4);
|
|---|
| 177 | float z = reader.getS15Fixed16(8 + i * 12 + 8);
|
|---|
| 178 | if (i > 0)
|
|---|
| 179 | res.append(", ");
|
|---|
| 180 | res.append("(").append(format.format(x)).append(", ").append(format.format(y)).append(", ").append(format.format(z)).append(")");
|
|---|
| 181 | }
|
|---|
| 182 | return res.toString();
|
|---|
| 183 | }
|
|---|
| 184 | case ICC_TAG_TYPE_MLUC: {
|
|---|
| 185 | int int1 = reader.getInt32(8);
|
|---|
| 186 | StringBuilder res = new StringBuilder();
|
|---|
| 187 | res.append(int1);
|
|---|
| 188 | //int int2 = reader.getInt32(12);
|
|---|
| 189 | //System.err.format("int1: %d, int2: %d\n", int1, int2);
|
|---|
| 190 | for (int i = 0; i < int1; i++) {
|
|---|
| 191 | String str = IccReader.getStringFromInt32(reader.getInt32(16 + i * 12));
|
|---|
| 192 | int len = reader.getInt32(16 + i * 12 + 4);
|
|---|
| 193 | int ofs = reader.getInt32(16 + i * 12 + 8);
|
|---|
| 194 | String name;
|
|---|
| 195 | try {
|
|---|
| 196 | name = new String(bytes, ofs, len, "UTF-16BE");
|
|---|
| 197 | } catch (UnsupportedEncodingException ex) {
|
|---|
| 198 | name = new String(bytes, ofs, len);
|
|---|
| 199 | }
|
|---|
| 200 | res.append(" ").append(str).append("(").append(name).append(")");
|
|---|
| 201 | //System.err.format("% 3d: %s, len: %d, ofs: %d, \"%s\"\n", i, str, len,ofs,name);
|
|---|
| 202 | }
|
|---|
| 203 | return res.toString();
|
|---|
| 204 | }
|
|---|
| 205 | case ICC_TAG_TYPE_CURV: {
|
|---|
| 206 | int num = reader.getInt32(8);
|
|---|
| 207 | StringBuilder res = new StringBuilder();
|
|---|
| 208 | for (int i = 0; i < num; i++) {
|
|---|
| 209 | if (i != 0)
|
|---|
| 210 | res.append(", ");
|
|---|
| 211 | res.append(formatDoubleAsString(((float)reader.getUInt16(12 + i * 2)) / 65535.0, 7, false));
|
|---|
| 212 | //res+=String.format("%1.7g",Math.round(((float)iccReader.getInt16(b,12+i*2))/0.065535)/1E7);
|
|---|
| 213 | }
|
|---|
| 214 | return res.toString();
|
|---|
| 215 | }
|
|---|
| 216 | default:
|
|---|
| 217 | return String.format("%s (0x%08X): %d bytes", IccReader.getStringFromInt32(iccTagType), iccTagType, bytes.length);
|
|---|
| 218 | }
|
|---|
| 219 | } catch (IOException e) {
|
|---|
| 220 | // TODO decode these values during IccReader.extract so we can report any errors at that time
|
|---|
| 221 | // It is convention to return null if a description cannot be formulated.
|
|---|
| 222 | // If an error is to be reported, it should be done during the extraction process.
|
|---|
| 223 | return null;
|
|---|
| 224 | }
|
|---|
| 225 | }
|
|---|
| 226 |
|
|---|
| 227 | @NotNull
|
|---|
| 228 | public static String formatDoubleAsString(double value, int precision, boolean zeroes)
|
|---|
| 229 | {
|
|---|
| 230 | if (precision < 1)
|
|---|
| 231 | return "" + Math.round(value);
|
|---|
| 232 | long intPart = Math.abs((long)value);
|
|---|
| 233 | long rest = (int)Math.round((Math.abs(value) - intPart) * Math.pow(10, precision));
|
|---|
| 234 | long restKept = rest;
|
|---|
| 235 | String res = "";
|
|---|
| 236 | byte cour;
|
|---|
| 237 | for (int i = precision; i > 0; i--) {
|
|---|
| 238 | cour = (byte)(Math.abs(rest % 10));
|
|---|
| 239 | rest /= 10;
|
|---|
| 240 | if (res.length() > 0 || zeroes || cour != 0 || i == 1)
|
|---|
| 241 | res = cour + res;
|
|---|
| 242 | }
|
|---|
| 243 | intPart += rest;
|
|---|
| 244 | boolean isNegative = ((value < 0) && (intPart != 0 || restKept != 0));
|
|---|
| 245 | return (isNegative ? "-" : "") + intPart + "." + res;
|
|---|
| 246 | }
|
|---|
| 247 |
|
|---|
| 248 | @Nullable
|
|---|
| 249 | private String getRenderingIntentDescription()
|
|---|
| 250 | {
|
|---|
| 251 | return getIndexedDescription(TAG_RENDERING_INTENT,
|
|---|
| 252 | "Perceptual",
|
|---|
| 253 | "Media-Relative Colorimetric",
|
|---|
| 254 | "Saturation",
|
|---|
| 255 | "ICC-Absolute Colorimetric");
|
|---|
| 256 | }
|
|---|
| 257 |
|
|---|
| 258 | @Nullable
|
|---|
| 259 | private String getPlatformDescription()
|
|---|
| 260 | {
|
|---|
| 261 | String str = _directory.getString(TAG_PLATFORM);
|
|---|
| 262 | if (str==null)
|
|---|
| 263 | return null;
|
|---|
| 264 | // Because Java doesn't allow switching on string values, create an integer from the first four chars
|
|---|
| 265 | // and switch on that instead.
|
|---|
| 266 | int i;
|
|---|
| 267 | try {
|
|---|
| 268 | i = getInt32FromString(str);
|
|---|
| 269 | } catch (IOException e) {
|
|---|
| 270 | return str;
|
|---|
| 271 | }
|
|---|
| 272 | switch (i) {
|
|---|
| 273 | case 0x4150504C: // "APPL"
|
|---|
| 274 | return "Apple Computer, Inc.";
|
|---|
| 275 | case 0x4D534654: // "MSFT"
|
|---|
| 276 | return "Microsoft Corporation";
|
|---|
| 277 | case 0x53474920:
|
|---|
| 278 | return "Silicon Graphics, Inc.";
|
|---|
| 279 | case 0x53554E57:
|
|---|
| 280 | return "Sun Microsystems, Inc.";
|
|---|
| 281 | case 0x54474E54:
|
|---|
| 282 | return "Taligent, Inc.";
|
|---|
| 283 | default:
|
|---|
| 284 | return String.format("Unknown (%s)", str);
|
|---|
| 285 | }
|
|---|
| 286 | }
|
|---|
| 287 |
|
|---|
| 288 | @Nullable
|
|---|
| 289 | private String getProfileClassDescription()
|
|---|
| 290 | {
|
|---|
| 291 | String str = _directory.getString(TAG_PROFILE_CLASS);
|
|---|
| 292 | if (str==null)
|
|---|
| 293 | return null;
|
|---|
| 294 | // Because Java doesn't allow switching on string values, create an integer from the first four chars
|
|---|
| 295 | // and switch on that instead.
|
|---|
| 296 | int i;
|
|---|
| 297 | try {
|
|---|
| 298 | i = getInt32FromString(str);
|
|---|
| 299 | } catch (IOException e) {
|
|---|
| 300 | return str;
|
|---|
| 301 | }
|
|---|
| 302 | switch (i) {
|
|---|
| 303 | case 0x73636E72:
|
|---|
| 304 | return "Input Device";
|
|---|
| 305 | case 0x6D6E7472: // mntr
|
|---|
| 306 | return "Display Device";
|
|---|
| 307 | case 0x70727472:
|
|---|
| 308 | return "Output Device";
|
|---|
| 309 | case 0x6C696E6B:
|
|---|
| 310 | return "DeviceLink";
|
|---|
| 311 | case 0x73706163:
|
|---|
| 312 | return "ColorSpace Conversion";
|
|---|
| 313 | case 0x61627374:
|
|---|
| 314 | return "Abstract";
|
|---|
| 315 | case 0x6E6D636C:
|
|---|
| 316 | return "Named Color";
|
|---|
| 317 | default:
|
|---|
| 318 | return String.format("Unknown (%s)", str);
|
|---|
| 319 | }
|
|---|
| 320 | }
|
|---|
| 321 |
|
|---|
| 322 | @Nullable
|
|---|
| 323 | private String getProfileVersionDescription()
|
|---|
| 324 | {
|
|---|
| 325 | Integer value = _directory.getInteger(TAG_PROFILE_VERSION);
|
|---|
| 326 |
|
|---|
| 327 | if (value == null)
|
|---|
| 328 | return null;
|
|---|
| 329 |
|
|---|
| 330 | int m = (value & 0xFF000000) >> 24;
|
|---|
| 331 | int r = (value & 0x00F00000) >> 20;
|
|---|
| 332 | int R = (value & 0x000F0000) >> 16;
|
|---|
| 333 |
|
|---|
| 334 | return String.format("%d.%d.%d", m, r, R);
|
|---|
| 335 | }
|
|---|
| 336 |
|
|---|
| 337 | private static int getInt32FromString(@NotNull String string) throws IOException
|
|---|
| 338 | {
|
|---|
| 339 | byte[] bytes = string.getBytes();
|
|---|
| 340 | return new ByteArrayReader(bytes).getInt32(0);
|
|---|
| 341 | }
|
|---|
| 342 | }
|
|---|