TwelveMonkeys: Unable to modify a JPEGImage10Metadata comment value

I am trying to modify the JFIF comment (note: not an EXIF tag, but the comment in the JFIF metadata) of an image and am running into the issue that the JPEGImage10Metadata object is readonly.

I am trying the approach of getting the metadata as a tree, walking the tree, replacing an attribute and setting the metadata as a tree, as outlined here: https://stackoverflow.com/a/9508479

Is there another way?

I have looked for ways to copy-and-mutate the JPEGImage10Metadata object, but not found any?

(I can see the existing value in the debugger so that it feels kind of annoying not to be able to change it… I’ve even considered using reflection… 😄 )

Here is the code that I’m trying to run:

    File downloadImageUrlToTempFile(AlbumEntry albumEntry, Path tempDir) {
        var imageUrl = albumEntry.getImageUrl();
        if (imageUrl == null || imageUrl.isEmpty()) {
            throw new OldAlbumException(String.format("Unable to download album entry matching id=%d, imageUrl is missing", albumEntry.getId()));
        }

        var fileName = findFileNamePartOfUrl(imageUrl);
        var tempfile = tempDir.resolve(fileName).toFile();
        IIOImage image = null;
        ImageWriter writer = null;
        try {
            HttpURLConnection connection = getConnectionFactory().connect(imageUrl);
            connection.setRequestMethod("GET");
            try(var inputStream = ImageIO.createImageInputStream(connection.getInputStream())) {
                var readers = ImageIO.getImageReaders(inputStream);
                if (readers.hasNext()) {
                    var reader = readers.next();
                    writer = ImageIO.getImageWriter(reader);
                    reader.setInput(inputStream);
                    image = reader.readAll(0, null);
                }
            }
        } catch (IOException e) {
            throw new OldAlbumException(String.format("Unable to download album entry matching id=%d from url=\"%s\"", albumEntry.getId(), albumEntry.getImageUrl()), e);
        }

        var metadata = image.getMetadata();
        var metadataAsTree = metadata.getAsTree("javax_imageio_1.0");
        findJfifCommentNode(metadataAsTree)
            .ifPresent(node -> node.setAttribute("value", albumEntry.getDescription()));
        try {
            metadata.setFromTree("javax_imageio_1.0", metadataAsTree);
        } catch (IIOInvalidTreeException e) {
            throw new OldAlbumException(String.format("Failed to replace comment in local copy of album entry matching id=%d from url=\"%s\"", albumEntry.getId(), albumEntry.getImageUrl()), e);
        }

        try (var outputStream = ImageIO.createImageOutputStream(new FileOutputStream(tempfile))){
            writer.setOutput(outputStream);
            writer.write(image);
            Files.setLastModifiedTime(tempfile.toPath(), FileTime.from(albumEntry.getLastModified().toInstant()));
            return tempfile;
        } catch (IOException e) {
            throw new OldAlbumException(String.format("Unable to save local copy of album entry matching id=%d from url=\"%s\"", albumEntry.getId(), albumEntry.getImageUrl()), e);
        }
    }

    Optional<IIOMetadataNode> findJfifCommentNode(Node metadataAsTree) {
        return StreamSupport.stream(iterable(metadataAsTree.getChildNodes()).spliterator(), false)
            .filter(n -> "Text".equals(n.getNodeName()))
            .findFirst()
            .flatMap(n -> StreamSupport.stream(iterable(n.getChildNodes()).spliterator(), false).findFirst());
    }

    public static Iterable<IIOMetadataNode> iterable(final NodeList nodeList) {
        return () -> new Iterator<IIOMetadataNode>() {

                private int index = 0;

                @Override
                public boolean hasNext() {
                    return index < nodeList.getLength();
                }

                @Override
                public IIOMetadataNode next() {
                    if (!hasNext())
                        throw new NoSuchElementException();
                    return (IIOMetadataNode) nodeList.item(index++);
                }
            };
    }

About this issue

  • Original URL
  • State: closed
  • Created 8 months ago
  • Comments: 17 (17 by maintainers)

Most upvoted comments

Sounds good!

I’ll close this issue as resolved, unless you have more questions about JPEG metadata? If so just reopen, or create a new, more specific issue.

Harald Kuhr @.***>:

One thing this odd XML-ish node structure is good for, is that it’s… well… XML. 😀

You can dump the whole tree like this:

 new XMLSerializer(System.out, "UTF-8").serialize(tree, false);

(using com.twelvemonkeys.xml. XMLSerializer, other serializers will probably do fine too…)

Oooh! Nice! Thanks! Found it: copy-paste-error!

The only thing you won’t see there it the userObjects, because the javax.imageio API invented userObject instead of reusing userData, not really sure why…

I found that the comment was put into a node named “com” by stepping through the build of the native tree in the debugger.

But I found a node named “com” directly under markerSequence and tried replacing it, but that probably wasn’t the right one.

According to the DTD I referenced above, the com (JFIF comment) marker is a child of markerSequence. It can’t appear anywhere else.

I ended up putting the comment as an attribute on markerSequence, because I had forgotten to change the name of the element I was searching for.

(I dumped out the tree before and after).

Now it is time to tidy up the code.

Current iteration (before tidying) looks like this: https://gist.github.com/steinarb/497c28d27f59e85614b0f221115199f4

I did this instead, and then I got further:

            try(var imageOutputStream = new MemoryCacheImageOutputStream(bytes)) {
                new TIFFWriter().write(entries, imageOutputStream);
            }

Now the EXIF metadata parses without throwing an EOFException.

Current problem: my replacement of the JFIF comment didn’t survive the addition of modifying EXIF comments: https://gist.github.com/steinarb/da81e5d71a6d9e44058ef02598972f3a#file-oldalbumserviceprovider-java-L30

Hi Steinar,

Short answer: JPEGImage10Metadata is (currently) read-only, see the isReadOnly method. Invoking mergeTree/setFromTree on an instance, will fail.

Long answer: You should still be able to achieve what you want, see #668. 😀 Especially this comment where I try to outline a workaround. I think it worked for jAlbum.

Try that first, but if you can’t make it work, let me know, and we’ll see if I can help.