Skip to content

Commit

Permalink
Add an xmp_elements key to the delegate script context
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Dolski committed Mar 11, 2022
1 parent f3dddb0 commit 3ba2c97
Show file tree
Hide file tree
Showing 10 changed files with 460 additions and 18 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
`delegate_script.pathname` configuration key.
* S3Cache uses multipart uploads, which reduces memory usage when caching
derivatives larger than 5 MB.
* The delegate script's `metadata` context key contains a new field,
`xmp_elements`, that provides a high-level key-value view of the XMP data.

## 5.0.6

Expand Down
16 changes: 13 additions & 3 deletions delegates.rb.sample
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,14 @@ class CustomDelegate
# "Field2Name": value
# ],
# "xmp_string": "<rdf:RDF>...</rdf:RDF>",
# "xmp_model": https://jena.apache.org/documentation/javadoc/jena/org/apache/jena/rdf/model/Model.html
# "xmp_model": See https://jena.apache.org/documentation/javadoc/jena/org/apache/jena/rdf/model/Model.html,
# "xmp_elements": {
# "Field1Name": "value",
# "Field2Name": [
# "value1",
# "value2"
# ]
# },
# "native": {
# # structure varies
# }
Expand All @@ -376,10 +383,13 @@ class CustomDelegate
# * The `exif` key refers to embedded EXIF data. This also includes IFD0
# metadata from source TIFFs, whether or not an EXIF IFD is present.
# * The `iptc` key refers to embedded IPTC IIM data.
# * The `xmp_string` key refers to raw embedded XMP data, which may or may
# not contain EXIF and/or IPTC information.
# * The `xmp_string` key refers to raw embedded XMP data.
# * The `xmp_model` key contains a Jena Model object pre-loaded with the
# contents of `xmp_string`.
# * The `xmp_elements` key contains a view of the embedded XMP data as key-
# value pairs. This is convenient to use, but may not work correctly with
# all XMP fields--in particular, those that cannot be expressed as
# key-value pairs.
# * The `native` key refers to format-specific metadata.
#
# Any combination of the above keys may be present or missing depending on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ public interface JavaContext {
* "Field2Name": value
* ],
* "xmp_string": "<rdf:RDF>...</rdf:RDF>",
* "xmp_model": https://jena.apache.org/documentation/javadoc/jena/org/apache/jena/rdf/model/Model.html
* "xmp_model": See https://jena.apache.org/documentation/javadoc/jena/org/apache/jena/rdf/model/Model.html,
* "xmp_elements": {
* "Field1Name": "value",
* "Field2Name": [
* "value1",
* "value2"
* ]
* },
* "native": {
* # structure varies
* }
Expand All @@ -83,6 +90,10 @@ public interface JavaContext {
* <li>The {@code xmp_model} key contains a {@link
* org.apache.jena.rdf.model.Model} object pre-loaded with the
* contents of {@code xmp_string}.</li>
* <li>The {@code xmp_elements} key contains a view of the embedded XMP
* data as key-value pairs. This is convenient to use, but may not work
* correctly with all XMP fields&mdash;in particular, those that cannot
* be expressed as key-value pairs.
* <li>The {@code native} key refers to format-specific metadata.</li>
* </ul>
*
Expand Down
31 changes: 26 additions & 5 deletions src/main/java/edu/illinois/library/cantaloupe/image/Metadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import edu.illinois.library.cantaloupe.image.exif.Directory;
import edu.illinois.library.cantaloupe.image.exif.Tag;
import edu.illinois.library.cantaloupe.image.iptc.DataSet;
import edu.illinois.library.cantaloupe.image.xmp.MapReader;
import edu.illinois.library.cantaloupe.util.StringUtils;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
Expand All @@ -17,6 +18,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
Expand Down Expand Up @@ -170,16 +172,32 @@ private void readOrientationFromXMP() {
}

/**
* Returns an RDF/XML string in UTF-8 encoding. The root element is
* {@literal rdf:RDF}, and there is no packet wrapper.
*
* @return XMP data packet.
* @return RDF/XML string in UTF-8 encoding. The root element is {@literal
* rdf:RDF}, and there is no packet wrapper.
*/
@JsonProperty
public Optional<String> getXMP() {
return Optional.ofNullable(xmp);
}

/**
* @return Map of elements found in the XMP data. If none are found, the
* map is empty.
*/
@JsonIgnore
public Map<String,Object> getXMPElements() {
loadXMP();
if (xmpModel != null) {
try {
MapReader reader = new MapReader(xmpModel);
return reader.readElements();
} catch (IOException e) {
LOGGER.warn("getXMPElements(): {}", e.getMessage());
}
}
return Collections.emptyMap();
}

/**
* @return XMP model backed by the contents of {@link #getXMP()}.
*/
Expand Down Expand Up @@ -290,7 +308,9 @@ public void setXMP(String xmp) {
* {
* "exif": See {@link Directory#toMap()},
* "iptc": See {@link DataSet#toMap()},
* "xmp": "<rdf:RDF>...</rdf:RDF>",
* "xmp_string": "<rdf:RDF>...</rdf:RDF>",
* "xmp_model": [Jena model],
* "xmp_elements": {@link Map}
* "native": String
* }}
*
Expand All @@ -314,6 +334,7 @@ public Map<String,Object> toMap() {
// XMP
getXMP().ifPresent(xmp -> map.put("xmp_string", xmp));
getXMPModel().ifPresent(model -> map.put("xmp_model", model));
map.put("xmp_elements", getXMPElements());
// Native metadata
getNativeMetadata().ifPresent(nm -> map.put("native", nm));
return Collections.unmodifiableMap(map);
Expand Down
148 changes: 148 additions & 0 deletions src/main/java/edu/illinois/library/cantaloupe/image/xmp/MapReader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package edu.illinois.library.cantaloupe.image.xmp;

import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.rdf.model.Statement;
import org.apache.jena.rdf.model.StmtIterator;
import org.apache.jena.riot.RIOT;
import org.apache.jena.riot.RiotException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
* Extracts label-value pairs from an XMP model, making them available in a
* {@link Map}.
*
* @author Alex Dolski UIUC
* @since 6.0
*/
public final class MapReader {

private static final Logger LOGGER =
LoggerFactory.getLogger(MapReader.class);

private static final Map<String,String> PREFIXES = Map.ofEntries(
Map.entry("http://ns.adobe.com/camera-raw-settings/1.0/", "crs"),
Map.entry("http://purl.org/dc/elements/1.1/", "dc"),
Map.entry("http://purl.org/dc/terms/", "dcterms"),
Map.entry("http://ns.adobe.com/exif/1.0/", "exif"),
Map.entry("http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/", "Iptc4xmpCore"),
Map.entry("http://ns.adobe.com/iX/1.0/", "iX"),
Map.entry("http://ns.adobe.com/pdf/1.3/", "pdf"),
Map.entry("http://ns.adobe.com/photoshop/1.0/", "photoshop"),
Map.entry("http://ns.adobe.com/tiff/1.0/", "tiff"),
Map.entry("http://ns.adobe.com/xap/1.0/", "xmp"),
Map.entry("http://ns.adobe.com/xap/1.0/bj/", "xmpBJ"),
Map.entry("http://ns.adobe.com/xmp/1.0/DynamicMedia/", "xmpDM"),
Map.entry("http://ns.adobe.com/xmp/identifier/qual/1.0/", "xmpidq"),
Map.entry("http://ns.adobe.com/xap/1.0/mm/", "xmpMM"),
Map.entry("http://ns.adobe.com/xap/1.0/rights/", "xmpRights"),
Map.entry("http://ns.adobe.com/xap/1.0/t/pg/", "xmpTPg"));

private final Model model;
private final Map<String,Object> elements = new TreeMap<>();
private boolean hasReadElements;

/**
* @param xmp XMP string. {@code <rdf:RDF>} must be the root element.
* @see edu.illinois.library.cantaloupe.util.StringUtils#trimXMP
*/
public MapReader(String xmp) throws IOException {
RIOT.init();
this.model = ModelFactory.createDefaultModel();
try (StringReader reader = new StringReader(xmp)) {
model.read(reader, null, "RDF/XML");
} catch (RiotException | NullPointerException e) {
// The XMP string may be invalid RDF/XML, or there may be a bug
// in Jena (that would be the NPE). Not much we can do.
throw new IOException(e);
}
}

/**
* @param model XMP model, already initialized.
*/
public MapReader(Model model) {
this.model = model;
}

public Map<String,Object> readElements() throws IOException {
if (!hasReadElements) {
StmtIterator it = model.listStatements();
while (it.hasNext()) {
Statement stmt = it.next();
//System.out.println(stmt.getSubject() + " " + stmt.getSubject().isAnon());
//System.out.println(" " + stmt.getPredicate());
//System.out.println(" " + stmt.getObject() + " " + stmt.getObject().isLiteral());
//System.out.println("---------------------------");
if (!stmt.getSubject().isAnon()) {
recurse(stmt);
}
}
LOGGER.trace("readElements(): read {} elements", elements.size());
hasReadElements = true;
}
return Collections.unmodifiableMap(elements);
}

private void recurse(Statement stmt) {
recurse(stmt, null);
}

private void recurse(Statement stmt, String predicateOverride) {
String predicate = stmt.getPredicate().toString();
if (stmt.getObject().isLiteral()) {
addElement(label(predicateOverride != null ? predicateOverride : predicate),
stmt.getObject().asLiteral().getValue());
} else {
StmtIterator it = model.listStatements(
stmt.getObject().asResource(), null, (RDFNode) null);
while (it.hasNext()) {
Statement substmt = it.next();
predicateOverride = null;
if (substmt.getPredicate().toString().matches("(.*)#_\\d+\\b")) {
predicateOverride = predicate;
}
recurse(substmt, predicateOverride);
}
}
}

private void addElement(String label, Object value) {
if (elements.containsKey(label)) {
if (elements.get(label) instanceof List) {
@SuppressWarnings("unchecked")
List<Object> valueList = (List<Object>) elements.get(label);
valueList.add(value);
} else {
List<Object> valueList = new ArrayList<>();
valueList.add(elements.get(label));
valueList.add(value);
elements.put(label, valueList);
}
} else {
elements.put(label, value);
}
}

private String label(String uri) {
for (Map.Entry<String,String> entry : PREFIXES.entrySet()) {
if (uri.startsWith(entry.getKey())) {
String prefix = entry.getValue();
String[] parts = uri.split("/");
return prefix + ":" + parts[parts.length - 1];
}
}
return uri;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* <p>Contains an XMP parser.</p>
*
* @since 6.0
*/
package edu.illinois.library.cantaloupe.image.xmp;
Loading

0 comments on commit 3ba2c97

Please sign in to comment.