Commit 472cbfa7 for libheif

commit 472cbfa7a87a21a00db16ae04ed35fa722ebea1b
Author: Brad Hards <bradh@frogmouth.net>
Date:   Sun Feb 22 19:37:41 2026 +1100

    feat: add support for omnidirectional (OMAF) image projection

    This is defined in ISO/IEC 23090-2.

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8b74df99..77986f65 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -77,6 +77,8 @@ include (TestBigEndian)
 TEST_BIG_ENDIAN(IS_BIG_ENDIAN)
 add_compile_definitions(IS_BIG_ENDIAN=${IS_BIG_ENDIAN})

+option(HEIF_WITH_OMAF "Enable omnidirectional media format (OMAF) support." ON)
+
 # --- codec plugins

 option(ENABLE_PLUGIN_LOADING "Support loading of plugins" ON)
@@ -530,6 +532,12 @@ else()
   set(LIBS_PRIVATE "-lstdc++")
 endif()

+if(HEIF_WITH_OMAF)
+  set(WITH_OMAF "1")
+else ()
+  set(WITH_OMAF "0")
+endif()
+
 configure_file(libheif.pc.in ${CMAKE_CURRENT_BINARY_DIR}/libheif.pc @ONLY)

 install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libheif.pc
diff --git a/CMakePresets.json b/CMakePresets.json
index 7bd0d39e..22d05e1c 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -25,6 +25,7 @@
         "WITH_DAV1D" : "ON",
         "WITH_DAV1D_PLUGIN" : "OFF",
         "ENABLE_EXPERIMENTAL_MINI_FORMAT" : "ON",
+        "HEIF_WITH_OMAF" : "ON",
         "WITH_LIBDE265" : "ON",
         "WITH_LIBDE265_PLUGIN" : "OFF",
         "WITH_RAV1E" : "ON",
@@ -75,6 +76,7 @@
         "BUILD_SHARED_LIBS": "ON",
         "BUILD_TESTING" : "OFF",
         "ENABLE_EXPERIMENTAL_FEATURES" : "OFF",
+        "HEIF_WITH_OMAF" : "ON",
         "CMAKE_COMPILE_WARNING_AS_ERROR" : "OFF",

         "ENABLE_PLUGIN_LOADING" : "ON",
@@ -131,6 +133,7 @@
         "BUILD_SHARED_LIBS": "ON",
         "BUILD_TESTING" : "OFF",
         "ENABLE_EXPERIMENTAL_FEATURES" : "OFF",
+        "HEIF_WITH_OMAF" : "ON",
         "CMAKE_COMPILE_WARNING_AS_ERROR" : "OFF",

         "ENABLE_PLUGIN_LOADING" : "OFF",
diff --git a/examples/heif_enc.cc b/examples/heif_enc.cc
index 99f04454..334b5895 100644
--- a/examples/heif_enc.cc
+++ b/examples/heif_enc.cc
@@ -126,6 +126,10 @@ int sequence_max_frames = 0; // 0 -> no maximum
 std::string option_gimi_track_id;
 std::string option_sai_data_file;

+#if HEIF_WITH_OMAF
+int option_image_projection;
+heif_image_projection image_projection = heif_image_projection::flat;
+#endif

 enum heif_output_nclx_color_profile_preset
 {
@@ -192,6 +196,9 @@ const int OPTION_METADATA_COMPRESSION = 1034;
 const int OPTION_SEQUENCES_GIMI_TRACK_ID = 1035;
 const int OPTION_SEQUENCES_SAI_DATA_FILE = 1036;
 const int OPTION_USE_HEVC_COMPRESSION = 1037;
+#if HEIF_WITH_OMAF
+const int OPTION_SET_IMAGE_PROJECTION = 1038;
+#endif

 static option long_options[] = {
     {(char* const) "help",                    no_argument,       0,              'h'},
@@ -260,6 +267,9 @@ static option long_options[] = {
     {(char* const) "max-keyframe-distance",       required_argument,       nullptr, OPTION_SEQUENCES_MAX_KEYFRAME_DISTANCE},
     {(char* const) "set-gimi-track-id",           required_argument,       nullptr, OPTION_SEQUENCES_GIMI_TRACK_ID},
     {(char* const) "sai-data-file",               required_argument,       nullptr, OPTION_SEQUENCES_SAI_DATA_FILE},
+#if HEIF_WITH_OMAF
+    {(char* const) "image-projection",            required_argument,       nullptr, OPTION_SET_IMAGE_PROJECTION},
+#endif
     {0, 0,                                                           0,  0}
 };

@@ -392,6 +402,10 @@ void show_help(const char* argv0)
             << "      --metadata-track-uri URI   uses the URI identifier for the metadata track (experimental)\n"
             << "      --set-gimi-track-id ID     set the GIMI track ID for the visual track (experimental)\n"
             << "      --sai-data-file FILE       use the specified FILE as input data for the video frames SAI data\n"
+#endif
+#if HEIF_WITH_OMAF
+            << "omnidirectional imagery:\n"
+            << "      --image-projection proj    set the image projection (0 = equirectangular, 1 = cube map)\n"
 #endif
             ;
 }
@@ -1605,6 +1619,19 @@ int main(int argc, char** argv)
       case OPTION_SEQUENCES_SAI_DATA_FILE:
         option_sai_data_file = optarg;
         break;
+#if HEIF_WITH_OMAF
+      case OPTION_SET_IMAGE_PROJECTION:
+        option_image_projection = atoi(optarg);
+        if (option_image_projection == 0) {
+          image_projection = heif_image_projection::equirectangular;
+        } else if (option_image_projection == 1) {
+          image_projection = heif_image_projection::cube_map;
+        } else {
+          std::cerr << "image projection must be 0 or 1\n";
+          return 5;
+        }
+        break;
+#endif
     }
   }

@@ -2019,6 +2046,12 @@ int do_encode_images(heif_context* context, heif_encoder* encoder, heif_encoding
       heif_image_handle_set_pixel_aspect_ratio(handle, pasp->h, pasp->v);
     }

+#if HEIF_WITH_OMAF
+    if (image_projection != heif_image_projection::flat) {
+      heif_image_handle_set_image_projection(handle, image_projection);
+    }
+#endif
+
     if (is_primary_image) {
       heif_context_set_primary_image(context, handle);
     }
diff --git a/examples/heif_info.cc b/examples/heif_info.cc
index 8ab72b38..c4dc685a 100644
--- a/examples/heif_info.cc
+++ b/examples/heif_info.cc
@@ -778,6 +778,28 @@ int main(int argc, char** argv)
       properties_shown = true;
     }

+#if HEIF_WITH_OMAF
+    // --- OMAF
+
+    if (heif_image_handle_has_image_projection(handle)) {
+      heif_image_projection projection = heif_image_handle_get_image_projection(handle);
+      std::cout << "  image projection: ";
+      switch (projection)
+      {
+      case heif_image_projection::equirectangular:
+        std::cout << "equirectangular";
+        break;
+      case heif_image_projection::cube_map:
+        std::cout << "cube map";
+      default:
+        std::cout << "(unknown)";
+        break;
+      }
+      std::cout << "\n";
+      properties_shown = true;
+    }
+#endif
+
     if (!properties_shown) {
       std::cout << "none\n";
     }
diff --git a/libheif.pc.in b/libheif.pc.in
index 1057594d..8a62403c 100644
--- a/libheif.pc.in
+++ b/libheif.pc.in
@@ -12,4 +12,4 @@ Requires.private: @REQUIRES_PRIVATE@
 Libs: -L${libdir} -lheif
 Libs.private: @LIBS_PRIVATE@
 Cflags: -I${includedir}
-Cflags.private: -DLIBHEIF_STATIC_BUILD
+Cflags.private: -DLIBHEIF_STATIC_BUILD -DHEIF_WITH_OMAF=@WITH_OMAF@
diff --git a/libheif/CMakeLists.txt b/libheif/CMakeLists.txt
index 97eca435..2d65a5b0 100644
--- a/libheif/CMakeLists.txt
+++ b/libheif/CMakeLists.txt
@@ -330,6 +330,13 @@ if (ENABLE_EXPERIMENTAL_MINI_FORMAT)
             mini.cc)
 endif ()

+if (HEIF_WITH_OMAF)
+    target_compile_definitions(heif PUBLIC HEIF_WITH_OMAF=1)
+    target_sources(heif PRIVATE
+            omaf_boxes.h
+            omaf_boxes.cc)
+endif ()
+
 write_basic_package_version_file(${PROJECT_NAME}-config-version.cmake COMPATIBILITY ExactVersion)

 install(TARGETS heif EXPORT ${PROJECT_NAME}-config
diff --git a/libheif/api/libheif/heif_image.cc b/libheif/api/libheif/heif_image.cc
index d0af524d..1f82dfed 100644
--- a/libheif/api/libheif/heif_image.cc
+++ b/libheif/api/libheif/heif_image.cc
@@ -307,6 +307,17 @@ void heif_image_handle_set_pixel_aspect_ratio(heif_image_handle* handle, uint32_
   handle->image->set_pixel_ratio(aspect_h, aspect_v);
 }

+#if HEIF_WITH_OMAF
+heif_image_projection heif_image_get_image_projection(const heif_image* image)
+{
+  return image->image->get_image_projection();
+}
+
+void heif_image_set_image_projection(const heif_image* image, heif_image_projection image_projection)
+{
+  return image->image->set_image_projection(image_projection);
+}
+#endif

 heif_error heif_image_create(int width, int height,
                              heif_colorspace colorspace,
diff --git a/libheif/api/libheif/heif_image.h b/libheif/api/libheif/heif_image.h
index 7de5780e..b5b95a0b 100644
--- a/libheif/api/libheif/heif_image.h
+++ b/libheif/api/libheif/heif_image.h
@@ -94,6 +94,40 @@ typedef enum heif_channel
   heif_channel_unknown = 65535
 } heif_channel;

+#if HEIF_WITH_OMAF
+/**
+ * Image projection.
+ *
+ * The image projection for most images is flat - it is projected as intended to be shown on
+ * a flat screen or print. For immersive or omnidirectional media (e.g. VR headsets, or
+ * equivalent), there are alternatives such as an equirectangular projection or cubemap projection.
+ *
+ * See ISO/IEC 23090-2 "Omnidirectional media format" for more information.
+ */
+typedef enum heif_image_projection
+{
+  /**
+   * Equirectangular projection.
+   */
+  equirectangular = 0x00,
+
+  /**
+   * Cube map.
+   */
+  cube_map = 0x01,
+
+  /* Values 2 through 31 are reserved in ISO/IEC 23090-2:2023 Table 10. */
+  /**
+   * Projection is specified, but not recognised.
+   */
+  unknown_other = 0xFE,
+
+  /**
+   * Flat projection, assumed if no projection information provided.
+   */
+  flat = 0xFF,
+} heif_image_projection;
+#endif

 // An heif_image contains a decoded pixel image in various colorspaces, chroma formats,
 // and bit depths.
@@ -266,6 +300,13 @@ void heif_image_set_pixel_aspect_ratio(heif_image*, uint32_t aspect_h, uint32_t
 LIBHEIF_API
 void heif_image_handle_set_pixel_aspect_ratio(heif_image_handle*, uint32_t aspect_h, uint32_t aspect_v);

+#if HEIF_WITH_OMAF
+LIBHEIF_API
+heif_image_projection heif_image_get_image_projection(const heif_image*);
+
+LIBHEIF_API
+void heif_image_set_image_projection(const heif_image*, heif_image_projection image_projection);
+#endif

 // --- heif_image allocation

diff --git a/libheif/api/libheif/heif_properties.cc b/libheif/api/libheif/heif_properties.cc
index bc18e416..a5be076f 100644
--- a/libheif/api/libheif/heif_properties.cc
+++ b/libheif/api/libheif/heif_properties.cc
@@ -458,3 +458,23 @@ heif_error heif_camera_extrinsic_matrix_get_rotation_matrix(const heif_camera_ex
   return heif_error_success;
 }

+#if HEIF_WITH_OMAF
+int heif_image_handle_has_image_projection(const heif_image_handle* handle)
+{
+  if (!handle) {
+    return false;
+  }
+
+  return handle->image->has_image_projection();
+}
+
+heif_image_projection heif_image_handle_get_image_projection(const heif_image_handle* handle)
+{
+  return handle->image->get_image_projection();
+}
+
+void heif_image_handle_set_image_projection(const heif_image_handle* handle, heif_image_projection image_projection)
+{
+  return handle->image->set_image_projection(image_projection);
+}
+#endif
diff --git a/libheif/api/libheif/heif_properties.h b/libheif/api/libheif/heif_properties.h
index ceda2aa2..5c20befc 100644
--- a/libheif/api/libheif/heif_properties.h
+++ b/libheif/api/libheif/heif_properties.h
@@ -228,6 +228,18 @@ LIBHEIF_API
 heif_error heif_camera_extrinsic_matrix_get_rotation_matrix(const heif_camera_extrinsic_matrix*,
                                                             double* out_matrix_row_major);

+#if HEIF_WITH_OMAF
+// ------------------------- projection information -------------------------
+LIBHEIF_API
+int heif_image_handle_has_image_projection(const heif_image_handle* handle);
+
+LIBHEIF_API
+heif_image_projection heif_image_handle_get_image_projection(const heif_image_handle* handle);
+
+LIBHEIF_API
+void heif_image_handle_set_image_projection(const heif_image_handle* handle, heif_image_projection image_projection);
+#endif
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/libheif/box.cc b/libheif/box.cc
index 3ffabf3c..bae957fb 100644
--- a/libheif/box.cc
+++ b/libheif/box.cc
@@ -884,6 +884,13 @@ Error Box::read(BitstreamRange& range, std::shared_ptr<Box>* result, const heif_
       box = std::make_shared<Box_sdtp>();
       break;

+#if HEIF_WITH_OMAF
+    // OMAF
+    case fourcc("prfr"):
+      box = std::make_shared<Box_prfr>();
+      break;
+#endif
+
     default:
       box = std::make_shared<Box_other>(hdr.get_short_type());
       break;
diff --git a/libheif/color-conversion/colorconversion.cc b/libheif/color-conversion/colorconversion.cc
index fca791a2..cf0a60ed 100644
--- a/libheif/color-conversion/colorconversion.cc
+++ b/libheif/color-conversion/colorconversion.cc
@@ -484,6 +484,10 @@ Result<std::shared_ptr<HeifPixelImage>> ColorConversionPipeline::convert_image(c

     out->set_sample_duration(in->get_sample_duration());

+#if HEIF_WITH_OMAF
+    out->set_image_projection(in->get_image_projection());
+#endif
+
     const auto& warnings = in->get_warnings();
     for (const auto& warning : warnings) {
       out->add_warning(warning);
diff --git a/libheif/context.cc b/libheif/context.cc
index a2be4750..d72cf54d 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -690,6 +690,13 @@ Error HeifContext::interpret_heif_file_images()
     if (auto box_gimi_content_id = image->get_property<Box_gimi_content_id>()) {
       image->set_gimi_sample_content_id(box_gimi_content_id->get_content_id());
     }
+
+#if HEIF_WITH_OMAF
+    // add image projection information
+    if (auto prfr = image->get_property<Box_prfr>()) {
+      image->set_image_projection(prfr->get_image_projection());
+    }
+#endif
   }


diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 54ddb716..71a725de 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -680,6 +680,14 @@ void ImageItem::set_color_profile_icc(const std::shared_ptr<const color_profile_
   add_property(get_colr_box_icc(), false);
 }

+#if HEIF_WITH_OMAF
+void ImageItem::set_image_projection(heif_image_projection projection)
+{
+  ImageExtraData::set_image_projection(projection);
+  add_property(get_prfr_box(), true);
+}
+#endif
+

 Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decoding_options& options,
                                                                 bool decode_tile_only, uint32_t tile_x0, uint32_t tile_y0,
@@ -910,8 +918,17 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decod
     if (gimi_content_id) {
       img->set_gimi_sample_content_id(gimi_content_id->get_content_id());
     }
+
+#if HEIF_WITH_OMAF
+    // Image projection (OMAF)
+    auto prfr = get_property<Box_prfr>();
+    if (prfr) {
+      img->set_image_projection(prfr->get_image_projection());
+    }
+#endif
   }

+
   return img;
 }

diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index a5ac0ac3..5932614f 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -287,6 +287,10 @@ public:

   void set_color_profile_icc(const std::shared_ptr<const color_profile_raw>& profile) override;

+#if HEIF_WITH_OMAF
+  void set_image_projection(heif_image_projection image_projection) override;
+#endif
+
   // --- miaf

   // TODO: we should have a function that checks all MIAF constraints and sets the compatibility flag.
diff --git a/libheif/omaf_boxes.cc b/libheif/omaf_boxes.cc
new file mode 100644
index 00000000..a264aa5f
--- /dev/null
+++ b/libheif/omaf_boxes.cc
@@ -0,0 +1,93 @@
+/*
+ * libheif OMAF (ISO/IEC 23090-2)
+ *
+ * Copyright (c) 2026 Brad Hards <bradh@frogmouth.net>
+ *
+ * This file is part of libheif.
+ *
+ * libheif is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * libheif is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libheif.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "omaf_boxes.h"
+
+#include <memory>
+#include <string>
+
+
+Error Box_prfr::parse(BitstreamRange& range, const heif_security_limits* limits)
+{
+  parse_full_box_header(range);
+
+  if (get_version() > 0) {
+    return unsupported_version_error("prfr");
+  }
+
+  uint8_t projection_type = (range.read8() & 0x1F);
+  switch (projection_type)
+  {
+  case 0x00:
+    m_projection = heif_image_projection::equirectangular;
+    break;
+  case 0x01:
+    m_projection = heif_image_projection::cube_map;
+    break;
+  default:
+    m_projection = heif_image_projection::unknown_other;
+    break;
+  }
+  return range.get_error();
+}
+
+std::string Box_prfr::dump(Indent& indent) const
+{
+  std::ostringstream sstr;
+  sstr << Box::dump(indent);
+  sstr << indent << "projection_type: " << m_projection << "\n";
+  return sstr.str();
+}
+
+Error Box_prfr::write(StreamWriter& writer) const
+{
+  size_t box_start = reserve_box_header_space(writer);
+  switch (m_projection) {
+    case heif_image_projection::equirectangular:
+      writer.write8(0x00);
+      break;
+    case heif_image_projection::cube_map:
+      writer.write8(0x01);
+      break;
+    default:
+      return {
+        heif_error_Invalid_input,
+        heif_suberror_Unspecified,
+        "Unsupported image projection value."
+    };
+  }
+  prepend_header(writer, box_start);
+  return Error::Ok;
+}
+
+Error Box_prfr::set_image_projection(heif_image_projection projection)
+{
+  if ((projection == heif_image_projection::equirectangular) || (projection == heif_image_projection::cube_map)) {
+    m_projection = projection;
+    return Error::Ok;
+  } else {
+    return {
+      heif_error_Invalid_input,
+      heif_suberror_Unspecified,
+      "Unsupported image projection value."
+    };
+  }
+}
diff --git a/libheif/omaf_boxes.h b/libheif/omaf_boxes.h
new file mode 100644
index 00000000..0d82d659
--- /dev/null
+++ b/libheif/omaf_boxes.h
@@ -0,0 +1,60 @@
+/*
+ * libheif OMAF (ISO/IEC 23090-2)
+ *
+ * Copyright (c) 2026 Brad Hards <bradh@frogmouth.net>
+ *
+ * This file is part of libheif.
+ *
+ * libheif is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * libheif is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with libheif.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef LIBHEIF_MINI_H
+#define LIBHEIF_MINI_H
+
+#include "libheif/heif.h"
+#include "box.h"
+
+#include <memory>
+#include <string>
+
+// Projection format for OMAF
+// See ISO/IEC 23090-2:2023 Section 7.9.3
+class Box_prfr : public FullBox
+{
+public:
+  Box_prfr()
+  {
+    set_short_type(fourcc("prfr"));
+  }
+
+  heif_image_projection get_image_projection() const { return m_projection; }
+
+  Error set_image_projection(heif_image_projection projection);
+
+  std::string dump(Indent&) const override;
+
+  const char* debug_box_name() const override { return "Projection Format"; }
+
+  [[nodiscard]] parse_error_fatality get_parse_error_fatality() const override { return parse_error_fatality::optional; }
+
+  Error write(StreamWriter& writer) const override;
+
+protected:
+  Error parse(BitstreamRange& range, const heif_security_limits*) override;
+
+private:
+  heif_image_projection m_projection;
+};
+
+#endif
\ No newline at end of file
diff --git a/libheif/pixelimage.cc b/libheif/pixelimage.cc
index ccc42cc1..b481966c 100644
--- a/libheif/pixelimage.cc
+++ b/libheif/pixelimage.cc
@@ -212,6 +212,19 @@ std::shared_ptr<Box_colr> ImageExtraData::get_colr_box_icc() const
   return colr;
 }

+#if HEIF_WITH_OMAF
+std::shared_ptr<Box_prfr> ImageExtraData::get_prfr_box() const
+{
+  if (!has_image_projection()) {
+    return {};
+  }
+
+  auto prfr = std::make_shared<Box_prfr>();
+  prfr->set_image_projection(get_image_projection());
+
+  return prfr;
+}
+#endif

 std::vector<std::shared_ptr<Box>> ImageExtraData::generate_property_boxes(bool generate_colr_boxes) const
 {
@@ -267,6 +280,14 @@ std::vector<std::shared_ptr<Box>> ImageExtraData::generate_property_boxes(bool g
     }
   }

+#if HEIF_WITH_OMAF
+  if (has_image_projection()) {
+    auto prfr = std::make_shared<Box_prfr>();
+    prfr->set_image_projection(get_image_projection());
+    properties.push_back(prfr);
+  }
+#endif
+
   return properties;
 }

@@ -1875,6 +1896,10 @@ void HeifPixelImage::forward_all_metadata_from(const std::shared_ptr<const HeifP
   // TODO: TAI timestamp and contentID (once we merge that branch)

   // TODO: should we also forward the warnings? It might be better to do that in ImageItem_Grid.
+
+#if HEIF_WITH_OMAF
+  set_image_projection(src_image->get_image_projection());
+#endif
 }


diff --git a/libheif/pixelimage.h b/libheif/pixelimage.h
index 396c10a6..165302f1 100644
--- a/libheif/pixelimage.h
+++ b/libheif/pixelimage.h
@@ -26,6 +26,9 @@
 #include "error.h"
 #include "nclx.h"
 #include <libheif/heif_experimental.h>
+#if HEIF_WITH_OMAF
+#include "omaf_boxes.h"
+#endif
 #include "security_limits.h"

 #include <vector>
@@ -177,6 +180,20 @@ public:

   std::string get_gimi_sample_content_id() const { assert(has_gimi_sample_content_id()); return *m_gimi_sample_content_id; }

+#if HEIF_WITH_OMAF
+  bool has_image_projection() const {
+    return (m_image_projection != heif_image_projection::flat);
+  }
+
+  const heif_image_projection get_image_projection() const {
+    return m_image_projection;
+  }
+
+  virtual void set_image_projection(const heif_image_projection projection) {
+    m_image_projection = projection;
+  }
+#endif
+
 private:
   bool m_premultiplied_alpha = false;
   nclx_profile m_color_profile_nclx = nclx_profile::undefined();
@@ -191,6 +208,10 @@ private:

   std::optional<std::string> m_gimi_sample_content_id;

+#if HEIF_WITH_OMAF
+  heif_image_projection m_image_projection = heif_image_projection::flat;
+#endif
+
 protected:
   std::shared_ptr<Box_clli> get_clli_box() const;

@@ -201,6 +222,10 @@ protected:
   std::shared_ptr<Box_colr> get_colr_box_nclx() const;

   std::shared_ptr<Box_colr> get_colr_box_icc() const;
+
+#if HEIF_WITH_OMAF
+  std::shared_ptr<Box_prfr> get_prfr_box() const;
+#endif
 };


diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 443f584f..557e7424 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -81,6 +81,13 @@ if (ENABLE_EXPERIMENTAL_MINI_FORMAT)
     add_libheif_test(mini_decode)
 endif()

+if (HEIF_WITH_OMAF)
+    if (NOT WITH_REDUCED_VISIBILITY)
+        add_libheif_test(omaf_boxes)
+    endif()
+    add_libheif_test(omaf)
+endif()
+
 if (WITH_UNCOMPRESSED_CODEC)
     add_libheif_test(uncompressed_decode)
     add_libheif_test(uncompressed_decode_mono)
diff --git a/tests/omaf.cc b/tests/omaf.cc
new file mode 100644
index 00000000..9ebc4a3e
--- /dev/null
+++ b/tests/omaf.cc
@@ -0,0 +1,92 @@
+/*
+  libheif OMAF (ISO/IEC 23090-2) unit tests
+
+  MIT License
+
+  Copyright (c) 2026 Brad Hards <bradh@frogmouth.net>
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+*/
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "api_structs.h"
+#include "pixelimage.h"
+
+#include "test_utils.h"
+
+#include <string.h>
+
+static heif_encoding_options * set_encoding_options()
+{
+  heif_encoding_options * options = heif_encoding_options_alloc();
+  options->macOS_compatibility_workaround = false;
+  options->macOS_compatibility_workaround_no_nclx_profile = true;
+  options->image_orientation = heif_orientation_normal;
+  return options;
+}
+
+static void do_encode(heif_image* input_image, const char* filename, heif_image_projection projection)
+{
+  REQUIRE(input_image != nullptr);
+  heif_init(nullptr);
+  heif_context *ctx = heif_context_alloc();
+  heif_encoder *encoder;
+  heif_error err;
+  err = heif_context_get_encoder_for_format(ctx, heif_compression_HEVC, &encoder);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_encoding_options *options = set_encoding_options();
+
+  heif_image_handle *output_image_handle;
+
+  err = heif_context_encode_image(ctx, input_image, encoder, options, &output_image_handle);
+  REQUIRE(err.code == heif_error_Ok);
+  heif_image_handle_set_image_projection(output_image_handle, projection);
+  err = heif_context_write_to_file(ctx, filename);
+  REQUIRE(err.code == heif_error_Ok);
+
+  heif_image_handle_release(output_image_handle);
+  heif_encoding_options_free(options);
+  heif_encoder_release(encoder);
+  heif_image_release(input_image);
+
+  heif_context_free(ctx);
+
+  heif_context *readbackCtx = get_context_for_local_file(filename);
+  heif_image_handle *readbackHandle = get_primary_image_handle(readbackCtx);
+  heif_image_projection readbackProjection = heif_image_handle_get_image_projection(readbackHandle);
+  REQUIRE(readbackProjection == projection);
+  heif_image_handle_release(readbackHandle);
+  heif_context_free(readbackCtx);
+
+  heif_deinit();
+}
+
+TEST_CASE("Encode OMAF HEIC")
+{
+  heif_image *input_image = createImage_RGB_planar();
+  do_encode(input_image, "encode_omaf_equirectangular.heic", heif_image_projection::equirectangular);
+}
+
+TEST_CASE("Encode OMAF HEIC Cubemap")
+{
+  heif_image *input_image = createImage_RGB_planar();
+  do_encode(input_image, "encode_omaf_cubemap.heic", heif_image_projection::cube_map);
+}
\ No newline at end of file
diff --git a/tests/omaf_boxes.cc b/tests/omaf_boxes.cc
new file mode 100644
index 00000000..73972c6b
--- /dev/null
+++ b/tests/omaf_boxes.cc
@@ -0,0 +1,62 @@
+/*
+  libheif OMAF (ISO/IEC 23090-2) unit tests
+
+  MIT License
+
+  Copyright (c) 2026 Brad Hards <bradh@frogmouth.net>
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+*/
+
+#include "catch_amalgamated.hpp"
+#include "omaf_boxes.h"
+#include "error.h"
+#include <cstdint>
+#include <iostream>
+#include <memory>
+
+
+TEST_CASE("prfr") {
+  std::vector<uint8_t> byteArray{0x00, 0x00, 0x00, 0x0d, 0x70, 0x72, 0x66, 0x72, 0x00, 0x00, 0x00, 0x00, 0x01};
+
+  auto reader = std::make_shared<StreamReader_memory>(byteArray.data(),
+                                                      byteArray.size(), false);
+
+  BitstreamRange range(reader, byteArray.size());
+  std::shared_ptr<Box> box;
+  Error error = Box::read(range, &box, heif_get_global_security_limits());
+  REQUIRE(error == Error::Ok);
+  REQUIRE(range.error() == 0);
+
+  REQUIRE(box->get_short_type() == fourcc("prfr"));
+  REQUIRE(box->get_type_string() == "prfr");
+  std::shared_ptr<Box_prfr> prfr = std::dynamic_pointer_cast<Box_prfr>(box);
+  REQUIRE(prfr->get_image_projection() == heif_image_projection::cube_map);
+  Indent indent;
+  std::string dumpResult = box->dump(indent);
+  REQUIRE(dumpResult == "Box: prfr ----- (Projection Format)\n"
+                        "size: 13   (header size: 12)\n"
+                        "projection_type: 1\n");
+
+  StreamWriter writer;
+  Error err = prfr->write(writer);
+  REQUIRE(err.error_code == heif_error_Ok);
+  const std::vector<uint8_t> bytes = writer.get_data();
+  REQUIRE(bytes == byteArray);
+}