hdr10plus

package module
v0.0.0-...-7292317 Latest Latest
Warning

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

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 17 Imported by: 1

README

go-hdr10-plus

A Go port of the HDR10+ brightness plotting functionality from quietvoid/hdr10plus_tool, primarily intended for use with upbrr.

A pure-Go HDR10+ brightness plotting library for Go 1.27 or newer. Decode upstream-style metadata JSON or supply Go values, choose a peak estimator and an inclusive frame crop, and render an opaque 3000 × 1200 image in memory.

go get github.com/Audionut/go-hdr10-plus
package application

import (
    "bytes"
    "image/png"

    hdr10plus "github.com/Audionut/go-hdr10-plus"
)

func plotPNG(input []byte) ([]byte, error) {
    metadata, err := hdr10plus.DecodeJSON(bytes.NewReader(input))
    if err != nil {
        return nil, err
    }
    img, err := hdr10plus.Render(metadata, hdr10plus.Options{
        Title: "My HDR10+ plot",
        PeakSource: hdr10plus.PeakHistogram,
    })
    if err != nil {
        return nil, err
    }
    var output bytes.Buffer
    if err := png.Encode(&output, img); err != nil {
        return nil, err
    }
    return output.Bytes(), nil
}

For direct input, use Metadata{Profile: "A", SceneCount: 1, Frames: []Frame{...}}. AverageRGB, DistributionValues, and MaxSCL are unsigned integers in 0.1-nit units: 1000 represents 100 nits. Metadata is a plotting model; marshaling it does not produce the upstream wire format. SceneCount must be nonnegative and always describes the whole input. Profile is a display label, including an empty string if provided.

Options{} uses title HDR10+ plot, PeakHistogram, and all frames. An empty title selects the default. Range: &FrameRange{Start: 20, End: 40} selects 21 frames by array position and rebases their x coordinates to zero. A nonnil FrameRange{0, 0} selects the first frame. The frame count and legend statistics describe the crop; the profile and scene count describe the full input.

Peak source Calculation
PeakHistogram Maximum supplied distribution value / 10
PeakHistogram99 Last supplied distribution value / 10, preserving order
PeakMaxSCL Maximum supplied component / 10; any nonempty list
PeakMaxSCLLuminance (0.2627*R + 0.678*G + 0.0593*B) / 10; exactly three RGB components

The y axis is linear in ST 2084 PQ codes with tick labels in nits. MaxCLL and MaxFALL follow the upstream plot's maximum peak and average series terminology. Each legend avg is inverse PQ of the mean PQ code, rather than arithmetic mean nits. Values above 10,000 nits remain in statistics and are clipped only at the chart viewport. An average above its peak remains valid. Single-frame plots include an opaque two-pixel sample marker.

DecodeJSON accepts exactly one JSON object followed by optional whitespace. It requires a nonnull string at JSONInfo.HDR10plusProfile, a nonempty SceneInfo array of objects containing LuminanceParameters.AverageRGB, and a nonnull SceneInfoSummary.SceneFrameNumbers array. AverageRGB is uint32; scene frame numbers are uint64 and only their array length is retained. Zero values and an empty scene summary are valid. Null numeric elements, negative/fractional numbers, overflow, invalid containers, and trailing JSON values are errors.

MaxScl and LuminanceDistributions.DistributionValues may be missing, null, or empty when unused. Every supplied plotting field is checked during decoding; rendering requires the selected peak source only for selected frames. Unconsumed extensions, including DistributionIndex, are ignored. Standard encoding/json case matching and duplicate processing apply. This plotting projection accepts smaller documents than upstream; it is not full HDR10+ profile/schema validation.

Classify errors with errors.Is(err, hdr10plus.ErrInvalidMetadata) or errors.Is(err, hdr10plus.ErrInvalidOptions). Original JSON errors remain available to errors.As, and reader failures retain their identity. Error wording is not an API guarantee. Failed operations return nil results.

The library does not close readers, open files, log, start goroutines, or mutate inputs. Every image owns its pixels. Concurrent renders may share unchanged metadata; callers must not mutate it while rendering. Applications own PNG encoding and handle writer errors. There is no CLI in the plotting package, resampling, arbitrary sizing, or alternate output format.

Both operations are synchronous and have no cancellation parameter. Applications own reader size limits and deadlines; io.LimitReader can bound input, with truncated JSON returning an error. Decode memory grows with input/frame/peak entry count. Render uses O(selected frames) samples, about 14.4 MB for the canvas, and bounded raster buffers. It does not downsample long inputs.

Representative render measurements on an Intel Core i5-14600K (14 cores, 20 logical processors), Windows amd64, Go 1.27.1, using go test -run '^$' -bench . -benchmem ./.... Inputs vary average brightness with constant histogram peaks; setup and PNG encoding are excluded. These are local measurements, not latency guarantees:

Selected frames Time/render Allocated bytes/render Allocations/render
259 49.3 ms 35,531,855 185
100,000 75.9 ms 37,133,525 235
1,000,000 421.9 ms 51,536,386 234

Numerical behavior is based on quietvoid/hdr10plus_tool revision 82cb01b. Go Regular is embedded, so text metrics and rasterization differ from Rust's fonts. An additional 20 pixels separate the y description from tick labels for readability. X ticks use the smallest 1, 2, 5 × 10^k interval at least max(1, ceil(frameCount/24)). Exact text baselines and PNG bytes are not parity guarantees. Numeric references and decoded-pixel Go goldens are independent.

Development checks:

go mod verify
gofmt -l .
go vet ./...
go test ./...
go test -race ./...
go test -run '^$' -fuzz '^FuzzDecodeJSON$' -fuzztime 30s .
go test -run '^$' -bench . -benchmem ./...
$env:CGO_ENABLED = '0'
go test ./...

See test fixture provenance for the Rust oracle, optional real-input differential check, tolerances, and visual baselines. The new Go implementation is MIT licensed; retained upstream and font notices are in THIRD_PARTY_NOTICES.md.

Native extraction is available in the separate extract package. Extract(ctx, seeker, extract.Options{}) detects raw Annex B HEVC, Matroska, ordinary/fragmented MP4, TS and M2TS by signature; DecodeT35 accepts complete registered HDR10+ payloads. The supported metadata dialect is application ID 4, version 1, one window, with no actual-peak grids or saturation mapping. Values, percentile indexes, target luminance and curves are retained. Metadata inheritance follows decode order before presentation ordering. The HEVC scope is progressive, base-layer Main/Main 10 with supported parameter and reference syntax. Unsupported extensions return extract.ErrUnsupportedInput. TrackID zero requires a unique HEVC video track; explicit IDs select the Matroska track number, MP4 track ID, or TS PID. Raw HEVC rejects nonzero IDs.

Container Timing and selection
Matroska All four lace modes, signed block timecodes, default/block duration, codec delay, CRC, unknown Segment/Cluster sizes; Cluster timestamp precedes its blocks
MP4 Complete ordinary tables, compact sizes, signed composition offsets, supported description changes, fragment defaults/offsets and one forward media edit with optional initial empty edit
TS/M2TS Bounded PAT/PMT/PES headers, CRC, original zero/33-bit timestamps, duplicate removal with permitted PCR updates, continuity checks and normal EOF

Encryption/compression, linked Matroska segments, CodecState changes, non-unit track scale, ambiguous sample timing, repeated/rate-changing MP4 edits and unverified transport resets fail explicitly. Samples contain one supported picture AU; several slices of that picture are accepted. Missing transport PTS can permit a frame-index result; timed playlist trims require proven clocks. Transport preflight checks mandatory markers, reserved bits, optional field extents, clocks, rates and stuffing. DSM trick modes, embedded MPEG-1 pack headers and embedded system headers return extract.ErrUnsupportedInput. PES CRC bytes are bounded diagnostic fields; their checksum is not verified.

result, err := extract.Extract(ctx, file, extract.Options{})
if err != nil {
    return err
}
metadata, err := result.PlotMetadata()
if err != nil {
    return err
}
img, err := hdr10plus.Render(metadata, hdr10plus.Options{})

Applications own the context, reader lifetime and image output. Extraction is synchronous, reads compressed bytes through EOF, and invokes no subprocess or pixel decoder. Success covers identified output pictures, not universal video integrity: a raw cut within skipped entropy data can be structurally indistinguishable. Cancellation and reader errors preserve their identities; absence, incomplete coverage, unsupported input/metadata, malformed syntax, ambiguous tracks and resource exhaustion have separate errors.Is sentinels. Results own their arrays; PlotMetadata validates and deep-copies them.

extract.Limits{} uses finite defaults: two million physical/output pictures, 512 MiB accounted retained state, 64 KiB parameter sets and slice headers, 1 MiB SEI, and at most 64 queued pictures. These quotas account owned extraction storage rather than process RSS. File extraction and assembly reserve 2 MiB for resolution plus 128 KiB for POC history. A shared MemoryBudget additionally bounds concurrent collectors and retained results for one session. Streams are single owner; separate streams and reservation tokens can share the budget concurrently. MemoryBudget.Usage reports charged and peak bytes. Completed results remain charged for that session. Container indexes and block scratch share the operation quota with collected syntax and output; selected compressed bodies are streamed or bounded by the block limit. These limits do not measure allocator overhead.

NewStream/Push/Finish collect immutable physical Clip records, borrowing each chunk only until Push returns. Abort preserves the original failure. Assemble accepts completed clips and exact neutral playlist occurrences, with half-open 45-kHz trims and explicit reset/seamless connections. It replays decode state for each occurrence, resolves required reference lookahead and recomputes final scenes/profile. Assembly accepts no reader and performs no IO. Unknown clocks, missing references and unprovable splices return explicit errors. Transport PacketRange boundaries require observed PES starts or physical start and EOF. A producer that strips PES headers supplies Marker.PESHeader for validation and a final SourceEnd marker carrying the observed packet count. These borrowed headers are not retained. A packet count does not measure IO calls.

The development BDInfo bridge is a separate GPL-compatible module using the local, unreleased issue-45 API. It is absent from the root dependency graph. Tests cover folder/UDF ISO timelines, shared/repeated clips, selected STC epochs, supported seamless reference cuts, complete ES delivery and bounded failures. Release pinning and private-media storage/performance measurements remain external validation gates. No performance overhead percentage is claimed. See native fixture provenance.

Documentation

Overview

Package hdr10plus decodes HDR10+ plotting metadata and renders brightness charts entirely in Go. Frame luminance values use units of 0.1 nit.

The numerical behavior follows quietvoid/hdr10plus_tool at revision 82cb01ba387184476dbf5fa3bddebf0a345693d7, including means calculated in PQ space. Embedded fonts and x-axis tick placement deliberately differ.

The API is synchronous: applications own reader limits, deadlines, PNG encoding, and output files. It does not perform full HDR10+ schema or profile validation. Metadata is a plotting model, not a JSON round-trip model.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrInvalidMetadata = errors.New("invalid HDR10+ metadata")

ErrInvalidMetadata identifies invalid plotting data or JSON. Use errors.Is; error wording is not a stable API. Underlying JSON and reader errors are kept.

View Source
var ErrInvalidOptions = errors.New("invalid HDR10+ plot options")

ErrInvalidOptions identifies an unknown peak source or invalid frame range. Callers can classify wrapped errors using errors.Is.

Functions

func Render

func Render(metadata *Metadata, options Options) (image.Image, error)

Render returns a fresh opaque 3000×1200 *image.RGBA. It validates the selected peak source only within the inclusive range, before allocating the canvas. Neither metadata nor options is modified. Concurrent calls may share input provided callers do not mutate it. Failed calls return a nil image.

Example
package main

import (
	"fmt"

	hdr10plus "github.com/Audionut/go-hdr10-plus"
)

func main() {
	metadata := &hdr10plus.Metadata{Profile: "A", SceneCount: 2, Frames: []hdr10plus.Frame{
		{AverageRGB: 100, MaxSCL: []uint32{1000, 2000, 3000}},
		{AverageRGB: 5000, MaxSCL: []uint32{7000, 5000, 9000}},
	}}
	// Raw values use 0.1 nit. The range is inclusive and scene count stays 2.
	img, err := hdr10plus.Render(metadata, hdr10plus.Options{PeakSource: hdr10plus.PeakMaxSCLLuminance, Range: &hdr10plus.FrameRange{Start: 1, End: 1}})
	if err != nil {
		panic(err)
	}
	fmt.Println(img.Bounds())
}
Output:
(0,0)-(3000,1200)

Types

type Frame

type Frame struct {
	AverageRGB         uint32
	DistributionValues []uint32
	MaxSCL             []uint32
}

Frame contains luminance values in units of 0.1 nit. DistributionValues retains wire order. MaxSCL holds ordered red, green, blue components when using PeakMaxSCLLuminance; PeakMaxSCL accepts any nonempty component list.

type FrameRange

type FrameRange struct {
	Start int
	End   int
}

FrameRange selects array positions, including both Start and End.

type Metadata

type Metadata struct {
	Profile    string
	SceneCount int
	Frames     []Frame
}

Metadata holds plotting data in frame order. SceneCount describes the whole input, including when rendering a crop. Profile is an unvalidated display label.

func DecodeJSON

func DecodeJSON(r io.Reader) (*Metadata, error)

DecodeJSON reads exactly one upstream-style JSON object, ignoring fields not consumed by plotting. Optional peak lists may be unavailable; Render checks only the selected source and frames. It does not close r. Returned slices are owned by the result. A failed call returns nil metadata.

Example
package main

import (
	"bytes"
	"fmt"
	"image/png"
	"strings"

	hdr10plus "github.com/Audionut/go-hdr10-plus"
)

func main() {
	const input = `{"JSONInfo":{"HDR10plusProfile":"B"},"SceneInfo":[{"LuminanceParameters":{"AverageRGB":100,"MaxScl":[1000,2000,3000]}}],"SceneInfoSummary":{"SceneFrameNumbers":[1]}}`
	metadata, err := hdr10plus.DecodeJSON(strings.NewReader(input))
	if err != nil {
		panic(err)
	}
	img, err := hdr10plus.Render(metadata, hdr10plus.Options{Title: "My HDR10+ plot", PeakSource: hdr10plus.PeakMaxSCL})
	if err != nil {
		panic(err)
	}
	var output bytes.Buffer
	if err := png.Encode(&output, img); err != nil {
		panic(err)
	}
	decoded, err := png.Decode(&output)
	if err != nil {
		panic(err)
	}
	fmt.Println(decoded.Bounds())
}
Output:
(0,0)-(3000,1200)

type Options

type Options struct {
	Title      string
	PeakSource PeakSource
	Range      *FrameRange
}

Options configures a chart. The zero value selects all frames, PeakHistogram, and the title "HDR10+ plot". A nonnil Range{0, 0} selects only the first frame.

type PeakSource

type PeakSource uint8

PeakSource selects the metadata estimator used for peak brightness.

const (
	// PeakHistogram uses the maximum supplied distribution value (the default).
	PeakHistogram PeakSource = iota
	// PeakHistogram99 uses the last supplied distribution value, without sorting.
	PeakHistogram99
	// PeakMaxSCL uses the maximum supplied MaxSCL component.
	PeakMaxSCL
	// PeakMaxSCLLuminance weights exactly three ordered RGB components.
	PeakMaxSCLLuminance
)

Directories

Path Synopsis
Package extract collects raw Annex B and length-prefixed HEVC syntax, decodes the single-window, application-version-1 HDR10+ registered T.35 dialect, and projects resolved picture metadata into the parent package's plotting model.
Package extract collects raw Annex B and length-prefixed HEVC syntax, decodes the single-window, application-version-1 HDR10+ registered T.35 dialect, and projects resolved picture metadata into the parent package's plotting model.
integration
bdinfo module

Jump to

Keyboard shortcuts

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