Как разобрать XMP в Java, когда не действительный XML?
Я извлекаю метаданные из изображений PNG, используя javax.imageio. Это отлично работает. Но метод getAsTree для доступа к актуальным метаданным возвращает недопустимый XML. Так что я не знаю, как проанализировать этот XML для получения определенных метаданных:
run:
Format name: javax_imageio_png_1.0
<javax_imageio_png_1.0>
<IHDR width="256" height="256" bitDepth="8" colorType="RGBAlpha" compressionMethod="deflate" filterMethod="adaptive" interlaceMethod="none"/>
<cHRM whitePointX="31269" whitePointY="32899" redX="63999" redY="33001" greenX="30000" greenY="60000" blueX="15000" blueY="5999"/>
<gAMA value="45454"/>
<iTXt>
<iTXtEntry keyword="XML:com.adobe.xmp" compressionFlag="FALSE" compressionMethod="0" languageTag="" translatedKeyword="" text="<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.0-c061 64.140949, 2010/12/07-10:57:01 ">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"
xmlns:lr="http://ns.adobe.com/lightroom/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmp:MetadataDate="2012-12-05T21:36:19+01:00"
xmpMM:InstanceID="xmp.iid:EF7F11740720681192B08F682498C71D"
xmpMM:DocumentID="xmp.did:FC7F11740720681192B0AE5890E66CAE"
xmpMM:OriginalDocumentID="xmp.did:FC7F11740720681192B0AE5890E66CAE">
<xmpMM:History>
<rdf:Seq>
<rdf:li
stEvt:action="saved"
stEvt:instanceID="xmp.iid:FC7F11740720681192B0AE5890E66CAE"
stEvt:when="2012-12-04T00:23:34+01:00"
stEvt:changed="/metadata"/>
<rdf:li
stEvt:action="saved"
stEvt:instanceID="xmp.iid:EF7F11740720681192B08F682498C71D"
stEvt:when="2012-12-05T21:36:19+01:00"
stEvt:changed="/metadata"/>
</rdf:Seq>
</xmpMM:History>
<lr:hierarchicalSubject>
<rdf:Bag>
<rdf:li>Component|Software</rdf:li>
<rdf:li>Places|Paris</rdf:li>
<rdf:li>Product|Christensen</rdf:li>
<rdf:li>Product|Simba</rdf:li>
</rdf:Bag>
</lr:hierarchicalSubject>
<dc:subject>
<rdf:Bag>
<rdf:li>Christensen</rdf:li>
<rdf:li>Paris</rdf:li>
<rdf:li>Simba</rdf:li>
<rdf:li>Software</rdf:li>
</rdf:Bag>
</dc:subject>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="r"?>"/>
</iTXt>
<pHYs pixelsPerUnitXAxis="2835" pixelsPerUnitYAxis="2835" unitSpecifier="meter"/>
</javax_imageio_png_1.0>
Format name: javax_imageio_1.0
<javax_imageio_1.0>
<Chroma>
<ColorSpaceType name="RGB"/>
<NumChannels value="4"/>
<Gamma value="0.45453998"/>
<BlackIsZero value="TRUE"/>
</Chroma>
<Compression>
<CompressionTypeName value="deflate"/>
<Lossless value="TRUE"/>
<NumProgressiveScans value="1"/>
</Compression>
<Data>
<PlanarConfiguration value="PixelInterleaved"/>
<SampleFormat value="UnsignedIntegral"/>
<BitsPerSample value="8 8 8 8"/>
</Data>
<Dimension>
<PixelAspectRatio value="1.0"/>
<ImageOrientation value="Normal"/>
<HorizontalPixelSize value="0.35273367"/>
<VerticalPixelSize value="0.35273367"/>
</Dimension>
<Text>
<TextEntry keyword="XML:com.adobe.xmp" value="<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.0-c061 64.140949, 2010/12/07-10:57:01 ">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"
xmlns:lr="http://ns.adobe.com/lightroom/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmp:MetadataDate="2012-12-05T21:36:19+01:00"
xmpMM:InstanceID="xmp.iid:EF7F11740720681192B08F682498C71D"
xmpMM:DocumentID="xmp.did:FC7F11740720681192B0AE5890E66CAE"
xmpMM:OriginalDocumentID="xmp.did:FC7F11740720681192B0AE5890E66CAE">
<xmpMM:History>
<rdf:Seq>
<rdf:li
stEvt:action="saved"
stEvt:instanceID="xmp.iid:FC7F11740720681192B0AE5890E66CAE"
stEvt:when="2012-12-04T00:23:34+01:00"
stEvt:changed="/metadata"/>
<rdf:li
stEvt:action="saved"
stEvt:instanceID="xmp.iid:EF7F11740720681192B08F682498C71D"
stEvt:when="2012-12-05T21:36:19+01:00"
stEvt:changed="/metadata"/>
</rdf:Seq>
</xmpMM:History>
<lr:hierarchicalSubject>
<rdf:Bag>
<rdf:li>Component|Software</rdf:li>
<rdf:li>Places|Paris</rdf:li>
<rdf:li>Product|Christensen</rdf:li>
<rdf:li>Product|Simba</rdf:li>
</rdf:Bag>
</lr:hierarchicalSubject>
<dc:subject>
<rdf:Bag>
<rdf:li>Christensen</rdf:li>
<rdf:li>Paris</rdf:li>
<rdf:li>Simba</rdf:li>
<rdf:li>Software</rdf:li>
</rdf:Bag>
</dc:subject>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="r"?>" language="" compression="none"/>
</Text>
<Transparency>
<Alpha value="nonpremultipled"/>
</Transparency>
</javax_imageio_1.0>
BUILD SUCCESSFUL (total time: 3 seconds)
Недопустимый XML начинается с элемента iTXtEntry, который имеет бит xpacket и содержит дочерние элементы, хотя он имеет формат самозакрывающегося тега вместо конечного тега. Поэтому, когда я пытаюсь разобрать это, используя документ DOM и xpath, я получаю сообщение об ошибке, в котором говорится, что этот элемент не может содержать ">" в содержимом элемента.
Я отключил проверку DTD в DocumentBuilderFactory. Это не помогает Я чувствую, что использую регулярные выражения, но это не так. Почему я в первую очередь получаю неверный XML из метода getAsTree в imageio, и что я могу с этим сделать?
1 ответ
Ваш вопрос бессмысленный, потому что IIOMetaData.getAsTree()
возвращает объект узла DOM, который является корнем дерева узлов. Это представление XML в памяти. Он нигде не анализируется, поэтому не может быть недействительным. Строка XML-документа может быть недопустимой, но здесь нет строки, которая анализируется. getAsTree
Метод создал XML напрямую, в памяти.
Проблема в том, что вы выводите неверный XML. Что бы ни сериализует ваш нод от getAsTree()
делает это неправильно. А именно, он не может должным образом избежать значения text
атрибут, который сам по себе является строкой документа XML.
Ниже приведен полный пример, который демонстрирует, как получить метаданные изображения и сериализовать в (действительную) строку XML.
import java.io.*;
import java.util.*;
// for imageio metadata
import javax.imageio.*;
import javax.imageio.stream.*;
import javax.imageio.metadata.*;
// for xml handling
import org.w3c.dom.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
public class imgmeta {
// Very lazy exception handling
// This is just a quick example
public static void main(String[] args) throws Exception {
String filename = args[0];
File file = new File(filename);
ImageInputStream imagestream = ImageIO.createImageInputStream(file);
// get a reader which is able to read this file
Iterator<ImageReader> readers = ImageIO.getImageReaders(imagestream);
ImageReader reader = readers.next();
// feed image to reader
reader.setInput(imagestream, true);
// get metadata of first image
IIOMetadata metadata = reader.getImageMetadata(0);
// get any metadata format name
// (you should prefer the native one, but not all images have one)
// String mdataname = metadata.getNativeMetadataFormatName(); // might be null
String[] mdatanames = metadata.getMetadataFormatNames();
String mdataname = mdatanames[0];
Node metadatadom = metadata.getAsTree(mdataname);
// metadatadom is now a DOM Node root of a DOM tree
// representing metadata in the image
// Since it's in-memory, it can't be "invalid"
// because it's already been parsed
// now let's serialize to an XML string
// javax.xml.transform.Transformer takes xml sources
// in one representation and transforms them to xml
// in another representation
// Representations include: DOM, JAXB, SAX, stream, etc
DOMSource source = new DOMSource(metadatadom);
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(source, result);
// THIS is what you want:
String metadata_in_xml = writer.toString();
// now print it:
System.out.print(metadata_in_xml);
}
}
Это тестовый прогон с использованием изображения, которое я имел вокруг:
$ java imgtest testimage.png | xmllint --format -
<?xml version="1.0" encoding="UTF-8"?>
<javax_imageio_png_1.0>
<IHDR width="149" height="237" bitDepth="8" colorType="RGBAlpha" compressionMethod="deflate" filterMethod="adaptive" interlaceMethod="none"/>
<iTXt>
<iTXtEntry keyword="XML:com.adobe.xmp" compressionFlag="0" compressionMethod="0" languageTag="" translatedKeyword="" text="<?xpacket begin="?" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.0-c061 64.140949, 2010/12/07-10:57:01 "> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/" xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/" xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#" xmp:CreatorTool="Adobe Photoshop CS5.1 Macintosh" xmpMM:InstanceID="xmp.iid:D281E43D34DC11E2BFE69DA1E5D17E5F" xmpMM:DocumentID="xmp.did:D281E43E34DC11E2BFE69DA1E5D17E5F"> <xmpMM:DerivedFrom stRef:instanceID="xmp.iid:D281E43B34DC11E2BFE69DA1E5D17E5F" stRef:documentID="xmp.did:D281E43C34DC11E2BFE69DA1E5D17E5F"/> </rdf:Description> </rdf:RDF> </x:xmpmeta> <?xpacket end="r"?>"/>
</iTXt>
<tEXt>
<tEXtEntry keyword="Software" value="Adobe ImageReady"/>
</tEXt>
</javax_imageio_png_1.0>
Созданный XML действителен:
$ java imgmeta testimage.png | xmllint --noout -
$
(Нет вывода означает, что действительный.)
Обратите внимание, как значение iTXtEntry
"s text
атрибут экранирован Если вы хотите получить данные внутри этого атрибута, вам нужно извлечь строку и затем проанализировать ее как свой собственный XML-документ, чтобы получить другое дерево DOM (или что-то еще). Этот атрибут: keyword="XML:com.adobe.xmp"
является сигналом того, что значение text
Атрибут - это документ XML с данными XMP.
ОБНОВЛЕНИЕ: анализ данных XMP
Вот пример кода, демонстрирующий извлечение значения атрибута и его синтаксический анализ в XML и в дереве DOM.
public class XMPExample {
public static String transformXML(Node xml) throws Exception {
StringWriter writer = new StringWriter();
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(new DOMSource(xml), new StreamResult(writer));
return writer.toString();
}
public static Document transformXML(String xml) throws Exception {
StringReader reader = new StringReader(xml);
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(new StreamSource(reader), new DOMResult(doc));
return doc;
}
public static String getXMP(Element metadata_dom) throws Exception {
// (Element) type because getElementsByTagName() method is required
// There are many more robust ways of selecting nodes
// (e.g. javax.xml.xpath), but this is for a simple example
// that only uses the native DOM methods
// This is very brittle because we're making assumptions about
// the metadata_dom structure. There are two sources of brittleness:
// 1. The metadata format from `metadata.getMetadataFormatNames()`.
// You should probably settle on a standard one you know will
// exist, like 'javax_imageio_1.0'
// 2. How the image stores the metadata. Usually XMP data will
// be in a text field with keyword 'XML:com.adobe.xmp', but
// I don't know that this is *always* the case.
// the code below assumes "javax_imageio_png_1.0" format
NodeList iTXtEntries = metadata_dom.getElementsByTagName("iTXtEntry");
Element iTXtEntry = null;
Element entry = null;
for (int i = 0; i < iTXtEntries.getLength(); i++) {
entry = (Element) iTXtEntries.item(i);
if (entry.getAttribute("keyword").equals("XML:com.adobe.xmp")) {
iTXtEntry = entry;
break;
}
}
if (iTXtEntry == null) {
return null;
}
String xmp_xml_doc = iTXtEntry.getAttribute("text");
return xmp_xml_doc;
}
}
// Use like so:
Node metadatanode = metadata.getAsTree(metadataname);
String xmp_xml = XMPExample.getXMP((Element) metadatanode);
// xmp_xml is now an xml document STRING
System.out.print(xmp_xml);
// If you want to parse it as an XML document, use an XML parser.
Document xmp_dom = XMPExample.transformXML(xmp_xml);
// ...and you can serialize it again when you are done.
String xmp_xml_roundtripped = XMPExample.transformXML(xmp_dom);