Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jpeg): Support encoding/decoding arbitrary metadata as comments #4430

Merged
merged 4 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cmake/testing.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ macro (oiio_add_all_tests)
oiiotool-demosaic
diff
dither dup-channels
jpeg-corrupt
jpeg-corrupt jpeg-metadata
maketx oiiotool-maketx
misnamed-file
missingcolor
Expand Down
8 changes: 8 additions & 0 deletions src/doc/builtinplugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,10 @@ anywhere near the acceptance of the original JPEG/JFIF format.
reader/writer, and you should assume that nearly everything described
Appendix :ref:`chap-stdmetadata` is properly translated when using
JPEG files.
* - *other*
-
- Extra attributes will be read from comment blocks in the JPEG file,
and can optionally be written if ``jpeg:com_attributes`` is enabled.

**Configuration settings for JPEG input**

Expand Down Expand Up @@ -1084,6 +1088,10 @@ control aspects of the writing itself:
* - ``jpeg:progressive``
- int
- If nonzero, will write a progressive JPEG file.
* - ``jpeg:com_attributes``
- int
- If nonzero, extra attributes will be written into the file as comment
blocks.


**Custom I/O Overrides**
Expand Down
7 changes: 7 additions & 0 deletions src/include/OpenImageIO/imageio.h
Original file line number Diff line number Diff line change
Expand Up @@ -2902,6 +2902,13 @@ OIIO_API std::string geterror(bool clear = true);
/// When nonzero, use the new "OpenEXR core C library" when available,
/// for OpenEXR >= 3.1. This is experimental, and currently defaults to 0.
///
/// - `int jpeg:com_attributes`
///
/// When nonzero, try to parse JPEG comment blocks as key-value attributes,
/// and only set ImageDescription if the parsing fails. Otherwise, always
/// set ImageDescription to the first comment block. Default is 1.
///
///
/// - `int limits:channels` (1024)
///
/// When nonzero, the maximum number of color channels in an image. Image
Expand Down
1 change: 1 addition & 0 deletions src/include/imageio_pvt.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ extern OIIO_UTIL_API int oiio_print_debug;
extern OIIO_UTIL_API int oiio_print_uncaught_errors;
extern int oiio_log_times;
extern int openexr_core;
extern int jpeg_com_attributes;
extern int limit_channels;
extern int limit_imagesize_MB;
extern int imagebuf_print_uncaught_errors;
Expand Down
39 changes: 36 additions & 3 deletions src/jpeg.imageio/jpeginput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <OpenImageIO/filesystem.h>
#include <OpenImageIO/fmath.h>
#include <OpenImageIO/imageio.h>
#include <OpenImageIO/strutil.h>
#include <OpenImageIO/tiffutils.h>

#include "jpeg_pvt.h"
Expand Down Expand Up @@ -287,10 +288,42 @@ JpgInput::open(const std::string& name, ImageSpec& newspec)
&& !strcmp((const char*)m->data, "Photoshop 3.0"))
jpeg_decode_iptc((unsigned char*)m->data);
else if (m->marker == JPEG_COM) {
std::string data((const char*)m->data, m->data_length);
// Additional string metadata can be stored in JPEG files as
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's worth explicitly starting by spelling out:

// The first COM block encountered will be interpreted as the image description.
// Subsequent COM blocks, if in the form "key:value", ... blah blah

By the way, is this exactly what we want? What if the first COM looks like "key:value", should that always be slotted into ImageDescription?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer only setting ImageDescription if the parsing fails, but I figured that's a potentially breaking change so I kept it safe for now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I bet that most traditional JPEG COM blocks that are true "image comments" are unlikely to have the specific form of "[ident:]string1:string2" where the optional ident (namespace prefix) follows C identifier rules and string1 won't start or end with whitespace. If we interpret only that pattern as metadata and the first COM that doesn't follow the pattern is "ImageDescription".

I'm willing to risk that an occasional "comment" with a quirky format might be incorrectly interpreted as metadata. Especially if there is some kind of OIIO global option that lets you revert to the old behavior (first COM is always ImageDescription), so somebody can get out of a bind if they have a pile of images with the ambiguous formatting of their COM blocks.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the verdict here, @lukasstockner? Do you want to make any more changes, or do you want to keep the logic as-is and we can always revise later if it causes trouble?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's fine with you, I'll go ahead and add some more logic to only set the ImageDescription if a global option is set and/or the matched key fails a heuristic.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me.

// comment markers in the form "key:value" or "ident:key:value".
// If the string contains a single colon, we assume key:value.
// If there's multiple, we try splitting as ident:key:value and
// check if ident and key are reasonable (in particular, whether
// ident is a C-style identifier and key is not surrounded by
// whitespace). If ident passes but key doesn't, assume key:value.
auto separator = data.find(':');
if (OIIO::get_int_attribute("jpeg:com_attributes")
&& (separator != std::string::npos && separator > 0)) {
std::string left = data.substr(0, separator);
std::string right = data.substr(separator + 1);
separator = right.find(':');
if (separator != std::string::npos && separator > 0) {
std::string mid = right.substr(0, separator);
std::string value = right.substr(separator + 1);
if (Strutil::string_is_identifier(left)
&& (mid == Strutil::trimmed_whitespace(mid))) {
// Valid parsing: left is ident, mid is key
std::string attribute = left + ":" + mid;
if (!m_spec.find_attribute(attribute, TypeDesc::STRING))
m_spec.attribute(attribute, value);
continue;
}
}
if (left == Strutil::trimmed_whitespace(left)) {
// Valid parsing: left is key, right is value
if (!m_spec.find_attribute(left, TypeDesc::STRING))
m_spec.attribute(left, right);
continue;
}
}
// If we made it this far, treat the comment as a description
if (!m_spec.find_attribute("ImageDescription", TypeDesc::STRING))
m_spec.attribute("ImageDescription",
std::string((const char*)m->data,
m->data_length));
m_spec.attribute("ImageDescription", data);
}
}

Expand Down
39 changes: 39 additions & 0 deletions src/jpeg.imageio/jpegoutput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <cassert>
#include <cstdio>
#include <set>
#include <vector>

#include <OpenImageIO/filesystem.h>
Expand Down Expand Up @@ -117,6 +118,14 @@ OIIO_PLUGIN_EXPORTS_END



static std::set<std::string> metadata_include { "oiio:ConstantColor",
"oiio:AverageColor",
"oiio:SHA-1" };
static std::set<std::string> metadata_exclude {
"XResolution", "YResolution", "PixelAspectRatio",
"ResolutionUnit", "Orientation", "ImageDescription"
};

bool
JpgOutput::open(const std::string& name, const ImageSpec& newspec,
OpenMode mode)
Expand Down Expand Up @@ -229,6 +238,36 @@ JpgOutput::open(const std::string& name, const ImageSpec& newspec,
comment.size() + 1);
}

// Write other metadata as JPEG comments if requested
if (m_spec.get_int_attribute("jpeg:com_attributes")) {
for (const auto& p : m_spec.extra_attribs) {
std::string name = p.name().string();
auto colon = name.find(':');
if (metadata_include.count(name)) {
// Allow explicitly included metadata
} else if (metadata_exclude.count(name))
continue; // Suppress metadata that is processed separately
else if (Strutil::istarts_with(name, "ICCProfile"))
continue; // Suppress ICC profile, gets written separately
else if (colon != ustring::npos) {
auto prefix = p.name().substr(0, colon);
if (Strutil::iequals(prefix, "oiio"))
continue; // Suppress internal metadata
else if (Strutil::iequals(prefix, "exif")
|| Strutil::iequals(prefix, "GPS")
|| Strutil::iequals(prefix, "XMP"))
continue; // Suppress EXIF metadata, gets written separately
else if (Strutil::iequals(prefix, "iptc"))
continue; // Suppress IPTC metadata
else if (is_imageio_format_name(prefix))
continue; // Suppress format-specific metadata
}
auto data = p.name().string() + ":" + p.get_string();
jpeg_write_marker(&m_cinfo, JPEG_COM, (JOCTET*)data.c_str(),
data.size());
}
}

if (equivalent_colorspace(m_spec.get_string_attribute("oiio:ColorSpace"),
"sRGB"))
m_spec.attribute("Exif:ColorSpace", 1);
Expand Down
9 changes: 9 additions & 0 deletions src/libOpenImageIO/imageio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ atomic_int oiio_try_all_readers(1);
#endif
// Should we use "Exr core C library"?
int openexr_core(OIIO_OPENEXR_CORE_DEFAULT);
int jpeg_com_attributes(1);
int tiff_half(0);
int tiff_multithread(1);
int dds_bc5normal(0);
Expand Down Expand Up @@ -366,6 +367,10 @@ attribute(string_view name, TypeDesc type, const void* val)
openexr_core = *(const int*)val;
return true;
}
if (name == "jpeg:com_attributes" && type == TypeInt) {
jpeg_com_attributes = *(const int*)val;
return true;
}
if (name == "tiff:half" && type == TypeInt) {
tiff_half = *(const int*)val;
return true;
Expand Down Expand Up @@ -520,6 +525,10 @@ getattribute(string_view name, TypeDesc type, void* val)
*(int*)val = openexr_core;
return true;
}
if (name == "jpeg:com_attributes" && type == TypeInt) {
*(int*)val = jpeg_com_attributes;
return true;
}
if (name == "tiff:half" && type == TypeInt) {
*(int*)val = tiff_half;
return true;
Expand Down
114 changes: 114 additions & 0 deletions testsuite/jpeg-metadata/ref/out.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
Reading src/blender-render.jpg
src/blender-render.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: A60D05FC42FDEE2FC8907531E3641C17D7C1E3AB
channel list: R, G, B
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Blender:Frame: "001"
Blender:RenderTime: "00:03.49"
Blender:Scene: "Scene"
Blender:Time: "00:00:00:01"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Comparing "src/blender-render.jpg" and "no-attribs.jpg"
PASS
Reading no-attribs.jpg
no-attribs.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: 329B449C07E6649023504E2C8E5130B41985CF7F
channel list: R, G, B
Exif:ColorSpace: 1
Exif:ExifVersion: "0230"
Exif:FlashPixVersion: "0100"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Reading src/blender-render.jpg
src/blender-render.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: A60D05FC42FDEE2FC8907531E3641C17D7C1E3AB
channel list: R, G, B
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Blender:Frame: "001"
Blender:RenderTime: "00:03.49"
Blender:Scene: "Scene"
Blender:Time: "00:00:00:01"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Comparing "src/blender-render.jpg" and "with-attribs.jpg"
PASS
Reading with-attribs.jpg
with-attribs.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: 329B449C07E6649023504E2C8E5130B41985CF7F
channel list: R, G, B
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Blender:Frame: "001"
Blender:RenderTime: "00:03.49"
Blender:Scene: "Scene"
Blender:Time: "00:00:00:01"
Exif:ColorSpace: 1
Exif:ExifVersion: "0230"
Exif:FlashPixVersion: "0100"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Reading src/blender-render.jpg
src/blender-render.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: A60D05FC42FDEE2FC8907531E3641C17D7C1E3AB
channel list: R, G, B
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Blender:Frame: "001"
Blender:RenderTime: "00:03.49"
Blender:Scene: "Scene"
Blender:Time: "00:00:00:01"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Comparing "src/blender-render.jpg" and "with-attribs-and-desc.jpg"
PASS
Reading with-attribs-and-desc.jpg
with-attribs-and-desc.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: 329B449C07E6649023504E2C8E5130B41985CF7F
channel list: R, G, B
ImageDescription: "A photo"
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Blender:Frame: "001"
Blender:RenderTime: "00:03.49"
Blender:Scene: "Scene"
Blender:Time: "00:00:00:01"
Exif:ColorSpace: 1
Exif:ExifVersion: "0230"
Exif:FlashPixVersion: "0100"
IPTC:Caption: "A photo"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Reading src/blender-render.jpg
src/blender-render.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: A60D05FC42FDEE2FC8907531E3641C17D7C1E3AB
channel list: R, G, B
Blender:Camera: "Camera"
Blender:Date: "2024/09/17 15:50:17"
Blender:File: "<untitled>"
Blender:Frame: "001"
Blender:RenderTime: "00:03.49"
Blender:Scene: "Scene"
Blender:Time: "00:00:00:01"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
Comparing "src/blender-render.jpg" and "with-colon-desc.jpg"
PASS
Reading with-colon-desc.jpg
with-colon-desc.jpg : 640 x 480, 3 channel, uint8 jpeg
SHA-1: 329B449C07E6649023504E2C8E5130B41985CF7F
channel list: R, G, B
ImageDescription: "Example:Text"
Exif:ColorSpace: 1
Exif:ExifVersion: "0230"
Exif:FlashPixVersion: "0100"
IPTC:Caption: "Example:Text"
jpeg:subsampling: "4:2:0"
oiio:ColorSpace: "sRGB"
38 changes: 38 additions & 0 deletions testsuite/jpeg-metadata/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python

# Copyright Contributors to the OpenImageIO project.
# SPDX-License-Identifier: Apache-2.0
# https://github.com/AcademySoftwareFoundation/OpenImageIO


redirect = ' >> out.txt 2>&1 '

# This file was rendered and saved in Blender, and therefore contains metadata
# in the form of comments.

# Check if the comments are correctly decoded as attributes, and that writing
# to a new JPEG does not include them by default.
command += rw_command ("src", "blender-render.jpg", use_oiiotool=1,
output_filename="no-attribs.jpg")
command += info_command ("no-attribs.jpg", safematch=True)

# Check that, when jpeg:com_attributes is set, the attributes are preserved.
command += rw_command ("src", "blender-render.jpg", use_oiiotool=1,
output_filename="with-attribs.jpg",
extraargs="--attrib:type=int jpeg:com_attributes 1")
command += info_command ("with-attribs.jpg", safematch=True)

# Check that JPEG comments that don't match an attribute will be read as ImageDescription.
command += rw_command ("src", "blender-render.jpg", use_oiiotool=1,
output_filename="with-attribs-and-desc.jpg",
extraargs="--attrib:type=int jpeg:com_attributes 1 "
"--attrib:type=string ImageDescription \"A photo\"")
command += info_command ("with-attribs-and-desc.jpg", safematch=True)

# Check that JPEG comments that would match an attribute will be read as ImageDescription
# if jpeg:com_attributes is 0.
command += rw_command ("src", "blender-render.jpg", use_oiiotool=1,
output_filename="with-colon-desc.jpg",
extraargs="--attrib:type=string ImageDescription \"Example:Text\"")
command += info_command ("with-colon-desc.jpg", safematch=True,
extraargs="--oiioattrib:type=int jpeg:com_attributes 0")
Binary file added testsuite/jpeg-metadata/src/blender-render.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading