-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement Apple vcgp (Video Card Gamma & Primary) tag decoder #25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). | |||||||||||||||||
|
|
||||||||||||||||||
| ## [Unreleased] | ||||||||||||||||||
|
|
||||||||||||||||||
| ### Added | ||||||||||||||||||
|
|
||||||||||||||||||
| * **`vcgp` decoder** — Apple `vcgp` (Video Card Gamma Profile) tag now decodes | ||||||||||||||||||
| to structured TOML with per-channel fields (`gamma_in`, `gamma_out`, `min`, | ||||||||||||||||||
| `max`) instead of a raw hex dump. All four values are decoded as S15.16 | ||||||||||||||||||
| fixed-point, consistent with the encoding used by the related `vcgt` formula | ||||||||||||||||||
| type. | ||||||||||||||||||
|
Comment on lines
+19
to
+22
|
||||||||||||||||||
| to structured TOML with per-channel fields (`gamma_in`, `gamma_out`, `min`, | |
| `max`) instead of a raw hex dump. All four values are decoded as S15.16 | |
| fixed-point, consistent with the encoding used by the related `vcgt` formula | |
| type. | |
| to structured TOML with per-channel fields (`gamma_in`, `gamma_out`, | |
| `primary_x`, `primary_y`) instead of a raw hex dump. `gamma_in` and | |
| `gamma_out` are decoded as S15.16 fixed-point values, while `primary_x` | |
| and `primary_y` are decoded as normalized U32 values. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,40 +1,251 @@ | ||||||||||||||||||||
| // SPDX-License-Identifier: Apache-2.0 OR MIT | ||||||||||||||||||||
| // Copyright (c) 2021-2026, Harbers Bik LLC | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #![allow(unused)] | ||||||||||||||||||||
| //! Apple `vcgp` (Video Card Gamma and Primary) tag data decoder. | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! `vcgp` is an Apple-private tag that combines two aspects of calibrated display | ||||||||||||||||||||
| //! state into one structure. It is not part of the ICC specification. | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! ## Relationship to `vcgt` | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! | Tag | Stands for | Contents | | ||||||||||||||||||||
| //! |--------|-----------------------------------|-----------------------------------| | ||||||||||||||||||||
| //! | `vcgt` | Video Card Gamma Table | Calibration LUT only | | ||||||||||||||||||||
| //! | `vcgp` | Video Card Gamma and Primary | Calibration curves + chromaticity | | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! ## Binary layout | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! ```text | ||||||||||||||||||||
| //! Bytes 0– 3 type signature 'vcgp' (0x76636770) | ||||||||||||||||||||
| //! Bytes 4– 7 reserved 0 | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! ── Calibration section (per-channel gamma, 24 bytes) ────────────────────── | ||||||||||||||||||||
| //! Bytes 8–11 red gamma_in S15.16 — reference / source gamma | ||||||||||||||||||||
| //! Bytes 12–15 red gamma_out S15.16 — target display gamma | ||||||||||||||||||||
| //! Bytes 16–19 green gamma_in S15.16 | ||||||||||||||||||||
| //! Bytes 20–23 green gamma_out S15.16 | ||||||||||||||||||||
| //! Bytes 24–27 blue gamma_in S15.16 | ||||||||||||||||||||
| //! Bytes 28–31 blue gamma_out S15.16 | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! ── Chromaticity section (per-channel primaries, 24 bytes) ───────────────── | ||||||||||||||||||||
| //! Bytes 32–35 red primary_x U32 normalized (value / 0xFFFF_FFFF → [0, 1]) | ||||||||||||||||||||
| //! Bytes 36–39 red primary_y U32 normalized | ||||||||||||||||||||
| //! Bytes 40–43 green primary_x U32 normalized | ||||||||||||||||||||
| //! Bytes 44–47 green primary_y U32 normalized | ||||||||||||||||||||
| //! Bytes 48–51 blue primary_x U32 normalized | ||||||||||||||||||||
| //! Bytes 52–55 blue primary_y U32 normalized | ||||||||||||||||||||
| //! ``` | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! ## Field semantics (reverse-engineered) | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! **Calibration section** — same role as `vcgt`. The `gamma_in` field (observed: 3.0) | ||||||||||||||||||||
| //! encodes the reference or native panel gamma; `gamma_out` (observed: 2.4) is the | ||||||||||||||||||||
| //! target electro-optical transfer function gamma (matching the sRGB TRC). Both are | ||||||||||||||||||||
| //! stored as S15.16 big-endian fixed-point. | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! **Chromaticity section** — distinguishes `vcgp` from `vcgt`. Encodes the effective | ||||||||||||||||||||
| //! RGB primary chromaticities (x, y) of the display after calibration, using a | ||||||||||||||||||||
| //! normalized unsigned 32-bit encoding where `0x00000000` = 0.0 and `0xFFFFFFFF` = 1.0. | ||||||||||||||||||||
| //! | ||||||||||||||||||||
| //! In the test profiles examined (Color LCD, SyncMaster, LG HDR WQHD) all three | ||||||||||||||||||||
| //! channels show the same placeholder values (`x ≈ 0.0`, `y ≈ 0.2`), indicating the | ||||||||||||||||||||
| //! chromaticity section is only populated after a full ColorSync calibration. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| use serde::Serialize; | ||||||||||||||||||||
| use zerocopy::{BigEndian, Immutable, KnownLayout, TryFromBytes, Unaligned, I32, U32}; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Typical structure for Vcgp tag | ||||||||||||||||||||
| pub struct VcgpData { | ||||||||||||||||||||
| // Header/signature | ||||||||||||||||||||
| signature: [u8; 4], // "vcgp" | ||||||||||||||||||||
| use crate::{s15fixed16, tag::tagdata::VcgpData}; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Parameters that might include: | ||||||||||||||||||||
| gamma: f32, // Gamma value (e.g., 2.2) | ||||||||||||||||||||
| black_point: f32, // Black level adjustment | ||||||||||||||||||||
| white_point: f32, // White level adjustment | ||||||||||||||||||||
| // ── Binary layout ───────────────────────────────────────────────────────────── | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Potentially other parameters for: | ||||||||||||||||||||
| // - Contrast | ||||||||||||||||||||
| // - Brightness | ||||||||||||||||||||
| // - Individual RGB channel controls | ||||||||||||||||||||
| /// Fixed-size layout of the `vcgp` payload (56 bytes total including the 8-byte header). | ||||||||||||||||||||
| #[derive(TryFromBytes, KnownLayout, Immutable, Unaligned)] | ||||||||||||||||||||
| #[repr(C, packed)] | ||||||||||||||||||||
| struct Layout { | ||||||||||||||||||||
| _type_signature: [u8; 4], | ||||||||||||||||||||
| _reserved: [u8; 4], | ||||||||||||||||||||
| // Calibration section: per-channel gamma (S15.16) | ||||||||||||||||||||
| red_gamma_in: I32<BigEndian>, | ||||||||||||||||||||
| red_gamma_out: I32<BigEndian>, | ||||||||||||||||||||
| green_gamma_in: I32<BigEndian>, | ||||||||||||||||||||
| green_gamma_out: I32<BigEndian>, | ||||||||||||||||||||
| blue_gamma_in: I32<BigEndian>, | ||||||||||||||||||||
| blue_gamma_out: I32<BigEndian>, | ||||||||||||||||||||
| // Chromaticity section: per-channel primaries (U32 normalized) | ||||||||||||||||||||
| red_primary_x: U32<BigEndian>, | ||||||||||||||||||||
| red_primary_y: U32<BigEndian>, | ||||||||||||||||||||
| green_primary_x: U32<BigEndian>, | ||||||||||||||||||||
| green_primary_y: U32<BigEndian>, | ||||||||||||||||||||
| blue_primary_x: U32<BigEndian>, | ||||||||||||||||||||
| blue_primary_y: U32<BigEndian>, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /* | ||||||||||||||||||||
| use crate::tags::common::*; | ||||||||||||||||||||
| use serde::Serialize; | ||||||||||||||||||||
| // ── Normalization helper ────────────────────────────────────────────────────── | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Convert a U32 chromaticity value to `f64` in [0, 1], rounded to 6 decimal places. | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// The encoding uses the full 32-bit range: `0x00000000` → 0.0, `0xFFFFFFFF` → 1.0. | ||||||||||||||||||||
| /// Rounding to 6 dp removes floating-point noise from the `/ 0xFFFF_FFFF` division while | ||||||||||||||||||||
| /// preserving more precision than ICC profiles ever report (4 significant figures). | ||||||||||||||||||||
| #[inline] | ||||||||||||||||||||
| fn norm_u32(v: u32) -> f64 { | ||||||||||||||||||||
| let raw = v as f64 / u32::MAX as f64; | ||||||||||||||||||||
| (raw * 1_000_000.0).round() / 1_000_000.0 | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ── Parsed / serialisable representation ────────────────────────────────────── | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /// Per-channel data decoded from an Apple `vcgp` tag. | ||||||||||||||||||||
| #[derive(Serialize)] | ||||||||||||||||||||
| pub struct VcgpChannel { | ||||||||||||||||||||
| /// Reference or source gamma (observed: 3.0), stored as S15.16. | ||||||||||||||||||||
| pub gamma_in: f64, | ||||||||||||||||||||
| /// Target display gamma (observed: 2.4, matching the sRGB electro-optical TRC), | ||||||||||||||||||||
| /// stored as S15.16. | ||||||||||||||||||||
| pub gamma_out: f64, | ||||||||||||||||||||
| /// Primary chromaticity x coordinate, normalized U32 encoding. | ||||||||||||||||||||
| /// Populated after ColorSync calibration; placeholder value ≈ 0.0 otherwise. | ||||||||||||||||||||
| pub primary_x: f64, | ||||||||||||||||||||
| /// Primary chromaticity y coordinate, normalized U32 encoding. | ||||||||||||||||||||
| /// Placeholder value ≈ 0.2 in uncalibrated profiles. | ||||||||||||||||||||
| pub primary_y: f64, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Debug, Serialize, Clone, PartialEq)] | ||||||||||||||||||||
| pub struct Vcgp { | ||||||||||||||||||||
| tbd: Vec<u8>, // can not find any information about this tag | ||||||||||||||||||||
| /// Decoded Apple `vcgp` (Video Card Gamma and Primary) tag. | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// Serialises to TOML as three per-channel sections: | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// ```toml | ||||||||||||||||||||
| /// [vcgp.red] | ||||||||||||||||||||
| /// gamma_in = 3.0 | ||||||||||||||||||||
| /// gamma_out = 2.4 | ||||||||||||||||||||
| /// primary_x = 0.0 | ||||||||||||||||||||
| /// primary_y = 0.2 | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// [vcgp.green] | ||||||||||||||||||||
| /// # … identical in uncalibrated profiles | ||||||||||||||||||||
| /// | ||||||||||||||||||||
| /// [vcgp.blue] | ||||||||||||||||||||
| /// # … identical in uncalibrated profiles | ||||||||||||||||||||
| /// ``` | ||||||||||||||||||||
| #[derive(Serialize)] | ||||||||||||||||||||
| pub struct VcgpType { | ||||||||||||||||||||
| pub red: VcgpChannel, | ||||||||||||||||||||
| pub green: VcgpChannel, | ||||||||||||||||||||
| pub blue: VcgpChannel, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl Vcgp { | ||||||||||||||||||||
| pub fn try_new(buf: &mut &[u8]) -> Result<Self> { | ||||||||||||||||||||
| Ok(Vcgp { | ||||||||||||||||||||
| tbd: read_vec(buf, buf.len())?, | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| // ── Parser ──────────────────────────────────────────────────────────────────── | ||||||||||||||||||||
|
|
||||||||||||||||||||
| impl From<&VcgpData> for VcgpType { | ||||||||||||||||||||
| fn from(data: &VcgpData) -> Self { | ||||||||||||||||||||
| let fallback = || VcgpType { | ||||||||||||||||||||
| red: VcgpChannel { gamma_in: 0.0, gamma_out: 0.0, primary_x: 0.0, primary_y: 0.0 }, | ||||||||||||||||||||
| green: VcgpChannel { gamma_in: 0.0, gamma_out: 0.0, primary_x: 0.0, primary_y: 0.0 }, | ||||||||||||||||||||
| blue: VcgpChannel { gamma_in: 0.0, gamma_out: 0.0, primary_x: 0.0, primary_y: 0.0 }, | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let Some(layout) = Layout::try_ref_from_bytes(data.0.as_slice()).ok() else { | ||||||||||||||||||||
| return fallback(); | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
Comment on lines
+148
to
+150
|
||||||||||||||||||||
| let Some(layout) = Layout::try_ref_from_bytes(data.0.as_slice()).ok() else { | |
| return fallback(); | |
| }; | |
| let Some(prefix) = data.0.get(..core::mem::size_of::<Layout>()) else { | |
| return fallback(); | |
| }; | |
| let Some(layout) = Layout::try_ref_from_bytes(prefix).ok() else { | |
| return fallback(); | |
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CHANGELOG description calls
vcgp“Video Card Gamma Profile”, but this PR (and the new decoder) treats it as “Video Card Gamma and Primary”. Updating the wording will reduce confusion withvcgtand align with the tag’s purpose.