ultrahdr

package module
v0.0.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 24, 2026 License: MIT Imports: 20 Imported by: 0

README

ultrahdr (pure Go)

This is a minimal, pure-Go port of libultrahdr focused on correctness and portability.

Why?

UltraHDR is an emerging format of presenting true HDR images with a great level of compatibility with legacy apps and devices. If you're interested in this topic, check https://gregbenzphotography.com/hdr/ for more context.

Because this technology is a relatively new, and is built on top of another relatively new (HDR displays) technology, tooling landscape is pretty sparse now.

Existing solutions for Go require CGO builds with non-trivial dependencies and environment requirements.

This project started as an experiment with codex LLM tool, with human steering and testing it turned out to be a success. Resulting code leverages Go's cross-compiltion and powers statically built binaries with no depencies, it also bridges the way to new features like uhdrtool rebase.

Initial development log (colored output in terminal):

curl -s https://raw.githubusercontent.com/vearutop/ultrahdr/refs/heads/master/testdata/codex.log

Usage

package main

import (
	"image"
	"image/jpeg"
	"os"

	"github.com/vearutop/ultrahdr"
)

func main() {
	// Load SDR base image.
	f, _ := os.Open("sdr.jpg")
	sdrImg, _, _ := image.Decode(f)
	f.Close()

	// Build HDR image data (linear RGB, 1.0 = SDR white).
	// This example is a placeholder; fill hdr.Pix with real data.
	hdr := &ultrahdr.HDRImage{Width: sdrImg.Bounds().Dx(), Height: sdrImg.Bounds().Dy(), Stride: sdrImg.Bounds().Dx() * 3, Pix: make([]float32, sdrImg.Bounds().Dx()*sdrImg.Bounds().Dy()*3)}

	// Encode JPEG/R.
	jpegrBytes, meta, _ := ultrahdr.Encode(hdr, sdrImg, &ultrahdr.EncodeOptions{Quality: 95})
	_ = meta
	_ = os.WriteFile("out.jpegr.jpg", jpegrBytes, 0644)

	// Decode JPEG/R.
	data, _ := os.ReadFile("out.jpegr.jpg")
	hdrOut, sdrOut, metaOut, _ := ultrahdr.Decode(data, &ultrahdr.DecodeOptions{MaxDisplayBoost: 4})
	_, _ = hdrOut, metaOut

	outFile, _ := os.Create("base.jpg")
	_ = jpeg.Encode(outFile, sdrOut, &jpeg.Options{Quality: 95})
	outFile.Close()
}

CLI

# resize UltraHDR (writes container + components)
uhdrtool resize -in testdata/uhdr.jpg -out testdata/uhdr_thumb.jpg -w 2400 -h 1600 -q 85 -gq 75 \
  -primary-out testdata/uhdr_thumb_primary.jpg -gainmap-out testdata/uhdr_thumb_gainmap.jpg

# crop (UltraHDR or SDR)
uhdrtool crop -in testdata/uhdr.jpg -out testdata/uhdr_crop.jpg -x 200 -y 120 -w 1800 -h 1200 -q 85 -gq 75

# grid (SDR or mixed, HDR grid when any input is UltraHDR)
uhdrtool grid -cols 2 -cell-w 400 -cell-h 300 -out testdata/grid.jpg testdata/sample_*.jpg

# split into components + metadata bundle
uhdrtool split -in testdata/uhdr.jpg \
  -primary-out primary.jpg -gainmap-out gainmap.jpg -meta-out meta.json

# join without the original template
uhdrtool join -meta meta.json -primary primary.jpg -gainmap gainmap.jpg -out out.jpg

# rebase on a better SDR (approximate gainmap adjustment)
uhdrtool rebase -in testdata/uhdr.jpg -primary better_sdr.jpg -out better_uhdr.jpg

# rebase using HDR EXR (new gainmap generation)
uhdrtool rebase -primary sdr.jpg -exr hdr.exr -out output.jpg

# rebase using HDR TIFF (new gainmap generation)
uhdrtool rebase -primary sdr.jpg -tiff hdr.tif -out output.jpg

# detect UltraHDR
uhdrtool detect -in testdata/uhdr.jpg

Resizing

Primary image interpolation is built in. Set ResizeSpec.Interpolation to one of InterpolationNearest, InterpolationBilinear, InterpolationBicubic, InterpolationMitchellNetravali, InterpolationLanczos2, or InterpolationLanczos3. Gainmap resizing uses the same interpolation mode.

ResizeHDR and ResizeSDR accept one or more ResizeSpec entries and deliver outputs via ReceiveResult. ResizeHDR also supports ReceiveSplit to inspect container metadata before resizing. ResizeSpec.Crop optionally crops the source before resizing (for UltraHDR, the gainmap is cropped to the corresponding region automatically).

Grid

files := []string{
	"testdata/sample_srgb.jpg",
	"testdata/sample_display_p3.jpg",
	"testdata/sample_adobe_rgb.jpg",
}
readers := make([]io.Reader, 0, len(files))
for _, p := range files {
	f, _ := os.Open(p)
	defer f.Close()
	readers = append(readers, f)
}
res, err := ultrahdr.Grid(readers, 2, 400, 300, &ultrahdr.GridOptions{
	Quality:       85,
	Interpolation: ultrahdr.InterpolationLanczos2,
	Background:    color.Black,
})
if err != nil {
	// handle error
}
_ = os.WriteFile("grid.jpg", res.Primary, 0o644)

When any input is UltraHDR, the grid output is encoded as UltraHDR with a synthesized gainmap. If all inputs are SDR, the grid output is a regular JPEG.

Compatibility

  • Google Pixel UltraHDR JPEG/R files that store gainmap metadata in XMP only (no secondary ISO APP2) are supported. Resize/rebase regenerates ISO 21496-1 metadata for the gainmap JPEG to preserve HDR rendering in Chrome.
  • Older Adobe Camera Raw UltraHDR files are supported when gainmap XMP values are encoded as rdf:Seq (<rdf:li>...) entries instead of attribute values.
  • Containers with embedded JPEG thumbnails are handled using MPF image ranges, so split/resize targets the correct primary and gainmap images.
  • Rebase applies ICC-aware gamut alignment for sRGB, Display P3, and Adobe RGB primaries before gainmap math.

Detection

f, err := os.Open("image.jpg")
if err != nil {
	// handle error
}
defer f.Close()

ok, err := ultrahdr.IsUltraHDR(f)
// ok == true means the file looks like a valid UltraHDR JPEG/R container.

ResizeSDR

f, err := os.Open("input.jpg")
if err != nil {
	// handle error
}
defer f.Close()
var resized *ultrahdr.Result
err := ultrahdr.ResizeSDR(f, ultrahdr.ResizeSpec{
  Width:         1600,
  Height:        1200,
  Quality:       85,
  Interpolation: ultrahdr.InterpolationLanczos2,
  KeepMeta:      true,
  ReceiveResult: func(res *ultrahdr.Result, err error) { resized = res },
})
if err != nil {
	// handle error
}
_ = os.WriteFile("output.jpg", resized.Primary, 0o644)

ResizeSDR behavior:

  • KeepMeta=true: preserves EXIF/ICC (including Display P3 and Adobe RGB profiles).

  • KeepMeta=false: strips metadata and converts Display P3/Adobe RGB input to sRGB pixels for web-safe output.

  • ResizeSDR accepts multiple ResizeSpec entries and performs a single source decode.

  • Each spec receives a result via its ReceiveResult callback.

Join

primary, _ := os.ReadFile("primary.jpg")
gainmap, _ := os.ReadFile("gainmap.jpg")
container, err := ultrahdr.Join(primary, gainmap, nil, nil)
if err != nil {
  // handle error
}
_ = os.WriteFile("out.jpg", container, 0o644)

If you have a split result, you can reuse its metadata:

f, _ := os.Open("template.jpg")
split, _ := ultrahdr.Split(f)
container, _ := ultrahdr.Join(primary, gainmap, nil, split)

Limitations

  • SDR base image is assumed to be sRGB.
  • HDR image input is assumed to be linear RGB relative to SDR white.
  • Gain map sampling uses nearest-neighbor.
  • Only XMP + ISO 21496-1 gain map metadata are generated.
  • ResizeSDR metadata preservation is limited to EXIF and ICC segments (XMP is not preserved).
  • Full ICC color management is not implemented; only sRGB/Display P3/Adobe RGB primary profile handling is applied in rebase and metadata-stripped ResizeSDR output.

Documentation

Overview

Package ultrahdr provides a pure-Go implementation of the UltraHDR JPEG/R format.

This is a pragmatic port focused on correctness and portability rather than performance. It uses the patched standard image/jpeg package for JPEG encode/decode and assembles/parses the JPEG/R container (MPF + XMP + ISO 21496-1 gain map metadata) in Go.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsUltraHDR

func IsUltraHDR(r io.Reader) (bool, error)

IsUltraHDR performs a streaming UltraHDR check without loading the full image. It reads until the gainmap header is reached and scans APP metadata for XMP/ISO.

Example
package main

import (
	"os"

	"github.com/vearutop/ultrahdr"
)

func main() {
	f, err := os.Open("testdata/uhdr.jpg")
	if err != nil {
		return
	}
	defer f.Close()

	_, _ = ultrahdr.IsUltraHDR(f)
}

func Join added in v0.0.6

func Join(primaryJPEG, gainmapJPEG []byte, bundle *MetadataBundle, template *Result) ([]byte, error)

Join assembles an UltraHDR container from primary and gainmap JPEGs. If bundle is provided, it is used as the metadata source. If template is provided, it is used to build the bundle. Otherwise gainmap metadata is extracted from the gainmap JPEG and EXIF/ICC are extracted from the primary JPEG.

func RebaseFile added in v0.0.6

func RebaseFile(inPath, newSDRPath, outPath string, opts ...RebaseOption) error

RebaseFile reads an UltraHDR JPEG, rebases it on newSDRPath, and writes the output.

func RebaseFromEXRFile added in v0.0.6

func RebaseFromEXRFile(primaryPath, exrPath, outPath string, opts ...RebaseOption) error

RebaseFromEXRFile generates an UltraHDR JPEG from an SDR primary and HDR EXR input.

func RebaseFromTIFFFile added in v0.0.6

func RebaseFromTIFFFile(primaryPath, hdrPath, outPath string, opts ...RebaseOption) error

RebaseFromTIFFFile generates an UltraHDR JPEG from an SDR primary and HDR TIFF input.

func ResizeHDR added in v0.0.6

func ResizeHDR(r io.Reader, specs ...ResizeSpec) error

ResizeHDR resizes an UltraHDR JPEG container to the requested dimensions. Results are delivered via ReceiveResult on each spec; ReceiveSplit runs before resizing.

Example
package main

import (
	"os"

	"github.com/vearutop/ultrahdr"
)

func main() {
	f, err := os.Open("testdata/uhdr.jpg")
	if err != nil {
		return
	}
	defer f.Close()
	_ = ultrahdr.ResizeHDR(f, ultrahdr.ResizeSpec{
		Width:  2400,
		Height: 1600,
	})
}

func ResizeSDR added in v0.0.6

func ResizeSDR(r io.Reader, specs ...ResizeSpec) error

ResizeSDR resizes one JPEG into multiple outputs with a single source decode. For each spec: when KeepMeta is true EXIF/ICC are preserved; otherwise output is metadata-free. Metadata-free outputs are converted to sRGB when source profile is recognized as wide gamut.

Example
package main

import (
	"image"
	"os"

	"github.com/vearutop/ultrahdr"
)

func main() {
	f, err := os.Open("testdata/sample_srgb.jpg")
	if err != nil {
		return
	}
	crop := image.Rect(120, 80, 920, 680)
	_ = ultrahdr.ResizeSDR(f, ultrahdr.ResizeSpec{
		Width:         800,
		Height:        600,
		Crop:          &crop,
		Quality:       85,
		Interpolation: ultrahdr.InterpolationLanczos2,
		KeepMeta:      true,
	})
}
Example (Multi)
package main

import (
	"os"

	"github.com/vearutop/ultrahdr"
)

func main() {
	f, err := os.Open("testdata/sample_display_p3.jpg")
	if err != nil {
		return
	}
	specs := []ultrahdr.ResizeSpec{
		{Width: 1200, Height: 800, Quality: 85, Interpolation: ultrahdr.InterpolationLanczos2, KeepMeta: true, ReceiveResult: func(res *ultrahdr.Result, err error) { _ = res }},
		{Width: 600, Height: 400, Quality: 82, Interpolation: ultrahdr.InterpolationLanczos2, KeepMeta: false, ReceiveResult: func(res *ultrahdr.Result, err error) { _ = res }},
		{Width: 300, Height: 200, Quality: 78, Interpolation: ultrahdr.InterpolationLanczos2, KeepMeta: false, ReceiveResult: func(res *ultrahdr.Result, err error) { _ = res }},
	}
	err = ultrahdr.ResizeSDR(f, specs...)
	if err != nil {
		return
	}
}

Types

type GainMapMetadata

type GainMapMetadata struct {
	Version         string
	MaxContentBoost [3]float32
	MinContentBoost [3]float32
	Gamma           [3]float32
	OffsetSDR       [3]float32
	OffsetHDR       [3]float32
	HDRCapacityMin  float32
	HDRCapacityMax  float32
	UseBaseCG       bool
}

GainMapMetadata corresponds to the float metadata in the C++ library.

type GridOptions added in v0.0.7

type GridOptions struct {
	Quality         int           // JPEG quality for the output (0 uses default).
	Interpolation   Interpolation // Resize interpolation mode.
	Background      color.Color   // Background fill color (nil uses black).
	ReceivePosition func(i int, top, left uint, width, height uint)
}

GridOptions configures grid rendering for SDR inputs.

type Interpolation added in v0.0.2

type Interpolation int

Interpolation selects the built-in interpolation mode.

const (
	// InterpolationNearest is nearest-neighbor sampling.
	InterpolationNearest Interpolation = iota
	// InterpolationBilinear is linear sampling.
	InterpolationBilinear
	// InterpolationBicubic is cubic sampling.
	InterpolationBicubic
	// InterpolationMitchellNetravali is Mitchell-Netravali sampling.
	InterpolationMitchellNetravali
	// InterpolationLanczos2 is Lanczos sampling with a=2.
	InterpolationLanczos2
	// InterpolationLanczos3 is Lanczos sampling with a=3.
	InterpolationLanczos3
)

type MetadataBundle

type MetadataBundle struct {
	Format       string   `json:"format"`
	PrimaryXMP   []byte   `json:"primary_xmp,omitempty"`
	PrimaryISO   []byte   `json:"primary_iso,omitempty"`
	SecondaryXMP []byte   `json:"secondary_xmp,omitempty"`
	SecondaryISO []byte   `json:"secondary_iso,omitempty"`
	Exif         []byte   `json:"exif,omitempty"`
	ICC          [][]byte `json:"icc,omitempty"`
}

MetadataBundle captures the metadata needed to reassemble an UltraHDR container. Byte fields are base64-encoded in JSON.

func (*MetadataBundle) Validate

func (b *MetadataBundle) Validate() error

Validate ensures the bundle has the required fields to build a container.

type MetadataSegments

type MetadataSegments struct {
	PrimaryXMP   []byte
	PrimaryISO   []byte
	SecondaryXMP []byte
	SecondaryISO []byte
}

MetadataSegments holds raw APP payloads for XMP/ISO blocks. These payloads include the namespace prefix and null terminator.

type RebaseOption added in v0.0.6

type RebaseOption func(*RebaseOptions)

RebaseOption configures rebase behavior.

func WithBaseQuality added in v0.0.6

func WithBaseQuality(quality int) RebaseOption

WithBaseQuality sets the JPEG quality for the primary SDR output.

func WithGainmapGamma added in v0.0.6

func WithGainmapGamma(gamma float32) RebaseOption

WithGainmapGamma sets the gamma to apply to gainmap encoding.

func WithGainmapOut added in v0.0.6

func WithGainmapOut(path string) RebaseOption

WithGainmapOut sets an optional output path for the rebased gainmap JPEG.

func WithGainmapQuality added in v0.0.6

func WithGainmapQuality(quality int) RebaseOption

WithGainmapQuality sets the JPEG quality for the gainmap output.

func WithGainmapScale added in v0.0.6

func WithGainmapScale(scale int) RebaseOption

WithGainmapScale sets the downscale factor for gainmap generation.

func WithHDRCapacityMax added in v0.0.6

func WithHDRCapacityMax(limit float32) RebaseOption

WithHDRCapacityMax clamps maximum HDR capacity when generating gainmaps.

func WithICCProfile added in v0.0.6

func WithICCProfile(profile []byte) RebaseOption

WithICCProfile sets the ICC profile bytes for the new SDR image.

func WithMultiChannelGainmap added in v0.0.6

func WithMultiChannelGainmap(enabled bool) RebaseOption

WithMultiChannelGainmap toggles RGB gainmap encoding.

func WithPrimaryOut added in v0.0.6

func WithPrimaryOut(path string) RebaseOption

WithPrimaryOut sets an optional output path for the rebased primary JPEG.

type RebaseOptions

type RebaseOptions struct {
	BaseQuality     int     // JPEG quality for the primary SDR output (0 uses default).
	GainmapQuality  int     // JPEG quality for the gainmap output (0 uses default).
	GainmapScale    int     // Downscale factor for gainmap generation (higher is smaller/faster).
	GainmapGamma    float32 // Gamma to apply to gainmap encoding (0 uses default).
	UseMultiChannel bool    // Encode gainmap as RGB instead of single-channel.
	HDRCapacityMax  float32 // Clamp maximum HDR capacity when generating gainmaps.
	ICCProfile      []byte  // ICC profile bytes for new SDR when not embedded in input.
	PrimaryOut      string  // Optional output path for the rebased primary JPEG.
	GainmapOut      string  // Optional output path for the rebased gainmap JPEG.
}

RebaseOptions controls gainmap rebase behavior.

type ResizeSpec added in v0.0.4

type ResizeSpec struct {
	Width          uint                         // Target width in pixels.
	Height         uint                         // Target height in pixels.
	Crop           *image.Rectangle             // Optional crop rectangle in source pixels.
	Quality        int                          // SDR/primary JPEG quality (0 uses default).
	GainmapQuality int                          // Gainmap JPEG quality for HDR resize (0 uses default or Quality).
	Interpolation  Interpolation                // Resize interpolation mode for SDR and HDR paths.
	KeepMeta       bool                         // SDR: preserve EXIF/ICC and skip sRGB conversion when true.
	ReceiveResult  func(res *Result, err error) // Callback for each output.
	ReceiveSplit   func(sr *Result)             // HDR: callback with split result before resizing.
}

ResizeSpec describes one output variant for ResizeSDR/ResizeHDR.

type Result added in v0.0.6

type Result struct {
	Container []byte
	Primary   []byte
	Gainmap   []byte
	Meta      *GainMapMetadata
	Segs      *MetadataSegments
}

Result contains the primary/gainmap JPEGs with optional container and metadata.

func Grid added in v0.0.7

func Grid(readers []io.Reader, cols int, cellW, cellH int, opts *GridOptions) (*Result, error)

Grid builds a sprite grid from SDR images. Inputs are resized to fit each cell preserving aspect ratio and padded with black. Output is encoded as sRGB JPEG.

Example
package main

import (
	"image/color"
	"io"
	"os"

	"github.com/vearutop/ultrahdr"
)

func main() {
	paths := []string{
		"testdata/sample_srgb.jpg",
		"testdata/sample_display_p3.jpg",
		"testdata/sample_adobe_rgb.jpg",
	}
	readers := make([]io.Reader, 0, len(paths))
	for _, p := range paths {
		f, err := os.Open(p)
		if err != nil {
			return
		}
		defer f.Close()
		readers = append(readers, f)
	}
	_, _ = ultrahdr.Grid(readers, 2, 400, 300, &ultrahdr.GridOptions{
		Quality:       85,
		Interpolation: ultrahdr.InterpolationLanczos2,
		Background:    color.Black,
	})
}

func Rebase added in v0.0.6

func Rebase(data []byte, newSDR image.Image, opts ...RebaseOption) (*Result, error)

Rebase replaces the primary SDR image while adjusting the gainmap to preserve the original HDR reconstruction as closely as possible.

func Split

func Split(r io.Reader) (*Result, error)

Split extracts primary/gainmap JPEGs, metadata, and raw XMP/ISO segments.

Example (JoinWithBundle)
package main

import (
	"os"

	"github.com/vearutop/ultrahdr"
)

func main() {
	f, err := os.Open("testdata/uhdr.jpg")
	if err != nil {
		return
	}
	defer f.Close()
	sr, err := ultrahdr.Split(f)
	if err != nil {
		return
	}
	bundle, err := sr.BuildMetadataBundle()
	if err != nil {
		return
	}
	_, _ = ultrahdr.Join(sr.Primary, sr.Gainmap, bundle, nil)
}

func (*Result) BuildMetadataBundle added in v0.0.6

func (r *Result) BuildMetadataBundle() (*MetadataBundle, error)

BuildMetadataBundle builds a metadata bundle from split segments and primary JPEG.

func (Result) Join added in v0.0.6

func (sr Result) Join() ([]byte, error)

Join assembles a JPEG/R container using raw metadata segments. PrimaryXMP is updated to reflect the new gainmap length.

Directories

Path Synopsis
cmd
uhdrtool command
Package main is a command-line tool for working with UltraHDR JPEGs.
Package main is a command-line tool for working with UltraHDR JPEGs.
internal
jpegx
Package jpegx is based on image/jpeg.
Package jpegx is based on image/jpeg.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL