Skip to content

Commit

Permalink
Correctly parse complex XMP array items
Browse files Browse the repository at this point in the history
Resolves: #129
  • Loading branch information
mattiasw committed Oct 31, 2021
1 parent 427d45d commit 0eac019
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 24 deletions.
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ ExifReader
ExifReader is a JavaScript library that parses image files and extracts the
metadata. It can also extract an embedded thumbnail. It can be used either in a
browser or from Node. Supports JPEG, TIFF, PNG, HEIC, and WebP files with Exif,
IPTC, XMP, ICC, and MPF meta data (depending on file type).
IPTC, XMP, ICC, and MPF metadata (depending on file type).

ExifReader is highly and easily configurable and the resulting bundle can be as
small as **3 KiB** (gzipped) if you're only interested in a few tags (e.g. date
Expand Down Expand Up @@ -184,6 +184,9 @@ in the output. If you need to see them there is an option that can be passed in:
const tags = ExifReader.load(fileBuffer, {includeUnknown: true});
```

If you discover an unknown tag that should be handled by ExifReader, please
reach out by filing an issue.

### GPS

If `expanded: true` is specified in the options, there will be a `gps` group.
Expand Down Expand Up @@ -387,11 +390,12 @@ Tips
[examples site](https://mattiasw.github.io/ExifReader/) to see how you can
do that.
- In some cases it can make sense to only load the beginning of the image
file. It's unfortunately not possible to know how big the meta data will be
in an image, but if you limit yourself to regular Exif tags you can most
probably get by with only reading the first 128 kB. This may exclude IPTC
and XMP metadata though (and possibly Exif too if they come in an irregular
order) so please check if this optimization fits your use case.
file since that is where the metadata is located. It's unfortunately not
possible to know how big the metadata will be in an image, but if you limit
yourself to regular Exif tags you can most probably get by with only reading
the first 128 kB. This may exclude IPTC and XMP metadata though (and
possibly Exif too if they come in an irregular order) so please check if
this optimization fits your use case.

Testing
-------
Expand Down Expand Up @@ -441,6 +445,15 @@ case is covered.
Changelog
---------

- **October 2021**:
- Major version update 4.0.0. A couple of small breaking changes that
shouldn't affect too many users:
- Node.js 10+ is needed to read XMP tags (requirement from
[xmldom](https://www.npmjs.com/package/@xmldom/xmldom) dependency)
- XMP arrays with complex items are now parsed correctly, e.g. `Regions`
(see [issue #129](https://github.com/mattiasw/ExifReader/issues/129)
for more details)
- [Unknown tags](#unknown-tags) are no longer included by default
- **June 2021**:
- Make it possible to directly pass in file path, URL, or File object.
- **December 2020**:
Expand Down
2 changes: 1 addition & 1 deletion dist/exif-reader.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/exif-reader.js.map

Large diffs are not rendered by default.

30 changes: 14 additions & 16 deletions src/xmp-tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ function getDescriptionOfObject(value) {
const descriptions = [];

for (const key in value) {
descriptions.push(`${getClearTextKey(key)}: ${value[key].value}`);
descriptions.push(`${getClearTextKey(key)}: ${getDescription(value[key].value)}`);
}

return descriptions.join('; ');
Expand Down Expand Up @@ -303,11 +303,14 @@ function parseNodeChildrenAsTags(children) {
function parseNodeAsTag(node, name) {
if (hasNestedSimpleRdfDescription(node)) {
return parseNodeAsSimpleRdfDescription(node, name);
} else if (hasNestedStructureRdfDescription(node)) {
}
if (hasNestedStructureRdfDescription(node)) {
return parseNodeAsStructureRdfDescription(node, name);
} else if (isCompactStructure(node)) {
}
if (isCompactStructure(node)) {
return parseNodeAsCompactStructure(node, name);
} else if (isArray(node)) {
}
if (isArray(node)) {
return parseNodeAsArray(node, name);
}
return parseNodeAsSimpleValue(node, name);
Expand Down Expand Up @@ -390,6 +393,7 @@ function parseNodeAsStructureRdfDescription(node, name) {

function isCompactStructure(node) {
return (Object.keys(node.value).length === 0)
&& (node.attributes['xml:lang'] === undefined)
&& (node.attributes['rdf:resource'] === undefined);
}

Expand Down Expand Up @@ -437,20 +441,14 @@ function parseArrayValue(item) {
if (hasNestedSimpleRdfDescription(item)) {
return parseNodeAsSimpleRdfDescription(item);
}

if (hasNestedArrayValue(item)) {
return parseNodeChildrenAsTags(item.value);
if (hasNestedStructureRdfDescription(item)) {
return parseNodeAsStructureRdfDescription(item).value;
}
if (isCompactStructure(item)) {
return parseNodeAsCompactStructure(item).value;
}

return {
value: item.value,
attributes: parseNodeAttributes(item),
description: getDescription(item.value)
};
}

function hasNestedArrayValue(node) {
return node.attributes['rdf:parseType'] === 'Resource';
return parseNodeAsSimpleValue(item);
}

function parseNodeAsSimpleValue(node, name) {
Expand Down
145 changes: 145 additions & 0 deletions test/unit/xmp-tags-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,90 @@ describe('xmp-tags', function () {
});
});

it('should be able to read an unordered array with a concise structure value', () => {
const xmlString = getXmlString(`
<rdf:Description xmlns:xmp="http://ns.example.com/xmp">
<xmp:MyXMPArray xml:lang="en">
<rdf:Bag>
<rdf:li>
<rdf:Description xmp:MyXMPStructure0="47">
<xmp:MyXMPStructure1 xml:lang="sv">11</xmp:MyXMPStructure1>
</rdf:Description>
</rdf:li>
</rdf:Bag>
</xmp:MyXMPArray>
</rdf:Description>
`);
const dataView = getDataView(xmlString);
const tags = XmpTags.read(dataView, [{dataOffset: 0, length: xmlString.length}]);
expect(tags['MyXMPArray']).to.deep.equal({
value: [
{
MyXMPStructure0: {
value: '47',
attributes: {},
description: '47'
},
MyXMPStructure1: {
value: '11',
attributes: {
lang: 'sv'
},
description: '11'
}
}
],
attributes: {
lang: 'en'
},
description: 'MyXMPStructure0: 47; MyXMPStructure1: 11'
});
});

it('should be able to read an unordered array with structure value as attribute', () => {
const xmlString = getXmlString(`
<rdf:Description xmlns:xmp="http://ns.example.com/xmp">
<xmp:MyXMPArray xml:lang="en">
<rdf:Bag>
<rdf:li>
<rdf:Description xmp:MyXMPStructure0="47">
<xmp:MyXMPStructure1 xmp:MyXMPTag0="11"/>
</rdf:Description>
</rdf:li>
</rdf:Bag>
</xmp:MyXMPArray>
</rdf:Description>
`);
const dataView = getDataView(xmlString);
const tags = XmpTags.read(dataView, [{dataOffset: 0, length: xmlString.length}]);
expect(tags['MyXMPArray']).to.deep.equal({
value: [
{
MyXMPStructure0: {
value: '47',
attributes: {},
description: '47'
},
MyXMPStructure1: {
value: {
MyXMPTag0: {
value: '11',
attributes: {},
description: '11'
}
},
attributes: {},
description: 'MyXMPTag0: 11'
}
}
],
attributes: {
lang: 'en'
},
description: 'MyXMPStructure0: 47; MyXMPStructure1: MyXMPTag0: 11'
});
});

it('should be able to read an ordered array value', () => {
const xmlString = getXmlString(`
<rdf:Description xmlns:xmp="http://ns.example.com/xmp">
Expand Down Expand Up @@ -566,6 +650,67 @@ describe('xmp-tags', function () {
});
});

it('should be able to read an array structure value as attributes', () => {
const xmlString = getXmlString(`
<rdf:Description xmlns:xmp="http://ns.example.com/xmp">
<xmp:MyXMPArray xml:lang="en">
<rdf:Bag>
<rdf:li xmp:MyXMPTag0="47" xmp:MyXMPTag1="11" />
</rdf:Bag>
</xmp:MyXMPArray>
</rdf:Description>
`);
const dataView = getDataView(xmlString);
const tags = XmpTags.read(dataView, [{dataOffset: 0, length: xmlString.length}]);
expect(tags['MyXMPArray']).to.deep.equal({
value: [
{
MyXMPTag0: {
value: '47',
attributes: {},
description: '47'
},
MyXMPTag1: {
value: '11',
attributes: {},
description: '11'
}
}
],
attributes: {
lang: 'en'
},
description: 'MyXMPTag0: 47; MyXMPTag1: 11'
});
});

it('should be able to read an xml:lang qualifier on an empty array item', () => {
const xmlString = getXmlString(`
<rdf:Description xmlns:xmp="http://ns.example.com/xmp">
<xmp:MyXMPArray>
<rdf:Bag>
<rdf:li xml:lang="en" />
</rdf:Bag>
</xmp:MyXMPArray>
</rdf:Description>
`);
const dataView = getDataView(xmlString);
const tags = XmpTags.read(dataView, [{dataOffset: 0, length: xmlString.length}]);
expect(tags['MyXMPArray']).to.deep.equal({
value: [
{
value: {},
attributes: {
lang: 'en'
},
description: ''
}
],
attributes: {},
description: ''
});
});

it('should be able to read an empty array value', () => {
const xmlString = getXmlString(`
<rdf:Description xmlns:xmp="http://ns.example.com/xmp">
Expand Down

0 comments on commit 0eac019

Please sign in to comment.