diff --git a/src/apv.imageio/CMakeLists.txt b/src/apv.imageio/CMakeLists.txt new file mode 100644 index 0000000000..afd4eed5f0 --- /dev/null +++ b/src/apv.imageio/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +if (APV_FOUND) + add_oiio_plugin (apvinput.cpp apvoutput.cpp + INCLUDE_DIRS ${APV_INCLUDES} + LINK_LIBRARIES ${APV_LIBRARIES} + DEFINITIONS "USE_APV" ${APV_DEFINITIONS}) +else () + message (WARNING "APV plugin will not be built") +endif () diff --git a/src/apv.imageio/apv_pvt.h b/src/apv.imageio/apv_pvt.h new file mode 100644 index 0000000000..aa5dd0e237 --- /dev/null +++ b/src/apv.imageio/apv_pvt.h @@ -0,0 +1,161 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +// Shared helpers for the APV (OpenAPV) reader and writer. + +#pragma once + +#include + +#include + +OIIO_PLUGIN_NAMESPACE_BEGIN + +namespace apv_pvt { + +// Raw APV bitstream files are a sequence of [4-byte big-endian AU size] +// [access unit]. Each AU begins with the 4-byte signature "aPv1". +inline constexpr unsigned char apv_signature[4] = { 0x61, 0x50, 0x76, 0x31 }; + +// Luma coefficients for the YCbCr matrices we support, by CICP +// MatrixCoefficients code (H.273). Returns false for codes we don't +// handle (callers should fall back to BT.709). +inline bool +matrix_luma_coefficients(int matrix_coefficients, float& kr, float& kb) +{ + switch (matrix_coefficients) { + case 1: // BT.709 + kr = 0.2126f; + kb = 0.0722f; + return true; + case 5: // BT.601 (625) + case 6: // BT.601 (525) + kr = 0.299f; + kb = 0.114f; + return true; + case 9: // BT.2020 non-constant luminance + case 10: // BT.2020 constant luminance (treated as NCL here) + kr = 0.2627f; + kb = 0.0593f; + return true; + default: return false; + } +} + + + +// Number of image channels our plugin exposes for an APV color format. +// Returns 0 for formats we do not support. +inline int +channels_for_format(int color_format) +{ + switch (color_format) { + case OAPV_CF_YCBCR400: return 1; + case OAPV_CF_YCBCR420: + case OAPV_CF_YCBCR422: + case OAPV_CF_YCBCR444: return 3; + case OAPV_CF_YCBCR4444: return 4; + default: return 0; + } +} + + + +// Scale an n-bit code value up to the full 16-bit range with bit +// replication (the same convention other OIIO readers use for 10/12 bit +// media). +inline uint16_t +scale_to_16bits(uint32_t v, int bits) +{ + return uint16_t((v << (16 - bits)) | (v >> (2 * bits - 16))); +} + + + +// Minimal oapv_imgb allocator, mirroring the reference application: the +// library does not export one, callers own the frame buffers. Plane +// dimensions are macroblock aligned as the library requires. + +inline int +imgb_addref(oapv_imgb_t* imgb) +{ + return ++imgb->refcnt; +} + +inline int +imgb_getref(oapv_imgb_t* imgb) +{ + return imgb->refcnt; +} + +inline int +imgb_release(oapv_imgb_t* imgb) +{ + int refcnt = --imgb->refcnt; + if (refcnt == 0) { + for (int i = 0; i < OAPV_MAX_CC; i++) + free(imgb->baddr[i]); + free(imgb); + } + return refcnt; +} + +inline oapv_imgb_t* +imgb_create(int w, int h, int cs) +{ + oapv_imgb_t* imgb = (oapv_imgb_t*)calloc(1, sizeof(oapv_imgb_t)); + if (!imgb) + return nullptr; + int bd = OAPV_CS_GET_BYTE_DEPTH(cs); + imgb->w[0] = w; + imgb->h[0] = h; + switch (OAPV_CS_GET_FORMAT(cs)) { + case OAPV_CF_YCBCR400: imgb->np = 1; break; + case OAPV_CF_YCBCR420: + imgb->w[1] = imgb->w[2] = (w + 1) >> 1; + imgb->h[1] = imgb->h[2] = (h + 1) >> 1; + imgb->np = 3; + break; + case OAPV_CF_YCBCR422: + imgb->w[1] = imgb->w[2] = (w + 1) >> 1; + imgb->h[1] = imgb->h[2] = h; + imgb->np = 3; + break; + case OAPV_CF_YCBCR444: + imgb->w[1] = imgb->w[2] = w; + imgb->h[1] = imgb->h[2] = h; + imgb->np = 3; + break; + case OAPV_CF_YCBCR4444: + imgb->w[1] = imgb->w[2] = imgb->w[3] = w; + imgb->h[1] = imgb->h[2] = imgb->h[3] = h; + imgb->np = 4; + break; + default: free(imgb); return nullptr; + } + for (int i = 0; i < imgb->np; i++) { + imgb->aw[i] = ((imgb->w[i] + OAPV_MB_W - 1) / OAPV_MB_W) * OAPV_MB_W; + imgb->ah[i] = ((imgb->h[i] + OAPV_MB_H - 1) / OAPV_MB_H) * OAPV_MB_H; + imgb->s[i] = imgb->aw[i] * bd; + imgb->e[i] = imgb->ah[i]; + imgb->bsize[i] = imgb->s[i] * imgb->e[i]; + imgb->a[i] = imgb->baddr[i] = calloc(1, imgb->bsize[i]); + if (!imgb->a[i]) { + for (int j = 0; j < i; j++) + free(imgb->baddr[j]); + free(imgb); + return nullptr; + } + } + imgb->cs = cs; + imgb->addref = imgb_addref; + imgb->getref = imgb_getref; + imgb->release = imgb_release; + imgb->addref(imgb); + return imgb; +} + +} // namespace apv_pvt + +OIIO_PLUGIN_NAMESPACE_END diff --git a/src/apv.imageio/apvinput.cpp b/src/apv.imageio/apvinput.cpp new file mode 100644 index 0000000000..78e57538fc --- /dev/null +++ b/src/apv.imageio/apvinput.cpp @@ -0,0 +1,424 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +// APV (Advanced Professional Video) reader, using the OpenAPV library. +// +// https://github.com/openapv/openapv +// +// APV is an all-intra professional video codec: every frame is coded +// independently, so a raw .apv bitstream is naturally addressable as a +// sequence of individual images. We expose one subimage per access unit +// (AU), decoding its primary frame. Auxiliary frames within an AU +// (preview, depth, alpha, non-primary) are not currently exposed. + +#include +#include +#include + +#include +#include +#include +#include + +#include "apv_pvt.h" + +OIIO_PLUGIN_NAMESPACE_BEGIN + +using namespace apv_pvt; + +class ApvInput final : public ImageInput { +public: + ApvInput() { init(); } + ~ApvInput() override { close(); } + const char* format_name(void) const override { return "apv"; } + int supports(string_view feature) const override + { + return feature == "ioproxy"; + } + bool valid_file(Filesystem::IOProxy* ioproxy) const override; + bool open(const std::string& name, ImageSpec& newspec) override; + bool open(const std::string& name, ImageSpec& newspec, + const ImageSpec& config) override; + int current_subimage(void) const override + { + lock_guard lock(*this); + return m_subimage; + } + bool seek_subimage(int subimage, int miplevel) override; + bool read_native_scanline(int subimage, int miplevel, int y, int z, + void* data) override; + bool close() override; + +private: + struct AuIndexEntry { + int64_t offset; // file offset of the AU payload (past the size field) + uint32_t size; // byte size of the AU payload + }; + + std::string m_filename; + std::vector m_au_index; + oapvd_t m_decoder; + int m_subimage; // Current subimage (== AU) index + std::vector m_pixels; // Decoded, converted RGB(A)/Y pixels + std::vector m_au; // Raw bytes of the current AU + + void init() + { + ioproxy_clear(); + m_filename.clear(); + m_au_index.clear(); + m_decoder = nullptr; + m_subimage = -1; + m_pixels.clear(); + m_au.clear(); + } + + // Read the 4-byte big-endian AU size fields to build the AU index. + bool index_aus(Filesystem::IOProxy* io); + // Decode AU number `subimage` and fill m_spec and m_pixels. + bool decode_au(int subimage); +}; + + + +// Obligatory material to make this a recognizable imageio plugin: +OIIO_PLUGIN_EXPORTS_BEGIN + +OIIO_EXPORT ImageInput* +apv_input_imageio_create() +{ + return new ApvInput; +} + +OIIO_EXPORT int apv_imageio_version = OIIO_PLUGIN_VERSION; + +OIIO_EXPORT const char* +apv_imageio_library_version() +{ + return "OpenAPV " OIIO_STRINGIZE(OAPV_VER_APISET) "." OIIO_STRINGIZE( + OAPV_VER_MAJOR) "." OIIO_STRINGIZE(OAPV_VER_MINOR) "." OIIO_STRINGIZE(OAPV_VER_PATCH); +} + +OIIO_EXPORT const char* apv_input_extensions[] = { "apv", nullptr }; + +OIIO_PLUGIN_EXPORTS_END + + + +bool +ApvInput::valid_file(Filesystem::IOProxy* ioproxy) const +{ + // A raw APV bitstream starts with a 4-byte AU size followed by the + // 4-byte AU signature "aPv1". + if (!ioproxy || ioproxy->mode() != Filesystem::IOProxy::Read) + return false; + unsigned char header[8]; + if (ioproxy->pread(header, sizeof(header), 0) != sizeof(header)) + return false; + return memcmp(header + 4, apv_signature, sizeof(apv_signature)) == 0; +} + + + +bool +ApvInput::open(const std::string& name, ImageSpec& newspec, + const ImageSpec& config) +{ + ioproxy_retrieve_from_config(config); + return open(name, newspec); +} + + + +bool +ApvInput::open(const std::string& name, ImageSpec& newspec) +{ + m_filename = name; + if (!ioproxy_use_or_open(name)) + return false; + Filesystem::IOProxy* io = ioproxy(); + if (!valid_file(io)) { + errorfmt("\"{}\" is not an APV bitstream", name); + close(); + return false; + } + if (!index_aus(io)) { + close(); + return false; + } + + oapvd_cdesc_t cdesc; + memset(&cdesc, 0, sizeof(cdesc)); + cdesc.threads = OAPV_CDESC_THREADS_AUTO; + int err = OAPV_OK; + m_decoder = oapvd_create(&cdesc, &err); + if (m_decoder == nullptr) { + errorfmt("Could not create APV decoder (error {})", err); + close(); + return false; + } + + if (!seek_subimage(0, 0)) { + close(); + return false; + } + newspec = m_spec; + return true; +} + + + +bool +ApvInput::index_aus(Filesystem::IOProxy* io) +{ + int64_t pos = 0; + const int64_t size = io->size(); + while (pos + 4 <= size) { + unsigned char szbuf[4]; + if (io->pread(szbuf, 4, pos) != 4) { + errorfmt("Truncated AU size field at byte {}", pos); + return false; + } + uint32_t au_size = (uint32_t(szbuf[0]) << 24) + | (uint32_t(szbuf[1]) << 16) + | (uint32_t(szbuf[2]) << 8) | uint32_t(szbuf[3]); + pos += 4; + if (au_size == 0 || pos + au_size > size) { + errorfmt("Corrupt AU size {} at byte {}", au_size, pos - 4); + return false; + } + m_au_index.push_back({ pos, au_size }); + pos += au_size; + } + if (m_au_index.empty()) { + errorfmt("No access units found"); + return false; + } + return true; +} + + + +bool +ApvInput::seek_subimage(int subimage, int miplevel) +{ + if (miplevel != 0 || subimage < 0 || subimage >= (int)m_au_index.size()) + return false; + if (subimage == m_subimage) + return true; + if (!decode_au(subimage)) + return false; + m_subimage = subimage; + return true; +} + + + +bool +ApvInput::decode_au(int subimage) +{ + Filesystem::IOProxy* io = ioproxy(); + const AuIndexEntry& au = m_au_index[subimage]; + m_au.resize(au.size); + if (io->pread(m_au.data(), au.size, au.offset) != au.size) { + errorfmt("Could not read AU {}", subimage); + return false; + } + + oapv_au_info_t aui; + if (oapvd_info(m_au.data(), (int)au.size, &aui) != OAPV_OK) { + errorfmt("Could not parse AU {}", subimage); + return false; + } + if (aui.num_frms <= 0 || aui.num_frms > OAPV_MAX_NUM_FRAMES) { + errorfmt("AU {} has invalid frame count {}", subimage, aui.num_frms); + return false; + } + + // Find the primary frame in this AU. + int primary = -1; + for (int i = 0; i < aui.num_frms; i++) { + if (aui.frm_info[i].pbu_type == OAPV_PBU_TYPE_PRIMARY_FRAME) { + primary = i; + break; + } + } + if (primary < 0) + primary = 0; // fall back to the first frame + const oapv_frm_info_t& finfo = aui.frm_info[primary]; + + const int fmt = OAPV_CS_GET_FORMAT(finfo.cs); + const int bits = OAPV_CS_GET_BIT_DEPTH(finfo.cs); + const int channels = channels_for_format(fmt); + if (channels == 0 || bits < 10 || bits > 16) { + errorfmt("Unsupported APV color format ({}) or bit depth ({})", fmt, + bits); + return false; + } + + // Decode the AU. The decoder requires caller-allocated buffers for + // every frame in the AU, not just the one we want. + oapv_frms_t ofrms; + memset(&ofrms, 0, sizeof(ofrms)); + ofrms.num_frms = aui.num_frms; + for (int i = 0; i < aui.num_frms; i++) { + ofrms.frm[i].imgb = imgb_create(aui.frm_info[i].w, aui.frm_info[i].h, + aui.frm_info[i].cs); + if (!ofrms.frm[i].imgb) { + for (int j = 0; j < i; j++) + ofrms.frm[j].imgb->release(ofrms.frm[j].imgb); + errorfmt("Could not allocate frame buffers for AU {}", subimage); + return false; + } + } + oapv_bitb_t bitb; + memset(&bitb, 0, sizeof(bitb)); + bitb.addr = m_au.data(); + bitb.bsize = (int)au.size; + bitb.ssize = (int)au.size; + oapvd_stat_t stat; + memset(&stat, 0, sizeof(stat)); + int ret = oapvd_decode(m_decoder, &bitb, &ofrms, nullptr, &stat); + if (ret != OAPV_OK) { + for (int i = 0; i < ofrms.num_frms; i++) + ofrms.frm[i].imgb->release(ofrms.frm[i].imgb); + errorfmt("APV decode failed for AU {} (error {})", subimage, ret); + return false; + } + + // Build the spec for this subimage. + m_spec = ImageSpec(finfo.w, finfo.h, channels, TypeDesc::UINT16); + m_spec.attribute("oiio:BitsPerSample", bits); + m_spec.attribute("apv:profile", finfo.profile_idc); + m_spec.attribute("apv:level", finfo.level_idc); + // N.B. Take the color description from the decode's frame info, not + // from oapvd_info(): OpenAPV's lightweight header probe misparses the + // color description fields (verified against its own reference + // encoder; the full decoder parses them correctly). + const oapv_frm_info_t& dinfo = stat.aui.frm_info[primary]; + float kr = 0.2126f, kb = 0.0722f; // default BT.709 + bool full_range = false; + if (dinfo.color_description_present_flag) { + matrix_luma_coefficients(dinfo.matrix_coefficients, kr, kb); + full_range = dinfo.full_range_flag; + // The pixels we return are RGB (matrix 0) and full range after + // conversion; the primaries and transfer carry over unchanged. + const int cicp[4] = { dinfo.color_primaries, + dinfo.transfer_characteristics, 0 /* RGB */, + 1 /* full range */ }; + m_spec.attribute("CICP", TypeDesc(TypeDesc::INT, 4), cicp); + const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); + string_view interop_id = colorconfig.get_color_interop_id(cicp); + if (!interop_id.empty()) + m_spec.attribute("oiio:ColorSpace", interop_id); + m_spec.attribute("apv:matrix_coefficients", + (int)dinfo.matrix_coefficients); + m_spec.attribute("apv:full_range_flag", dinfo.full_range_flag); + } + + // Convert the primary frame to interleaved RGB(A)/Y uint16 pixels. + const oapv_imgb_t* imgb = ofrms.frm[primary].imgb; + const int w = finfo.w, h = finfo.h; + const float maxval = float((1 << bits) - 1); + const float lo = full_range ? 0.0f : float(16 << (bits - 8)); + const float yrange = full_range ? maxval : float(219 << (bits - 8)); + const float crange = full_range ? maxval : float(224 << (bits - 8)); + const float chalf = float(1 << (bits - 1)); + const float kg = 1.0f - kr - kb; + const int subx = (fmt == OAPV_CF_YCBCR420 || fmt == OAPV_CF_YCBCR422); + const int suby = (fmt == OAPV_CF_YCBCR420); + + m_pixels.resize(size_t(w) * h * channels); + auto plane = [&](int p) { + return reinterpret_cast(imgb->a[p]); + }; + auto pstride = [&](int p) { return imgb->s[p] / 2; }; + + for (int y = 0; y < h; y++) { + uint16_t* dst = m_pixels.data() + size_t(y) * w * channels; + const uint16_t* yrow = plane(0) + size_t(y) * pstride(0); + if (channels == 1) { + for (int x = 0; x < w; x++) { + float Y = (yrow[x] - lo) / yrange; + dst[x] = uint16_t(clamp(Y, 0.0f, 1.0f) * 65535.0f + 0.5f); + } + continue; + } + const int cy = suby ? (y >> 1) : y; + const uint16_t* cbrow = plane(1) + size_t(cy) * pstride(1); + const uint16_t* crrow = plane(2) + size_t(cy) * pstride(2); + const uint16_t* arow = channels == 4 ? plane(3) + size_t(y) * pstride(3) + : nullptr; + const int cw = imgb->w[1]; + for (int x = 0; x < w; x++) { + float cb, cr; + if (subx) { + // Simple co-sited linear interpolation of chroma. + // ponytail: nearest+average; proper resampling filters can + // come later if quality demands. + int cx = x >> 1; + if (x & 1) { + int cx1 = std::min(cx + 1, cw - 1); + cb = 0.5f * (cbrow[cx] + cbrow[cx1]); + cr = 0.5f * (crrow[cx] + crrow[cx1]); + } else { + cb = cbrow[cx]; + cr = crrow[cx]; + } + } else { + cb = cbrow[x]; + cr = crrow[x]; + } + float Y = (yrow[x] - lo) / yrange; + float pb = (cb - chalf) / crange; + float pr = (cr - chalf) / crange; + float r = Y + 2.0f * (1.0f - kr) * pr; + float b = Y + 2.0f * (1.0f - kb) * pb; + float g = (Y - kr * r - kb * b) / kg; + uint16_t* px = dst + size_t(x) * channels; + px[0] = uint16_t(clamp(r, 0.0f, 1.0f) * 65535.0f + 0.5f); + px[1] = uint16_t(clamp(g, 0.0f, 1.0f) * 65535.0f + 0.5f); + px[2] = uint16_t(clamp(b, 0.0f, 1.0f) * 65535.0f + 0.5f); + if (arow) // alpha is carried full range + px[3] = scale_to_16bits(arow[x], bits); + } + } + + for (int i = 0; i < ofrms.num_frms; i++) + ofrms.frm[i].imgb->release(ofrms.frm[i].imgb); + return true; +} + + + +bool +ApvInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/, + void* data) +{ + lock_guard lock(*this); + if (!seek_subimage(subimage, miplevel)) + return false; + if (y < 0 || y >= m_spec.height) + return false; + size_t scanline_bytes = size_t(m_spec.width) * m_spec.nchannels + * sizeof(uint16_t); + memcpy(data, m_pixels.data() + size_t(y) * m_spec.width * m_spec.nchannels, + scanline_bytes); + return true; +} + + + +bool +ApvInput::close() +{ + if (m_decoder) { + oapvd_delete(m_decoder); + m_decoder = nullptr; + } + init(); + return true; +} + +OIIO_PLUGIN_NAMESPACE_END diff --git a/src/apv.imageio/apvoutput.cpp b/src/apv.imageio/apvoutput.cpp new file mode 100644 index 0000000000..f423b7c17c --- /dev/null +++ b/src/apv.imageio/apvoutput.cpp @@ -0,0 +1,434 @@ +// Copyright Contributors to the OpenImageIO project. +// SPDX-License-Identifier: Apache-2.0 +// https://github.com/AcademySoftwareFoundation/OpenImageIO + +// APV (Advanced Professional Video) writer, using the OpenAPV library. +// +// Each subimage is encoded as one access unit (AU) containing a single +// primary frame, so appending subimages produces a valid multi-frame APV +// bitstream. + +#include +#include +#include + +#include +#include +#include +#include + +#include "apv_pvt.h" + +OIIO_PLUGIN_NAMESPACE_BEGIN + +using namespace apv_pvt; + +class ApvOutput final : public ImageOutput { +public: + ApvOutput() { init(); } + ~ApvOutput() override { close(); } + const char* format_name(void) const override { return "apv"; } + int supports(string_view feature) const override + { + return (feature == "alpha" || feature == "multiimage" + || feature == "appendsubimage" || feature == "ioproxy"); + } + bool open(const std::string& name, const ImageSpec& spec, + OpenMode mode = Create) override; + bool write_scanline(int y, int z, TypeDesc format, const void* data, + stride_t xstride) override; + bool write_scanlines(int ybegin, int yend, int z, TypeDesc format, + const void* data, stride_t xstride, + stride_t ystride) override; + bool close() override; + +private: + std::string m_filename; + std::vector m_staging; // Interleaved float pixels for one frame + int m_color_format; // OAPV_CF_* for the frame being written + int m_bits; // encoded bit depth + int m_profile_idc; + bool m_dirty; // any scanlines written since open? + + void init() + { + ioproxy_clear(); + m_filename.clear(); + m_staging.clear(); + m_color_format = OAPV_CF_UNKNOWN; + m_bits = 0; + m_profile_idc = 0; + m_dirty = false; + } + + bool setup_profile(); + bool finalize_subimage(); +}; + + + +// Obligatory material to make this a recognizable imageio plugin: +OIIO_PLUGIN_EXPORTS_BEGIN + +OIIO_EXPORT ImageOutput* +apv_output_imageio_create() +{ + return new ApvOutput; +} + +OIIO_EXPORT const char* apv_output_extensions[] = { "apv", nullptr }; + +OIIO_PLUGIN_EXPORTS_END + + + +// Map a profile name to (profile_idc, color format, bit depth). +// The supported profile names match OpenAPV's own option strings. +struct ApvProfileDef { + const char* name; + int idc; + int color_format; + int bits; +}; +static const ApvProfileDef apv_profiles[] = { + { "422-10", OAPV_PROFILE_422_10, OAPV_CF_YCBCR422, 10 }, + { "422-12", OAPV_PROFILE_422_12, OAPV_CF_YCBCR422, 12 }, + { "444-10", OAPV_PROFILE_444_10, OAPV_CF_YCBCR444, 10 }, + { "444-12", OAPV_PROFILE_444_12, OAPV_CF_YCBCR444, 12 }, + { "4444-10", OAPV_PROFILE_4444_10, OAPV_CF_YCBCR4444, 10 }, + { "4444-12", OAPV_PROFILE_4444_12, OAPV_CF_YCBCR4444, 12 }, + { "400-10", OAPV_PROFILE_400_10, OAPV_CF_YCBCR400, 10 }, +}; + + + +bool +ApvOutput::setup_profile() +{ + string_view profile = m_spec.get_string_attribute("apv:profile", ""); + if (profile.empty()) { + if (m_spec.nchannels == 1) + profile = "400-10"; + else if (m_spec.nchannels == 4) + profile = "4444-10"; + else + profile = "422-10"; + } + for (const auto& p : apv_profiles) { + if (profile == p.name) { + m_profile_idc = p.idc; + m_color_format = p.color_format; + m_bits = p.bits; + const int want = channels_for_format(p.color_format); + if (m_spec.nchannels != want) { + errorfmt( + "APV profile \"{}\" requires {} channels, but image has {}", + profile, want, m_spec.nchannels); + return false; + } + return true; + } + } + errorfmt("Unknown APV profile \"{}\"", profile); + return false; +} + + + +bool +ApvOutput::open(const std::string& name, const ImageSpec& userspec, + OpenMode mode) +{ + if (mode == AppendMIPLevel) { + errorfmt("APV does not support MIP levels"); + return false; + } + if (mode == AppendSubimage) { + if (!finalize_subimage()) + return false; + m_spec = userspec; + if (!setup_profile()) + return false; + m_staging.assign(size_t(m_spec.width) * m_spec.height + * m_spec.nchannels, + 0.0f); + m_dirty = false; + return true; + } + + m_filename = name; + m_spec = userspec; + if (!check_open(mode, userspec, { 0, 1 << 20, 0, 1 << 20, 0, 1, 0, 4 })) + return false; + if (m_spec.depth > 1) { + errorfmt("APV does not support volume images"); + return false; + } + if (!setup_profile()) + return false; + if (!ioproxy_use_or_open(name)) + return false; + m_staging.assign(size_t(m_spec.width) * m_spec.height * m_spec.nchannels, + 0.0f); + m_dirty = false; + return true; +} + + + +bool +ApvOutput::write_scanline(int y, int z, TypeDesc format, const void* data, + stride_t xstride) +{ + return write_scanlines(y, y + 1, z, format, data, xstride, AutoStride); +} + + + +bool +ApvOutput::write_scanlines(int ybegin, int yend, int z, TypeDesc format, + const void* data, stride_t xstride, stride_t ystride) +{ + if (z != 0 || ybegin < 0 || yend > m_spec.height) { + errorfmt("Scanline range [{}, {}) out of bounds", ybegin, yend); + return false; + } + stride_t pixelsize = stride_t(m_spec.nchannels * format.size()); + if (xstride == AutoStride) + xstride = pixelsize; + if (ystride == AutoStride) + ystride = xstride * m_spec.width; + for (int y = ybegin; y < yend; y++) { + const char* src = (const char*)data + (y - ybegin) * ystride; + float* dst = m_staging.data() + + size_t(y) * m_spec.width * m_spec.nchannels; + OIIO::convert_image(m_spec.nchannels, m_spec.width, 1, 1, src, format, + xstride, AutoStride, AutoStride, dst, + TypeDesc::FLOAT, AutoStride, AutoStride, + AutoStride); + } + m_dirty = true; + return true; +} + + + +bool +ApvOutput::finalize_subimage() +{ + if (!m_dirty) + return true; + + const int w = m_spec.width; + const int h = m_spec.height; + const int channels = m_spec.nchannels; + const int bits = m_bits; + const int cs = OAPV_CS_SET(m_color_format, bits, 0); + + // Color handling, following Color Interop Forum conventions: an + // explicit CICP attribute wins; otherwise, if oiio:ColorSpace names + // a color space the default ColorConfig can map to CICP code points + // (e.g. a color interop ID), derive them from it. If neither yields + // anything, no color description is signaled ("don't guess") and + // BT.709 limited range is used for the YCbCr conversion. + int color_present = 0, color_primaries = 2, color_transfer = 2; + int full_range = 0; + if (const ParamValue* p = m_spec.find_attribute("CICP")) { + if (p->type() == TypeDesc(TypeDesc::INT, 4)) { + const int* cicp = (const int*)p->data(); + color_present = 1; + color_primaries = cicp[0]; + color_transfer = cicp[1]; + full_range = cicp[3] ? 1 : 0; + } + } else { + string_view csname = m_spec.get_string_attribute("oiio:ColorSpace"); + if (!csname.empty()) { + const ColorConfig& colorconfig(ColorConfig::default_colorconfig()); + cspan cicp = colorconfig.get_cicp(csname); + if (cicp.size() == 4) { + color_present = 1; + color_primaries = cicp[0]; + color_transfer = cicp[1]; + // The registry tuple describes full-range RGB pixels; + // our YCbCr carrier follows video convention instead: + // limited range. + full_range = 0; + } + } + } + // Choose matrix coefficients to match the primaries, per common + // practice: BT.2020 primaries pair with the 2020 non-constant + // luminance matrix, the BT.601 families with their own matrices, + // and everything else (including P3, conventionally) with BT.709. + int matrix = 1; + if (color_present) { + if (color_primaries == 9) + matrix = 9; + else if (color_primaries == 5) + matrix = 5; + else if (color_primaries == 6) + matrix = 6; + } + float kr = 0.2126f, kb = 0.0722f; + matrix_luma_coefficients(matrix, kr, kb); + const float kg = 1.0f - kr - kb; + const float maxval = float((1 << bits) - 1); + const float lo = full_range ? 0.0f : float(16 << (bits - 8)); + const float yrange = full_range ? maxval : float(219 << (bits - 8)); + const float crange = full_range ? maxval : float(224 << (bits - 8)); + const float chalf = float(1 << (bits - 1)); + + // Allocate and fill the input frame. + oapv_imgb_t* imgb = imgb_create(w, h, cs); + if (!imgb) { + errorfmt("Could not allocate APV frame buffer"); + return false; + } + auto plane = [&](int p) { return reinterpret_cast(imgb->a[p]); }; + auto pstride = [&](int p) { return imgb->s[p] / 2; }; + auto quant = [&](float v, float scale, float offset) -> uint16_t { + float q = offset + v * scale; + return uint16_t(clamp(q, 0.0f, maxval) + 0.5f); + }; + + const bool subx = (m_color_format == OAPV_CF_YCBCR422); + for (int y = 0; y < h; y++) { + const float* src = m_staging.data() + size_t(y) * w * channels; + uint16_t* yrow = plane(0) + size_t(y) * pstride(0); + if (channels == 1) { + for (int x = 0; x < w; x++) + yrow[x] = quant(clamp(src[x], 0.0f, 1.0f), yrange, lo); + continue; + } + uint16_t* cbrow = plane(1) + size_t(y) * pstride(1); + uint16_t* crrow = plane(2) + size_t(y) * pstride(2); + uint16_t* arow = channels == 4 ? plane(3) + size_t(y) * pstride(3) + : nullptr; + const int cw = imgb->w[1]; + for (int cx = 0; cx < cw; cx++) { + // For 4:2:2, box-filter each horizontal pair of pixels. + int x0 = subx ? std::min(2 * cx, w - 1) : cx; + int x1 = subx ? std::min(2 * cx + 1, w - 1) : cx; + float r = 0.5f * (src[x0 * channels + 0] + src[x1 * channels + 0]); + float g = 0.5f * (src[x0 * channels + 1] + src[x1 * channels + 1]); + float b = 0.5f * (src[x0 * channels + 2] + src[x1 * channels + 2]); + float Y = kr * r + kg * g + kb * b; + float pb = (b - Y) / (2.0f * (1.0f - kb)); + float pr = (r - Y) / (2.0f * (1.0f - kr)); + cbrow[cx] = quant(pb, crange, chalf); + crrow[cx] = quant(pr, crange, chalf); + } + for (int x = 0; x < w; x++) { + float r = src[x * channels + 0]; + float g = src[x * channels + 1]; + float b = src[x * channels + 2]; + float Y = kr * r + kg * g + kb * b; + yrow[x] = quant(clamp(Y, 0.0f, 1.0f), yrange, lo); + if (arow) + arow[x] = uint16_t( + clamp(src[x * channels + 3], 0.0f, 1.0f) * maxval + 0.5f); + } + } + + // Set up the encoder. + oapve_cdesc_t cdesc; + memset(&cdesc, 0, sizeof(cdesc)); + cdesc.max_bs_buf_size = w * h * channels * 4 + (1 << 20); + cdesc.max_num_frms = 1; + cdesc.threads = OAPV_CDESC_THREADS_AUTO; + oapve_param_t& param = cdesc.param[0]; + oapve_param_default(¶m); + param.profile_idc = m_profile_idc; + param.w = w; + param.h = h; + int fps[2] = { 30, 1 }; + if (const ParamValue* p = m_spec.find_attribute("FramesPerSecond", + TypeRational)) { + const int* r = (const int*)p->data(); + if (r[0] > 0 && r[1] > 0) { + fps[0] = r[0]; + fps[1] = r[1]; + } + } + param.fps_num = fps[0]; + param.fps_den = fps[1]; + int qp = m_spec.get_int_attribute("apv:qp", -1); + if (qp >= 0) + param.qp = (unsigned char)qp; + int bitrate = m_spec.get_int_attribute("apv:bitrate", 0); + if (bitrate > 0) { + param.bitrate = bitrate; + param.rc_type = OAPV_RC_ABR; + } + string_view preset = m_spec.get_string_attribute("apv:preset", ""); + if (!preset.empty()) { + for (const oapv_dict_str_int_t* d = oapv_param_opts_preset; d->key[0]; + d++) { + if (preset == d->key) { + param.preset = d->val; + break; + } + } + } + param.color_description_present_flag = color_present; + if (color_present) { + param.color_primaries = (unsigned char)color_primaries; + param.transfer_characteristics = (unsigned char)color_transfer; + param.matrix_coefficients = (unsigned char)matrix; + param.full_range_flag = full_range; + } + + int err = OAPV_OK; + oapve_t encoder = oapve_create(&cdesc, &err); + if (encoder == nullptr) { + imgb->release(imgb); + errorfmt("Could not create APV encoder (error {})", err); + return false; + } + + std::vector bsbuf(cdesc.max_bs_buf_size); + oapv_bitb_t bitb; + memset(&bitb, 0, sizeof(bitb)); + bitb.addr = bsbuf.data(); + bitb.bsize = (int)bsbuf.size(); + + oapv_frms_t ifrms, rfrms; + memset(&ifrms, 0, sizeof(ifrms)); + memset(&rfrms, 0, sizeof(rfrms)); + ifrms.num_frms = 1; + ifrms.frm[0].imgb = imgb; + ifrms.frm[0].pbu_type = OAPV_PBU_TYPE_PRIMARY_FRAME; + ifrms.frm[0].group_id = 1; + oapve_stat_t stat; + memset(&stat, 0, sizeof(stat)); + + int ret = oapve_encode(encoder, &ifrms, nullptr, &bitb, &stat, &rfrms); + oapve_delete(encoder); + imgb->release(imgb); + if (ret != OAPV_OK || stat.write <= 0) { + errorfmt("APV encode failed (error {})", ret); + return false; + } + + // The encoder's output already carries the raw bitstream framing + // ([4-byte big-endian AU size][AU]), so write it verbatim. + if (!iowrite(bsbuf.data(), stat.write)) + return false; + m_dirty = false; + return true; +} + + + +bool +ApvOutput::close() +{ + if (!ioproxy_opened()) // already closed + return true; + bool ok = finalize_subimage(); + init(); + return ok; +} + +OIIO_PLUGIN_NAMESPACE_END diff --git a/src/cmake/build_APV.cmake b/src/cmake/build_APV.cmake new file mode 100644 index 0000000000..d3221a5728 --- /dev/null +++ b/src/cmake/build_APV.cmake @@ -0,0 +1,45 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +###################################################################### +# APV (OpenAPV) by hand! +###################################################################### + +set_cache (APV_BUILD_VERSION 0.3.0.0 "OpenAPV version for local builds") +set (APV_GIT_REPOSITORY "https://github.com/openapv/openapv") +set_cache (APV_GIT_TAG "v${APV_BUILD_VERSION}" "Git branch or tag") +set_cache (APV_GIT_COMMIT "9f6fd2a7369db90acec67d99fc57724f1136fb84" + "commit hash to verify tag against") +set_cache (APV_BUILD_SHARED_LIBS ${LOCAL_BUILD_SHARED_LIBS_DEFAULT} + DOC "Should a local APV build, if necessary, build shared libraries" ADVANCED) + +if (APV_BUILD_SHARED_LIBS) + set (_apv_lib_args -D OAPV_BUILD_SHARED_LIB=ON -D OAPV_BUILD_STATIC_LIB=OFF) +else () + set (_apv_lib_args -D OAPV_BUILD_SHARED_LIB=OFF -D OAPV_BUILD_STATIC_LIB=ON) +endif () + +build_dependency_with_cmake(APV + VERSION ${APV_BUILD_VERSION} + GIT_REPOSITORY ${APV_GIT_REPOSITORY} + GIT_TAG ${APV_GIT_TAG} + GIT_COMMIT ${APV_GIT_COMMIT} + CMAKE_ARGS + -D CMAKE_POSITION_INDEPENDENT_CODE=ON + -D CMAKE_INSTALL_LIBDIR=lib + -D OAPV_BUILD_APPS=OFF + ${_apv_lib_args} + ) +unset (_apv_lib_args) + +set (APV_ROOT ${APV_LOCAL_INSTALL_DIR}) + +# Signal to caller that we need to find again at the installed location. +# N.B. the library reports its own apiset-based version from oapv.h, which +# does not match the release tag, so don't pass a REFIND_VERSION here. +set (APV_REFIND TRUE) + +if (APV_BUILD_SHARED_LIBS) + install_local_dependency_libs (APV oapv) +endif () diff --git a/src/cmake/externalpackages.cmake b/src/cmake/externalpackages.cmake index bcf0ca921c..9b4f1dfbb0 100644 --- a/src/cmake/externalpackages.cmake +++ b/src/cmake/externalpackages.cmake @@ -100,6 +100,11 @@ checked_find_package (JXL VERSION_MIN 0.10.1 DEFINITIONS USE_JXL=1) +# APV (Advanced Professional Video) via the OpenAPV library +checked_find_package (APV + VERSION_MIN 1.0 + DEFINITIONS USE_APV=1) + # Pugixml setup. Normally we just use the version bundled with oiio, but # some linux distros are quite particular about having separate packages so we # allow this to be overridden to use the distro-provided package if desired. diff --git a/src/cmake/modules/FindAPV.cmake b/src/cmake/modules/FindAPV.cmake new file mode 100644 index 0000000000..95895c65c3 --- /dev/null +++ b/src/cmake/modules/FindAPV.cmake @@ -0,0 +1,55 @@ +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO +# +# Module to find the OpenAPV library (APV codec) +# https://github.com/openapv/openapv +# +# Will define: +# - APV_FOUND +# - APV_INCLUDES directory to include for oapv headers +# - APV_LIBRARIES libraries to link to +# - APV_VERSION library version (apiset.major.minor.patch from oapv.h) + +include (FindPackageHandleStandardArgs) + +find_path(APV_INCLUDE_DIR + NAMES oapv/oapv.h) +mark_as_advanced(APV_INCLUDE_DIR) + +if (APV_INCLUDE_DIR) + file (STRINGS "${APV_INCLUDE_DIR}/oapv/oapv.h" TMP REGEX "^#define OAPV_VER_APISET .*$") + string (REGEX MATCHALL "[0-9]+" OAPV_VER_APISET ${TMP}) + file (STRINGS "${APV_INCLUDE_DIR}/oapv/oapv.h" TMP REGEX "^#define OAPV_VER_MAJOR .*$") + string (REGEX MATCHALL "[0-9]+" OAPV_VER_MAJOR ${TMP}) + file (STRINGS "${APV_INCLUDE_DIR}/oapv/oapv.h" TMP REGEX "^#define OAPV_VER_MINOR .*$") + string (REGEX MATCHALL "[0-9]+" OAPV_VER_MINOR ${TMP}) + file (STRINGS "${APV_INCLUDE_DIR}/oapv/oapv.h" TMP REGEX "^#define OAPV_VER_PATCH .*$") + string (REGEX MATCHALL "[0-9]+" OAPV_VER_PATCH ${TMP}) + set (APV_VERSION "${OAPV_VER_APISET}.${OAPV_VER_MAJOR}.${OAPV_VER_MINOR}.${OAPV_VER_PATCH}") +endif () + +# N.B. OpenAPV installs its libraries into a lib/oapv subdirectory. +find_library(APV_LIBRARY + NAMES oapv liboapv + PATH_SUFFIXES oapv) +mark_as_advanced ( + APV_LIBRARY + APV_VERSION + ) + +find_package_handle_standard_args(APV + REQUIRED_VARS APV_LIBRARY APV_INCLUDE_DIR + VERSION_VAR APV_VERSION) + +if (APV_FOUND) + set(APV_LIBRARIES ${APV_LIBRARY}) + set(APV_INCLUDES ${APV_INCLUDE_DIR}) + # A static liboapv has no generated oapv_exports.h; consumers must + # define OAPV_STATIC_DEFINE so oapv.h doesn't try to include it. + get_filename_component(_apv_lib_ext ${APV_LIBRARY} LAST_EXT) + if (_apv_lib_ext STREQUAL ".a") + set (APV_DEFINITIONS OAPV_STATIC_DEFINE) + endif () + unset (_apv_lib_ext) +endif () diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index bfa588ca7b..08e9a3cc9d 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -292,6 +292,8 @@ macro (oiio_add_all_tests) URL http://www.itu.int/net/ITU-T/sigdb/speimage/ImageForm-s.aspx?val=10100803) oiio_add_tests (jxl FOUNDVAR JXL_FOUND) + oiio_add_tests (apv + FOUNDVAR APV_FOUND) set (all_openexr_tests openexr-suite openexr-multires openexr-chroma openexr-decreasingy openexr-v2 openexr-window perchannel oiiotool-deep) diff --git a/src/doc/builtinplugins.rst b/src/doc/builtinplugins.rst index 92c8a5f6bf..d6bac65c06 100644 --- a/src/doc/builtinplugins.rst +++ b/src/doc/builtinplugins.rst @@ -13,6 +13,81 @@ attributes, etc. The plugins are listed alphabetically by format name. +| + +.. _sec-bundledplugins-apv: + +APV +=============================================== + +APV (Advanced Professional Video) is a royalty-free, all-intra +professional video codec. Because every frame is coded independently, +raw APV bitstreams (extension :file:`.apv`) are usable as sequences of +individually addressable images: OpenImageIO exposes one *subimage* per +access unit, decoding its primary frame, and writes one access unit per +subimage. This reader/writer uses the OpenAPV library +(https://github.com/openapv/openapv). + +Pixel data in the file are 10--16 bit YCbCr (4:2:2, 4:4:4, 4:4:4:4 with +alpha, or 4:0:0 grayscale); OpenImageIO presents them as full-range +``uint16`` RGB(A) or grayscale, converting with the matrix coefficients +signaled in the bitstream (BT.709 assumed if unspecified, BT.601 and +BT.2020 also honored). Writing converts RGB(A) input to YCbCr with +BT.709 coefficients. Auxiliary frames within an access unit (preview, +depth, alpha, non-primary) are not currently exposed. + +If the bitstream carries a color description, the reader sets the +``CICP`` attribute (with matrix and range reflecting the delivered RGB +pixels) and, when possible, an ``oiio:ColorSpace`` color interop ID +derived from it. The writer, symmetrically, passes the primaries and +transfer characteristics of a ``CICP`` attribute into the bitstream's +color description; lacking that attribute, an ``oiio:ColorSpace`` +naming a color space that OpenImageIO's default color configuration +can map to CICP code points (such as a color interop ID) is used +instead. Matrix coefficients for the encode are chosen to match the +primaries (BT.2020 primaries pair with the 2020 matrix, the BT.601 +families with theirs, everything else — including P3, conventionally — +with BT.709). + +**Configuration settings for APV output** + +When writing, the following special metadata tokens control aspects of +the encoding: + +.. list-table:: + :widths: 30 10 65 + :header-rows: 1 + + * - Output configuration Attribute + - Type + - Meaning + * - ``apv:profile`` + - string + - Encoding profile, one of ``"422-10"`` (default for 3-channel + images), ``"422-12"``, ``"444-10"``, ``"444-12"``, ``"4444-10"`` + (default for 4-channel images), ``"4444-12"``, ``"400-10"`` + (default for 1-channel images). + * - ``apv:qp`` + - int + - Constant quantization parameter (0--63 for 10-bit profiles). + * - ``apv:bitrate`` + - int + - Target bitrate in kbps; if set, rate control is used instead of + constant QP. + * - ``apv:preset`` + - string + - Encoder speed/quality trade-off: ``"fastest"``, ``"fast"``, + ``"medium"`` (default), ``"slow"``, or ``"placebo"``. + * - ``FramesPerSecond`` + - rational + - Frame rate metadata signaled in the bitstream (default 30/1). + +**Custom I/O Overrides** + +APV input and output both support the "custom I/O" feature via the +special ``"oiio:ioproxy"`` attributes (see Section +:ref:`sec-imageinput-ioproxy`) as well as the `set_ioproxy()` methods. + | .. _sec-bundledplugins-bmp: diff --git a/src/libOpenImageIO/imageioplugin.cpp b/src/libOpenImageIO/imageioplugin.cpp index 68462cebf1..ec72346ee2 100644 --- a/src/libOpenImageIO/imageioplugin.cpp +++ b/src/libOpenImageIO/imageioplugin.cpp @@ -273,6 +273,7 @@ catalog_plugin(const std::string& format_name, extern const char* name##_output_extensions[]; \ extern const char* name##_imageio_library_version(); +PLUGENTRY(apv); PLUGENTRY(bmp); PLUGENTRY(cineon); PLUGENTRY(dds); @@ -352,6 +353,9 @@ catalog_builtin_plugins() #endif // Now all the less common formats, in alphabetical order. +#if defined(USE_APV) && !defined(DISABLE_APV) + DECLAREPLUG (apv); +#endif #if !defined(DISABLE_BMP) DECLAREPLUG (bmp); #endif diff --git a/testsuite/apv/ref/out.txt b/testsuite/apv/ref/out.txt new file mode 100644 index 0000000000..b6792906ac --- /dev/null +++ b/testsuite/apv/ref/out.txt @@ -0,0 +1,71 @@ +Reading out422.apv +out422.apv : 128 x 96, 3 channel, uint10 apv + SHA-1: 71086C86BB2C37BD60A6ACEC71937A4EF266A3F5 + channel list: R, G, B + apv:level: 30 + apv:profile: 33 + oiio:BitsPerSample: 10 +Computing diff of "pattern.tif" vs "out422.apv" +PASS +Computing diff of "pattern.tif" vs "out444.apv" +PASS +Reading out444-12.apv +out444-12.apv : 128 x 96, 3 channel, uint12 apv + SHA-1: 30A0FA5F39EFE7002C980B96924737E5F44AB7A7 + channel list: R, G, B + apv:level: 30 + apv:profile: 66 + oiio:BitsPerSample: 12 +Computing diff of "pattern.tif" vs "out444-12.apv" +PASS +Reading out4444.apv +out4444.apv : 64 x 64, 4 channel, uint10 apv + SHA-1: A7AE8C53CED3000E82DCACD31C3A1625C07DA677 + channel list: R, G, B, A + apv:level: 30 + apv:profile: 77 + oiio:BitsPerSample: 10 +Reading out400.apv +out400.apv : 64 x 64, 1 channel, uint10 apv + SHA-1: 0CFCAE621E0938136DD39CD856C73373CD6ADB17 + channel list: Y + apv:level: 30 + apv:profile: 99 + oiio:BitsPerSample: 10 +Reading multi.apv +multi.apv : 128 x 96, 3 channel, uint10 apv + 2 subimages: 128x96 [u10,u10,u10], 128x96 [u10,u10,u10] + subimage 0: 128 x 96, 3 channel, uint10 apv + SHA-1: 949B246E561511FF09A4376E437CEE9CFF13A87F + channel list: R, G, B + apv:level: 30 + apv:profile: 33 + oiio:BitsPerSample: 10 + subimage 1: 128 x 96, 3 channel, uint10 apv + SHA-1: F1C646340D636ACBCF0EA333D54EB14DCA1EEBE6 + channel list: R, G, B + apv:level: 30 + apv:profile: 33 + oiio:BitsPerSample: 10 +Reading cicp.apv +cicp.apv : 128 x 96, 3 channel, uint10 apv + SHA-1: 1561CF2F412992B44D793A9817CB6165728677AE + channel list: R, G, B + CICP: 9, 16, 0, 1 + apv:full_range_flag: 1 + apv:level: 30 + apv:matrix_coefficients: 9 + apv:profile: 33 + oiio:BitsPerSample: 10 + oiio:ColorSpace: "pq_rec2020_display" +Reading ciid.apv +ciid.apv : 128 x 96, 3 channel, uint10 apv + SHA-1: 915AC0206C3C8C3AD46C595FB23CA9E581AE3D53 + channel list: R, G, B + CICP: 1, 13, 0, 1 + apv:full_range_flag: 0 + apv:level: 30 + apv:matrix_coefficients: 1 + apv:profile: 33 + oiio:BitsPerSample: 10 + oiio:ColorSpace: "srgb_rec709_scene" diff --git a/testsuite/apv/run.py b/testsuite/apv/run.py new file mode 100644 index 0000000000..267fc6bed8 --- /dev/null +++ b/testsuite/apv/run.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +# Copyright Contributors to the OpenImageIO project. +# SPDX-License-Identifier: Apache-2.0 +# https://github.com/AcademySoftwareFoundation/OpenImageIO + +# APV is lossy, so round trips are verified with oiiotool --diff and +# generous thresholds rather than exact reference images. + +# A smooth gradient survives 4:2:2 chroma subsampling well. +command += oiiotool ("--pattern fill:topleft=0.1,0.1,0.1:topright=0.9,0.2,0.1:bottomleft=0.1,0.7,0.2:bottomright=0.2,0.2,0.9 128x96 3 -d uint16 -o pattern.tif") + +# Default profile (422-10) write, then verify the metadata we expect. +command += oiiotool ("pattern.tif --attrib apv:qp 8 -o out422.apv") +command += info_command ("out422.apv", safematch=True) +command += oiiotool ("pattern.tif out422.apv --fail 0.05 --warn 0.02 --diff") + +# 4:4:4 write / read round trip. +command += oiiotool ("pattern.tif --attrib apv:profile 444-10 --attrib apv:qp 5 -o out444.apv") +command += oiiotool ("pattern.tif out444.apv --fail 0.05 --warn 0.02 --diff") + +# 12-bit 4:4:4. +command += oiiotool ("pattern.tif --attrib apv:profile 444-12 --attrib apv:qp 5 -o out444-12.apv") +command += info_command ("out444-12.apv", safematch=True) +command += oiiotool ("pattern.tif out444-12.apv --fail 0.05 --warn 0.02 --diff") + +# RGBA via the 4444 profile. +command += oiiotool ("--pattern fill:topleft=0.1,0.1,0.1,1:topright=0.9,0.2,0.1,0.8:bottomleft=0.1,0.7,0.2,0.5:bottomright=0.2,0.2,0.9,0.2 64x64 4 -d uint16 --attrib apv:qp 5 -o out4444.apv") +command += info_command ("out4444.apv", safematch=True) + +# Grayscale via the 400 profile. +command += oiiotool ("--pattern fill:topleft=0.05:topright=0.95:bottomleft=0.5:bottomright=0.5 64x64 1 -d uint16 -o out400.apv") +command += info_command ("out400.apv", safematch=True) + +# Multiple subimages become multiple access units (frames). +command += oiiotool ("pattern.tif pattern.tif --mulc 0.5,0.5,0.5 --siappendall -d uint16 -o multi.apv") +command += info_command ("multi.apv", safematch=True) + +# CICP carries through: tag as BT.2020/PQ, expect it back on read. +command += oiiotool ("pattern.tif --attrib:type=int[4] CICP 9,16,0,1 --attrib apv:qp 5 -o cicp.apv") +command += info_command ("cicp.apv", safematch=True) + +# CICP derived from a color interop ID (no explicit CICP attribute): +# writing tags the bitstream, reading recovers both CICP and the ID. +command += oiiotool ("pattern.tif --attrib oiio:ColorSpace srgb_rec709_display --attrib apv:qp 5 -o ciid.apv") +command += info_command ("ciid.apv", safematch=True)