Как разобрать 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 и что я могу с этим поделать?


person Anders    schedule 05.12.2012    source источник
comment
Разве вам не нужно сначала сделать его действительным перед синтаксическим анализом? Как вы это сделаете, все будет зависеть от того, что делает его недействительным.   -  person Hovercraft Full Of Eels    schedule 06.12.2012
comment
Согласно документы интерфейса getAsTree, этот метод возвращает узел DOM, а не строку xml, поэтому я не уверен, что вы имеете в виду, говоря, что он недействителен?   -  person Francis Avila    schedule 06.12.2012
comment
Другими словами, метаданные из javax.imageio — это не анализируемая строка xml, а DOM-дерево в памяти, поэтому они не могут быть недействительными, поскольку недействительность относится к анализу. Если вы имеете в виду, что эта строка, которую я разместил на этой странице, недействительна, то проблема заключается в том, что вы делаете для сериализации полученного узла DOM. Вы должны показать этот код.   -  person Francis Avila    schedule 06.12.2012
comment
@FrancisAvila: Да, но, как вы уже знаете, это метод getAsTree для объекта IOMetadata, который создает узел, вот и все. Так что я мало что могу там сделать. Затем я хотел разобрать его, но обнаружил, что он недействителен. Ошибки довольно мелкие, легко исправляются, если вставить в XML-редактор и сделать пару правок, но я, конечно, не могу этого сделать...   -  person Anders    schedule 06.12.2012
comment
@HovercraftFullOfEels: Да, и, как я уже упоминал в своем предыдущем комментарии, это метод getAsTree, который создает узел. И я определенно хотел бы сначала сделать его действительным (желательно, чтобы он был действительным непосредственно из метода, конечно), но я не знаю, как сделать недопустимый узел действительным в коде...   -  person Anders    schedule 06.12.2012
comment
Узел не может быть недействительным. Недействительны только сериализации. Как вы превращаете DOM в текст, который вы разместили здесь? Этот код нужно исправить. Нет ничего плохого в дереве Node из getAsTree. Покажите свой код сериализации или используйте этот код в качестве примера.   -  person Francis Avila    schedule 06.12.2012
comment
@FrancisAvila: А, хорошо, теперь я понимаю, что ты говоришь. Я проверил код, печатающий xml, и на самом деле он делает это правильно, но проблема в том, что метаданные, которые мне нужны, по какой-то причине являются тегами xml, а инструкции по обработке называются xpacket ВНУТРИ атрибута... Что не имеет смысла. Вот почему парсер ругается, потому что теги xml находятся внутри атрибута (текстовый атрибут элемента iTXtEntry).   -  person Anders    schedule 06.12.2012
comment
Если код, печатающий xml, выдает недопустимый xml, то он не печатает его правильно. Правильным способом печати было бы text="&lt;?xpacket begin=&quot;&quot; id=&quot;W5M0MpCehiHzreSzNTczkc9d&quot;?&gt;, но это явно не так. Да, странно помещать полный xml-документ в виде текста в значение атрибута, но это не является препятствием для достоверности.   -  person Francis Avila    schedule 06.12.2012


Ответы (1)


Ваш вопрос бессмыслен, потому что IIOMetaData.getAsTree() возвращает объект узла DOM, который является корнем дерева узлов. Это представление XML в памяти. Он нигде не анализируется, поэтому он не может быть недействительным. Строка XML-документа может быть недействительной, но здесь нет анализируемой строки. Метод getAsTree создает XML напрямую в памяти.

Проблема в том, что ваш выход создает недопустимый XML. Все, что сериализует ваш Node из 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="&lt;?xpacket begin=&quot;?&quot; id=&quot;W5M0MpCehiHzreSzNTczkc9d&quot;?&gt; &lt;x:xmpmeta xmlns:x=&quot;adobe:ns:meta/&quot; x:xmptk=&quot;Adobe XMP Core 5.0-c061 64.140949, 2010/12/07-10:57:01        &quot;&gt; &lt;rdf:RDF xmlns:rdf=&quot;http://www.w3.org/1999/02/22-rdf-syntax-ns#&quot;&gt; &lt;rdf:Description rdf:about=&quot;&quot; xmlns:xmp=&quot;http://ns.adobe.com/xap/1.0/&quot; xmlns:xmpMM=&quot;http://ns.adobe.com/xap/1.0/mm/&quot; xmlns:stRef=&quot;http://ns.adobe.com/xap/1.0/sType/ResourceRef#&quot; xmp:CreatorTool=&quot;Adobe Photoshop CS5.1 Macintosh&quot; xmpMM:InstanceID=&quot;xmp.iid:D281E43D34DC11E2BFE69DA1E5D17E5F&quot; xmpMM:DocumentID=&quot;xmp.did:D281E43E34DC11E2BFE69DA1E5D17E5F&quot;&gt; &lt;xmpMM:DerivedFrom stRef:instanceID=&quot;xmp.iid:D281E43B34DC11E2BFE69DA1E5D17E5F&quot; stRef:documentID=&quot;xmp.did:D281E43C34DC11E2BFE69DA1E5D17E5F&quot;/&gt; &lt;/rdf:Description&gt; &lt;/rdf:RDF&gt; &lt;/x:xmpmeta&gt; &lt;?xpacket end=&quot;r&quot;?&gt;"/>
  </iTXt>
  <tEXt>
    <tEXtEntry keyword="Software" value="Adobe ImageReady"/>
  </tEXt>
</javax_imageio_png_1.0>

Полученный XML действителен:

$ java imgmeta testimage.png | xmllint --noout -
$

(Нет вывода означает, что он действителен.)

Обратите внимание, как экранируется значение атрибута text iTXtEntry. Если вы хотите получить данные внутри этого атрибута, вам нужно получить строку, а затем проанализировать это как собственный 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);
person Francis Avila    schedule 06.12.2012
comment
Хорошо, спасибо. Да, это имеет смысл, и это работает, как вы это показываете. Итак, это объясняет мою проблему... но я все еще не уверен, как ее решить, потому что, как вы догадались, то, что мне нужно, находится внутри этого текстового атрибута. Но как я могу проанализировать это как XML-документ, не возвращаясь к регулярному выражению или чему-то еще? Он начинается и заканчивается инструкцией по обработке xpacket, нужно ли мне удалять их с помощью регулярных выражений, чтобы проанализировать это как документ xml? А как насчет того, чтобы отменить экранирование escape-символов обратно в теги? - person Anders; 06.12.2012
comment
С представлением DOM (или другим проанализированным XML-представлением по вашему выбору) возьмите значение атрибута (которое будет строкой). Затем передайте эту строку анализатору XML. Я дополню этот ответ примером, если хотите. (Поскольку XMP — это система метаданных на основе rdf, вам, вероятно, следует поискать библиотеку, которая напрямую моделирует RDF или XMP, так как с ней будет намного проще работать, чем возиться с этим XML.) - person Francis Avila; 06.12.2012