Skip to content

Commit

Permalink
feat(jpeg): Support encoding/decoding arbitrary metadata as comments (#…
Browse files Browse the repository at this point in the history
…4430)

This is needed to port Blender's current JPEG IO code to using OIIO, but
is also a useful feature to have in general.

For reading, the code tries to parse comments as colon-separated
key-value pairs and sets metadata accordingly. For writing, this needs
to be explicitly enabled by setting jpeg:com_attributes to 1 in order to
avoid accidentally bloating files for existing applications.

Tests:

I've added a small (~10KB) JPEG file containing Blender metadata and a
basic test that parses it, checks that the metadata was read correctly,
writes it twice (once with and once without `jpeg:com_attributes`), and
then checks that those files are also parsed as expected.
In case you're wondering why the info for "no-attribs.jpg" still
contains one Blender attribute - that's because the first COM field is
still put into `ImageDescription` just like before, so even without
`jpeg:com_attributes` it ends up being written to the output file and
recognized during parsing.


---------

Signed-off-by: Lukas Stockner <[email protected]>
  • Loading branch information
lukasstockner authored Nov 7, 2024
1 parent de51ee6 commit 24dcdf9
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 4 deletions.
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
// 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 @@ -537,6 +542,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.

0 comments on commit 24dcdf9

Please sign in to comment.