This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 0f1fd02de2 TIKA-4902: skip ICC curve and lookup-table tags by default
in image parsers (#3192)
0f1fd02de2 is described below
commit 0f1fd02de281584dfda48cb7224508a486e43685
Author: Tim Allison <[email protected]>
AuthorDate: Fri Sep 18 11:15:16 2026 -0400
TIKA-4902: skip ICC curve and lookup-table tags by default in image parsers
(#3192)
---
CHANGES.txt | 5 ++
docs/modules/ROOT/nav.adoc | 1 +
docs/modules/ROOT/pages/configuration/index.adoc | 2 +
.../pages/configuration/parsers/image-parsers.adoc | 54 ++++++++++++++++
.../org/apache/tika/parser/image/BPGParser.java | 19 +++++-
.../org/apache/tika/parser/image/HeifParser.java | 20 +++++-
.../tika/parser/image/ImageMetadataConfig.java | 58 ++++++++++++++++++
.../tika/parser/image/ImageMetadataExtractor.java | 67 ++++++++++++++++++--
.../org/apache/tika/parser/image/JpegParser.java | 20 +++++-
.../apache/tika/parser/image/RawTiffParser.java | 10 ++-
.../org/apache/tika/parser/image/TiffParser.java | 24 +++++++-
.../org/apache/tika/parser/image/WebPParser.java | 20 +++++-
.../parser/image/ImageMetadataExtractorTest.java | 53 ++++++++++++++++
.../apache/tika/parser/image/JpegParserTest.java | 71 ++++++++++++++++++++++
.../configs/tika-config-icc-curves-on.json | 10 +++
15 files changed, 423 insertions(+), 11 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 60f2f9866a..82516f4584 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,10 @@
Release 4.1.0 - unreleased
+ * ICC tone-curve and lookup-table tags (rTRC and friends) are no longer
+ written to icc:* by default; "jpeg-parser": {"includeIccCurvesAndLuts":
+ true} (likewise tiff-, heif-, bpg-, web-p- and raw-tiff-parser) restores
+ them (TIKA-4902).
+
* Digesting an embedded document no longer makes its source hand out the
bytes twice. (TIKA-4901).
diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc
index 66286e90eb..8f77dade5c 100644
--- a/docs/modules/ROOT/nav.adoc
+++ b/docs/modules/ROOT/nav.adoc
@@ -57,6 +57,7 @@
*** xref:pipes/plugins/json.adoc[JSON]
* xref:configuration/index.adoc[Configuration]
** xref:configuration/parsers/pdf-parser.adoc[PDF Parser]
+** xref:configuration/parsers/image-parsers.adoc[Image Parsers]
** xref:configuration/parsers/tesseract-ocr-parser.adoc[Tesseract OCR]
** xref:configuration/parsers/vlm-parsers.adoc[VLM Parsers (Claude, Gemini,
OpenAI)]
** xref:configuration/parsers/external-parser.adoc[External Parser (ffmpeg,
exiftool, etc.)]
diff --git a/docs/modules/ROOT/pages/configuration/index.adoc
b/docs/modules/ROOT/pages/configuration/index.adoc
index 25db6243b6..541432897f 100644
--- a/docs/modules/ROOT/pages/configuration/index.adoc
+++ b/docs/modules/ROOT/pages/configuration/index.adoc
@@ -294,6 +294,8 @@ Parser configuration:
* xref:configuration/pages.adoc[Pages] — rendering, OCR policy and page
images, for every parser that renders
* xref:configuration/parsers/pdf-parser.adoc[PDFParser] — PDF parsing options
+* xref:configuration/parsers/image-parsers.adoc[Image Parsers] — ICC metadata
options for JPEG,
+ TIFF, HEIF, BPG and WebP
* xref:configuration/parsers/tesseract-ocr-parser.adoc[TesseractOCRParser] —
OCR for image-based
text extraction
* xref:configuration/parsers/tess4j-parser.adoc[Tess4J OCR Parser] —
in-process OCR via tess4j JNA
diff --git a/docs/modules/ROOT/pages/configuration/parsers/image-parsers.adoc
b/docs/modules/ROOT/pages/configuration/parsers/image-parsers.adoc
new file mode 100644
index 0000000000..dac1de871d
--- /dev/null
+++ b/docs/modules/ROOT/pages/configuration/parsers/image-parsers.adoc
@@ -0,0 +1,54 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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.
+//
+
+= Image Parsers Configuration
+
+Options shared by the parsers that read EXIF, IPTC and ICC metadata through
metadata-extractor:
+`jpeg-parser`, `tiff-parser`, `raw-tiff-parser`, `heif-parser`, `bpg-parser`
and `web-p-parser`.
+Each parser takes the options under its own name; `raw-tiff-parser` takes them
alongside its
+preview options.
+
+[cols="1,1,3"]
+|===
+| Option | Default | Description
+
+| `includeIccCurvesAndLuts`
+| `false`
+| Write ICC profile tags whose data is a tone curve or lookup table (`curv`,
`para`, `mft1`,
+ `mft2`, `mAB`, `mBA`; in practice `icc:Red TRC` and friends) to the
metadata. Off, an
+ sRGB-profiled JPEG loses about 30 KB of numbers per file that nothing
queries, and the cost of
+ formatting them. Header and text tags (`icc:Profile Description`, `icc:Color
space`,
+ `icc:Device manufacturer`, ...) are always written.
+|===
+
+[source,json]
+----
+{
+ "parsers": [
+ "default-parser",
+ {
+ "jpeg-parser": {
+ "includeIccCurvesAndLuts": true
+ }
+ }
+ ]
+}
+----
+
+The same option can be set per request on the `ParseContext`, keyed by
+`org.apache.tika.parser.image.ImageMetadataConfig`, which then applies to
every one of these
+parsers.
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/BPGParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/BPGParser.java
index a7439f4256..664a288901 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/BPGParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/BPGParser.java
@@ -28,6 +28,8 @@ import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.exception.TikaException;
import org.apache.tika.exception.TikaMemoryLimitException;
import org.apache.tika.io.EndianUtils;
@@ -58,6 +60,20 @@ public class BPGParser extends AbstractImageParser {
new HashSet<>(
Arrays.asList(MediaType.image("x-bpg"),
MediaType.image("bpg"))));
+ private final ImageMetadataConfig defaultConfig;
+
+ public BPGParser() {
+ this(new ImageMetadataConfig());
+ }
+
+ public BPGParser(ImageMetadataConfig config) {
+ this.defaultConfig = config;
+ }
+
+ public BPGParser(JsonConfig jsonConfig) {
+ this(ConfigDeserializer.buildConfig(jsonConfig,
ImageMetadataConfig.class));
+ }
+
public Set<MediaType> getSupportedTypes(ParseContext context) {
return SUPPORTED_TYPES;
}
@@ -149,7 +165,8 @@ public class BPGParser extends AbstractImageParser {
// Extension Data
if (hasExtensions) {
long extensionsDataSeen = 0;
- ImageMetadataExtractor metadataExtractor = new
ImageMetadataExtractor(metadata);
+ ImageMetadataExtractor metadataExtractor = new
ImageMetadataExtractor(metadata,
+ ImageMetadataConfig.resolve(parseContext, "bpg-parser",
defaultConfig));
while (extensionsDataSeen < extensionDataLength) {
int extensionType = (int) EndianUtils.readUE7(stream);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/HeifParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/HeifParser.java
index e6a1940346..757c1927c6 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/HeifParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/HeifParser.java
@@ -29,6 +29,8 @@ import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TemporaryResources;
import org.apache.tika.io.TikaInputStream;
@@ -45,6 +47,20 @@ public class HeifParser extends AbstractImageParser {
MediaType.image("heic"), MediaType.image("heic-sequence"),
MediaType.image("avif"))));
+ private final ImageMetadataConfig defaultConfig;
+
+ public HeifParser() {
+ this(new ImageMetadataConfig());
+ }
+
+ public HeifParser(ImageMetadataConfig config) {
+ this.defaultConfig = config;
+ }
+
+ public HeifParser(JsonConfig jsonConfig) {
+ this(ConfigDeserializer.buildConfig(jsonConfig,
ImageMetadataConfig.class));
+ }
+
@Override
public Set<MediaType> getSupportedTypes(ParseContext context) {
return SUPPORTED_TYPES;
@@ -66,7 +82,9 @@ public class HeifParser extends AbstractImageParser {
ImageXmp.scanAndExtract(tis, metadata, parseContext);
}
try (InputStream heif = Files.newInputStream(file.toPath())) {
- new ImageMetadataExtractor(metadata).parseHeif(heif);
+ new ImageMetadataExtractor(metadata,
+ ImageMetadataConfig.resolve(parseContext,
"heif-parser", defaultConfig))
+ .parseHeif(heif);
}
} finally {
tmp.dispose();
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataConfig.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataConfig.java
new file mode 100644
index 0000000000..456a097293
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataConfig.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.tika.parser.image;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+import org.apache.tika.config.ParseContextConfig;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * Options for the metadata-extractor pass shared by the JPEG, TIFF, HEIF, BPG
and WebP
+ * parsers. Each parser reads it from its own component name in the config, or
from a
+ * class-keyed instance in the {@link ParseContext}.
+ */
+public class ImageMetadataConfig implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private boolean includeIccCurvesAndLuts = false;
+
+ /**
+ * Whether ICC profile tags whose data is a tone curve or lookup table
(curv, para, mft1,
+ * mft2, mAB, mBA) are written to {@code icc:*}. Off by default: the
values are long
+ * numeric dumps that nobody queries, and formatting them dominates the
cost of reading an
+ * ICC-profiled image. Header and text tags (profile description, color
space, device
+ * class, ...) are always kept.
+ */
+ public boolean isIncludeIccCurvesAndLuts() {
+ return includeIccCurvesAndLuts;
+ }
+
+ public void setIncludeIccCurvesAndLuts(boolean includeIccCurvesAndLuts) {
+ this.includeIccCurvesAndLuts = includeIccCurvesAndLuts;
+ }
+
+ static ImageMetadataConfig resolve(ParseContext context, String configKey,
+ ImageMetadataConfig defaultConfig)
+ throws TikaException, IOException {
+ return ParseContextConfig.getConfig(context, configKey,
ImageMetadataConfig.class,
+ defaultConfig);
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataExtractor.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataExtractor.java
index 41f2b4bca3..6fa3299e3d 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataExtractor.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/ImageMetadataExtractor.java
@@ -106,10 +106,18 @@ public class ImageMetadataExtractor {
private DirectoryHandler[] handlers;
/**
- * @param metadata to extract to, using default directory handlers
+ * @param metadata to extract to, using default directory handlers and
options
*/
public ImageMetadataExtractor(Metadata metadata) {
- this(metadata, new CopyUnknownFieldsHandler(), new
TiffPageNumberHandler(),
+ this(metadata, new ImageMetadataConfig());
+ }
+
+ /**
+ * @param metadata to extract to, using default directory handlers
+ * @param config options for the default handlers
+ */
+ public ImageMetadataExtractor(Metadata metadata, ImageMetadataConfig
config) {
+ this(metadata, new CopyUnknownFieldsHandler(config), new
TiffPageNumberHandler(),
new JpegCommentHandler(), new ExifHandler(), new
DimensionsHandler(),
new GeotagHandler(), new IptcHandler());
}
@@ -347,6 +355,24 @@ public class ImageMetadataExtractor {
* This leads to more predictable behavior than {@link
CopyAllFieldsHandler}.
*/
static class CopyUnknownFieldsHandler implements DirectoryHandler {
+ // ICC tag-table entries carry a 4-byte type signature; these types
are curves and LUTs
+ private static final int ICC_TYPE_CURV = 0x63757276;
+ private static final int ICC_TYPE_PARA = 0x70617261;
+ private static final int ICC_TYPE_MFT1 = 0x6D667431;
+ private static final int ICC_TYPE_MFT2 = 0x6D667432;
+ private static final int ICC_TYPE_MAB = 0x6D414220;
+ private static final int ICC_TYPE_MBA = 0x6D424120;
+
+ private final boolean includeIccCurvesAndLuts;
+
+ CopyUnknownFieldsHandler() {
+ this(new ImageMetadataConfig());
+ }
+
+ CopyUnknownFieldsHandler(ImageMetadataConfig config) {
+ this.includeIccCurvesAndLuts = config.isIncludeIccCurvesAndLuts();
+ }
+
public boolean supports(Class<? extends Directory> directoryType) {
return true;
}
@@ -355,8 +381,17 @@ public class ImageMetadataExtractor {
if (directory.getTags() != null) {
for (Tag tag : directory.getTags()) {
String name = tag.getTagName();
- if (!MetadataFields.isMetadataField(name) &&
tag.getDescription() != null) {
- String value = tag.getDescription().trim();
+ if (MetadataFields.isMetadataField(name)) {
+ continue;
+ }
+ // decided before getDescription(): formatting a curve is
the whole cost
+ if (!includeIccCurvesAndLuts && directory instanceof
IccDirectory &&
+ isIccCurveOrLut(directory, tag.getTagType())) {
+ continue;
+ }
+ String description = tag.getDescription();
+ if (description != null) {
+ String value = description.trim();
if (Boolean.TRUE.toString().equalsIgnoreCase(value)) {
value = Boolean.TRUE.toString();
} else if
(Boolean.FALSE.toString().equalsIgnoreCase(value)) {
@@ -373,6 +408,30 @@ public class ImageMetadataExtractor {
}
}
}
+
+ private static boolean isIccCurveOrLut(Directory directory, int
tagType) {
+ // header fields sit below the printable-ASCII range of tag-table
signatures
+ if (tagType <= 0x20202020 || tagType >= 0x7a7a7a7a) {
+ return false;
+ }
+ byte[] data = directory.getByteArray(tagType);
+ if (data == null || data.length < 4) {
+ return false;
+ }
+ int type = ((data[0] & 0xff) << 24) | ((data[1] & 0xff) << 16) |
+ ((data[2] & 0xff) << 8) | (data[3] & 0xff);
+ switch (type) {
+ case ICC_TYPE_CURV:
+ case ICC_TYPE_PARA:
+ case ICC_TYPE_MFT1:
+ case ICC_TYPE_MFT2:
+ case ICC_TYPE_MAB:
+ case ICC_TYPE_MBA:
+ return true;
+ default:
+ return false;
+ }
+ }
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/JpegParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/JpegParser.java
index bf49883b5a..151fa1e287 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/JpegParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/JpegParser.java
@@ -25,6 +25,8 @@ import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.CacheMemoryBudget;
import org.apache.tika.io.TemporaryResources;
@@ -44,6 +46,20 @@ public class JpegParser extends AbstractImageParser {
private static final Set<MediaType> SUPPORTED_TYPES =
Collections.singleton(MediaType.image("jpeg"));
+ private final ImageMetadataConfig defaultConfig;
+
+ public JpegParser() {
+ this(new ImageMetadataConfig());
+ }
+
+ public JpegParser(ImageMetadataConfig config) {
+ this.defaultConfig = config;
+ }
+
+ public JpegParser(JsonConfig jsonConfig) {
+ this(ConfigDeserializer.buildConfig(jsonConfig,
ImageMetadataConfig.class));
+ }
+
public Set<MediaType> getSupportedTypes(ParseContext context) {
return SUPPORTED_TYPES;
}
@@ -60,7 +76,9 @@ public class JpegParser extends AbstractImageParser {
// XMP first so it is canonical; the metadata-extractor handlers
(IPTC/EXIF) fill gaps.
ImageXmp.extractJpeg(tis, metadata, parseContext);
tis.rewind();
- new ImageMetadataExtractor(metadata).parseJpeg(tis);
+ new ImageMetadataExtractor(metadata,
+ ImageMetadataConfig.resolve(parseContext, "jpeg-parser",
defaultConfig))
+ .parseJpeg(tis);
} finally {
tmp.dispose();
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
index 44ca2202f8..825e2cc2f0 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/RawTiffParser.java
@@ -18,7 +18,6 @@ package org.apache.tika.parser.image;
import java.io.IOException;
import java.io.RandomAccessFile;
-import java.io.Serializable;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.util.ArrayDeque;
@@ -128,6 +127,7 @@ public class RawTiffParser extends TiffParser {
}
public RawTiffParser(RawTiffParserConfig config) {
+ super(config);
this.defaultConfig = config;
}
@@ -165,6 +165,12 @@ public class RawTiffParser extends TiffParser {
RawTiffParserConfig.class, defaultConfig);
}
+ @Override
+ protected ImageMetadataConfig getImageMetadataConfig(ParseContext context)
+ throws TikaException, IOException {
+ return getConfig(context);
+ }
+
private void extractPreviews(TikaInputStream tis, XHTMLContentHandler
xhtml, Metadata metadata,
ParseContext context, RawTiffParserConfig
config)
throws IOException, SAXException {
@@ -531,7 +537,7 @@ public class RawTiffParser extends TiffParser {
/**
* Configuration class for RawTiffParser.
*/
- public static class RawTiffParserConfig implements Serializable {
+ public static class RawTiffParserConfig extends ImageMetadataConfig {
private static final long serialVersionUID = 1990316744955315312L;
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/TiffParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/TiffParser.java
index 4a5cad5f48..056f36dcae 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/TiffParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/TiffParser.java
@@ -25,6 +25,8 @@ import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.CacheMemoryBudget;
import org.apache.tika.io.TemporaryResources;
@@ -44,6 +46,20 @@ public class TiffParser extends AbstractImageParser {
private static final Set<MediaType> SUPPORTED_TYPES =
Collections.singleton(MediaType.image("tiff"));
+ private final ImageMetadataConfig defaultConfig;
+
+ public TiffParser() {
+ this(new ImageMetadataConfig());
+ }
+
+ public TiffParser(ImageMetadataConfig config) {
+ this.defaultConfig = config;
+ }
+
+ public TiffParser(JsonConfig jsonConfig) {
+ this(ConfigDeserializer.buildConfig(jsonConfig,
ImageMetadataConfig.class));
+ }
+
public Set<MediaType> getSupportedTypes(ParseContext context) {
return SUPPORTED_TYPES;
}
@@ -59,9 +75,15 @@ public class TiffParser extends AbstractImageParser {
// XMP first so it is canonical; metadata-extractor (IPTC/EXIF)
fills gaps.
ImageXmp.scanAndExtract(tis, metadata, parseContext);
tis.rewind();
- new ImageMetadataExtractor(metadata).parseTiff(tis);
+ new ImageMetadataExtractor(metadata,
getImageMetadataConfig(parseContext))
+ .parseTiff(tis);
} finally {
tmp.dispose();
}
}
+
+ protected ImageMetadataConfig getImageMetadataConfig(ParseContext context)
+ throws TikaException, IOException {
+ return ImageMetadataConfig.resolve(context, "tiff-parser",
defaultConfig);
+ }
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/WebPParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/WebPParser.java
index 459aa07def..e4890576fa 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/WebPParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/main/java/org/apache/tika/parser/image/WebPParser.java
@@ -24,6 +24,8 @@ import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.apache.tika.annotation.TikaComponent;
+import org.apache.tika.config.ConfigDeserializer;
+import org.apache.tika.config.JsonConfig;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.CacheMemoryBudget;
import org.apache.tika.io.TikaInputStream;
@@ -44,6 +46,20 @@ public class WebPParser implements Parser {
private static final Set<MediaType> SUPPORTED_TYPES =
Collections.singleton(MediaType.image("webp"));
+ private final ImageMetadataConfig defaultConfig;
+
+ public WebPParser() {
+ this(new ImageMetadataConfig());
+ }
+
+ public WebPParser(ImageMetadataConfig config) {
+ this.defaultConfig = config;
+ }
+
+ public WebPParser(JsonConfig jsonConfig) {
+ this(ConfigDeserializer.buildConfig(jsonConfig,
ImageMetadataConfig.class));
+ }
+
public Set<MediaType> getSupportedTypes(ParseContext context) {
return SUPPORTED_TYPES;
}
@@ -54,7 +70,9 @@ public class WebPParser implements Parser {
tis.enableRewind(context.get(CacheMemoryBudget.class));
ImageXmp.extractWebp(tis, metadata, context);
tis.rewind();
- new ImageMetadataExtractor(metadata).parseWebP(tis);
+ new ImageMetadataExtractor(metadata,
+ ImageMetadataConfig.resolve(context, "web-p-parser",
defaultConfig))
+ .parseWebP(tis);
XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata,
context);
xhtml.startDocument();
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/ImageMetadataExtractorTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/ImageMetadataExtractorTest.java
index 4db12e8b71..7bc751c9b5 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/ImageMetadataExtractorTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/ImageMetadataExtractorTest.java
@@ -21,6 +21,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.GregorianCalendar;
import java.util.Iterator;
@@ -35,6 +38,7 @@ import com.drew.metadata.Tag;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.exif.GpsDirectory;
+import com.drew.metadata.icc.IccDirectory;
import com.drew.metadata.jpeg.JpegCommentDirectory;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@@ -159,4 +163,53 @@ public class ImageMetadataExtractorTest {
assertNull(metadata.get(TikaCoreProperties.DESCRIPTION));
}
+ @Test
+ public void testIccCurvesSkippedUnlessConfigured() throws
MetadataException {
+ List<Integer> described = new ArrayList<>();
+ IccDirectory d = new IccDirectory() {
+ @Override
+ public String getDescription(int tagType) {
+ described.add(tagType);
+ return super.getDescription(tagType);
+ }
+ };
+ d.setString(IccDirectory.TAG_COLOR_SPACE, "RGB ");
+ d.setByteArray(IccDirectory.TAG_TAG_desc, iccDesc("sRGB"));
+ d.setByteArray(IccDirectory.TAG_TAG_rTRC, iccCurve(2));
+
+ Metadata metadata = new Metadata();
+ new ImageMetadataExtractor.CopyUnknownFieldsHandler().handle(d,
metadata);
+ assertEquals("RGB", metadata.get(ImageMetadataExtractor.ICC_NS +
"Color space"));
+ assertEquals("sRGB", metadata.get(ImageMetadataExtractor.ICC_NS +
"Profile Description"));
+ assertNull(metadata.get(ImageMetadataExtractor.ICC_NS + "Red TRC"));
+ //the skip happens before the curve is formatted, not after
+ assertFalse(described.contains(IccDirectory.TAG_TAG_rTRC));
+
+ ImageMetadataConfig config = new ImageMetadataConfig();
+ config.setIncludeIccCurvesAndLuts(true);
+ metadata = new Metadata();
+ new ImageMetadataExtractor.CopyUnknownFieldsHandler(config).handle(d,
metadata);
+ assertEquals("sRGB", metadata.get(ImageMetadataExtractor.ICC_NS +
"Profile Description"));
+ assertEquals("0.0, 1.0", metadata.get(ImageMetadataExtractor.ICC_NS +
"Red TRC"));
+ }
+
+ //ICC 'desc' tag: type, reserved, byte count including the NUL, ASCII, NUL
+ private static byte[] iccDesc(String text) {
+ byte[] ascii = text.getBytes(StandardCharsets.US_ASCII);
+ ByteBuffer b = ByteBuffer.allocate(12 + ascii.length + 1);
+
b.put("desc".getBytes(StandardCharsets.US_ASCII)).putInt(0).putInt(ascii.length
+ 1)
+ .put(ascii).put((byte) 0);
+ return b.array();
+ }
+
+ //ICC 'curv' tag: type, reserved, count, count uint16 samples spread over
0..1
+ private static byte[] iccCurve(int count) {
+ ByteBuffer b = ByteBuffer.allocate(12 + 2 * count);
+
b.put("curv".getBytes(StandardCharsets.US_ASCII)).putInt(0).putInt(count);
+ for (int i = 0; i < count; i++) {
+ b.putShort((short) (65535L * i / (count - 1)));
+ }
+ return b.array();
+ }
+
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/JpegParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/JpegParserTest.java
index a493205ce0..11d6d5a9f0 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/JpegParserTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/java/org/apache/tika/parser/image/JpegParserTest.java
@@ -18,12 +18,20 @@ package org.apache.tika.parser.image;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.awt.color.ColorSpace;
+import java.awt.color.ICC_Profile;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.TimeZone;
+import javax.imageio.ImageIO;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
@@ -34,6 +42,7 @@ import org.junit.jupiter.api.parallel.Resources;
import org.xml.sax.helpers.DefaultHandler;
import org.apache.tika.TikaTest;
+import org.apache.tika.config.loader.TikaLoader;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Geographic;
import org.apache.tika.metadata.HttpHeaders;
@@ -296,4 +305,66 @@ public class JpegParserTest extends TikaTest {
assertEquals("xmp.did:49E997348D4911E1AB62EBF9B374B234",
metadata.get(XMPMM.DOCUMENTID));
}
+ @Test
+ public void testIccCurvesSkippedByDefault() throws Exception {
+ Metadata metadata = parseSyntheticIccJpeg(parser, new ParseContext());
+ assertEquals("RGB", metadata.get(ImageMetadataExtractor.ICC_NS +
"Color space"));
+ assertNotNull(metadata.get(ImageMetadataExtractor.ICC_NS + "Profile
Description"));
+ assertNull(metadata.get(ImageMetadataExtractor.ICC_NS + "Red TRC"));
+ assertNull(metadata.get(ImageMetadataExtractor.ICC_NS + "Blue TRC"));
+ }
+
+ @Test
+ public void testIccCurvesViaParseContext() throws Exception {
+ ImageMetadataConfig config = new ImageMetadataConfig();
+ config.setIncludeIccCurvesAndLuts(true);
+ ParseContext context = new ParseContext();
+ context.set(ImageMetadataConfig.class, config);
+ Metadata metadata = parseSyntheticIccJpeg(parser, context);
+ String redTrc = metadata.get(ImageMetadataExtractor.ICC_NS + "Red
TRC");
+ assertNotNull(redTrc);
+ //the JDK sRGB profile has a 1024-point curve
+ assertEquals(1024, redTrc.split(", ").length);
+ }
+
+ @Test
+ public void testIccCurvesViaJsonConfig() throws Exception {
+ Parser loaded = TikaLoader
+ .load(getConfigPath(JpegParserTest.class,
"tika-config-icc-curves-on.json"))
+ .loadParsers();
+ Metadata metadata = parseSyntheticIccJpeg(loaded, new ParseContext());
+ assertNotNull(metadata.get(ImageMetadataExtractor.ICC_NS + "Red TRC"));
+ }
+
+ private static Metadata parseSyntheticIccJpeg(Parser parser, ParseContext
context)
+ throws Exception {
+ Metadata metadata = new Metadata();
+ metadata.set(HttpHeaders.CONTENT_TYPE, "image/jpeg");
+ try (TikaInputStream tis = TikaInputStream.get(jpegWithSrgbProfile()))
{
+ parser.parse(tis, new DefaultHandler(), metadata, context);
+ }
+ return metadata;
+ }
+
+ //a 4x4 JPEG with the JDK's sRGB profile in an APP2 ICC_PROFILE segment
after SOI
+ private static byte[] jpegWithSrgbProfile() throws IOException {
+ ByteArrayOutputStream plain = new ByteArrayOutputStream();
+ ImageIO.write(new BufferedImage(4, 4, BufferedImage.TYPE_INT_RGB),
"jpeg", plain);
+ byte[] jpeg = plain.toByteArray();
+ byte[] profile = ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData();
+ byte[] marker = "ICC_PROFILE\0".getBytes(StandardCharsets.US_ASCII);
+ int segmentLength = 2 + marker.length + 2 + profile.length;
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.write(jpeg, 0, 2);
+ out.write(0xFF);
+ out.write(0xE2);
+ out.write(segmentLength >> 8);
+ out.write(segmentLength & 0xFF);
+ out.write(marker);
+ out.write(1);
+ out.write(1);
+ out.write(profile);
+ out.write(jpeg, 2, jpeg.length - 2);
+ return out.toByteArray();
+ }
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/configs/tika-config-icc-curves-on.json
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/configs/tika-config-icc-curves-on.json
new file mode 100644
index 0000000000..f115ee5b62
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-image-module/src/test/resources/configs/tika-config-icc-curves-on.json
@@ -0,0 +1,10 @@
+{
+ "parsers": [
+ "default-parser",
+ {
+ "jpeg-parser": {
+ "includeIccCurvesAndLuts": true
+ }
+ }
+ ]
+}