├── .gitignore ├── assets ├── 01external-gen.jpg └── 02internal-gen.jpg ├── src ├── app │ ├── utils.rs │ ├── read_file_ops.rs │ ├── eframe_app.rs │ ├── calibration │ │ ├── results_summary.rs │ │ ├── luminance_plot.rs │ │ ├── rgb_balance_plot.rs │ │ ├── gamma_tracking_plot.rs │ │ ├── mod.rs │ │ └── cie_diagram_plot.rs │ ├── external_generator_ui.rs │ ├── mod.rs │ └── internal_generator_ui.rs ├── generators │ ├── mod.rs │ ├── resolve.rs │ ├── internal.rs │ └── tcp_generator_client.rs ├── calibration │ ├── mod.rs │ ├── cct.rs │ ├── luminance_eotf.rs │ └── reading_result.rs ├── main.rs ├── pgen │ ├── controller │ │ ├── daemon.rs │ │ ├── mod.rs │ │ └── handler.rs │ ├── pattern_config.rs │ ├── mod.rs │ ├── commands.rs │ └── client.rs ├── utils.rs ├── external.rs └── spotread.rs ├── .github ├── actions │ └── setup-release-env │ │ └── action.yml └── workflows │ ├── ci.yml │ └── release.yml ├── Cargo.toml ├── README.md └── data └── CIE_cc_1931_2deg.csv /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /assets/01external-gen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/pgen_client/HEAD/assets/01external-gen.jpg -------------------------------------------------------------------------------- /assets/02internal-gen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quietvoid/pgen_client/HEAD/assets/02internal-gen.jpg -------------------------------------------------------------------------------- /src/app/utils.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui; 2 | 3 | pub fn is_dragvalue_finished(res: egui::Response) -> bool { 4 | !res.has_focus() && (res.drag_stopped() || res.lost_focus()) 5 | } 6 | -------------------------------------------------------------------------------- /.github/actions/setup-release-env/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup release env 2 | runs: 3 | using: "composite" 4 | steps: 5 | - name: Install Rust 6 | uses: dtolnay/rust-toolchain@stable 7 | 8 | - name: Get the package versions 9 | shell: bash 10 | run: | 11 | RELEASE_PKG_VERSION=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[].version') 12 | 13 | echo "RELEASE_PKG_VERSION=${RELEASE_PKG_VERSION}" >> $GITHUB_ENV 14 | echo "ARCHIVE_PREFIX=${{ env.RELEASE_BIN }}-${RELEASE_PKG_VERSION}" >> $GITHUB_ENV 15 | 16 | - name: Create artifacts directory 17 | shell: bash 18 | run: | 19 | mkdir ${{ env.RELEASE_DIR }} 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | ci: 11 | name: Check, test, rustfmt and clippy 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Install dependencies 17 | run: sudo apt-get install -y libgtk-3-dev 18 | 19 | - name: Install Rust, clippy and rustfmt 20 | uses: dtolnay/rust-toolchain@stable 21 | with: 22 | components: clippy, rustfmt 23 | 24 | - name: Check 25 | run: | 26 | cargo check --workspace --all-features 27 | 28 | - name: Test 29 | run: | 30 | cargo test --workspace --all-features 31 | 32 | - name: Rustfmt 33 | run: | 34 | cargo fmt --all --check 35 | 36 | - name: Clippy 37 | run: | 38 | cargo clippy --workspace --all-features --all-targets --tests -- --deny warnings 39 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pgen_client" 3 | version = "1.0.5" 4 | authors = ["quietvoid"] 5 | edition = "2024" 6 | rust-version = "1.88.0" 7 | license = "GPL-3.0" 8 | 9 | [dependencies] 10 | clap = { version = "4.5.51", features = ["derive", "wrap_help", "deprecated"] } 11 | log = "0.4.28" 12 | regex = "*" 13 | once_cell = "*" 14 | pretty_env_logger = "0.5" 15 | clap-verbosity-flag = "3.0.4" 16 | anyhow = "1.0.100" 17 | itertools = "0.14.0" 18 | ndarray = { version = "0.16.1", features = ["rayon"] } 19 | strum = { version = "0.27.2", features = ["derive"] } 20 | base64 = "0.22.1" 21 | csv = "1.4.0" 22 | rfd = "0.15.4" 23 | serde = "1.0.228" 24 | serde_derive = "1.0.228" 25 | yaserde = { version = "0.12.0", features = ["yaserde_derive"] } 26 | 27 | tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync", "process"] } 28 | tokio-stream = "0.1.17" 29 | futures = "0.3.31" 30 | async-stream = "0.3.6" 31 | 32 | eframe = { version = "0.33.0", features = ["persistence"] } 33 | egui_extras = "0.33.0" 34 | egui_plot = "0.34.0" 35 | ecolor = "*" 36 | kolor-64 = "0.1.9" 37 | deltae = "0.3.2" 38 | 39 | [profile.release-deploy] 40 | inherits = "release" 41 | lto = "thin" 42 | strip = "symbols" 43 | -------------------------------------------------------------------------------- /src/app/read_file_ops.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use serde::Deserialize; 4 | 5 | use crate::generators::internal::InternalPattern; 6 | 7 | use super::PGenApp; 8 | 9 | #[derive(Deserialize)] 10 | struct CsvPatchRecord { 11 | _idx: usize, 12 | red: u16, 13 | green: u16, 14 | blue: u16, 15 | _label: Option, 16 | } 17 | 18 | pub fn parse_patch_list_csv_file(app: &mut PGenApp, path: PathBuf) { 19 | let reader = csv::ReaderBuilder::new().has_headers(false).from_path(path); 20 | 21 | if let Ok(mut rdr) = reader { 22 | let pattern_iter = rdr 23 | .deserialize::() 24 | .filter_map(Result::ok) 25 | .map(Into::into); 26 | 27 | app.cal_state.internal_gen.list.clear(); 28 | app.cal_state.internal_gen.list.extend(pattern_iter); 29 | 30 | log::trace!( 31 | "Patch list CSV loaded: {} patches.", 32 | app.cal_state.internal_gen.list.len() 33 | ); 34 | } 35 | } 36 | 37 | impl From for InternalPattern { 38 | fn from(record: CsvPatchRecord) -> Self { 39 | Self { 40 | rgb: [record.red, record.green, record.blue], 41 | ..Default::default() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/generators/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use strum::{AsRefStr, Display, EnumIs, EnumIter}; 3 | 4 | pub mod internal; 5 | pub mod resolve; 6 | pub mod tcp_generator_client; 7 | 8 | pub use tcp_generator_client::{ 9 | TcpGeneratorClient, TcpGeneratorInterface, start_tcp_generator_client, 10 | }; 11 | 12 | #[derive(Debug, Default, Deserialize, Serialize, Clone, Copy)] 13 | pub struct GeneratorState { 14 | pub client: GeneratorClient, 15 | pub listening: bool, 16 | } 17 | 18 | #[derive( 19 | Display, 20 | AsRefStr, 21 | Debug, 22 | Default, 23 | Deserialize, 24 | Serialize, 25 | Copy, 26 | Clone, 27 | PartialEq, 28 | Eq, 29 | EnumIter, 30 | EnumIs, 31 | )] 32 | pub enum GeneratorType { 33 | #[default] 34 | Internal, 35 | External, 36 | } 37 | 38 | #[derive(Debug, Deserialize, Serialize, Clone, Copy)] 39 | pub enum GeneratorInterface { 40 | Tcp(TcpGeneratorInterface), 41 | } 42 | 43 | #[derive( 44 | Display, AsRefStr, Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, EnumIter, 45 | )] 46 | pub enum GeneratorClient { 47 | #[default] 48 | Resolve, 49 | } 50 | 51 | #[derive(Debug, Clone)] 52 | pub enum GeneratorClientCmd { 53 | Shutdown, 54 | } 55 | 56 | impl GeneratorState { 57 | pub fn initial_setup(&mut self) { 58 | self.listening = false; 59 | } 60 | } 61 | 62 | impl GeneratorClient { 63 | pub const fn interface(&self) -> GeneratorInterface { 64 | match self { 65 | Self::Resolve => GeneratorInterface::Tcp(TcpGeneratorInterface::Resolve), 66 | } 67 | } 68 | } 69 | 70 | impl GeneratorInterface { 71 | pub const fn client(&self) -> GeneratorClient { 72 | match self { 73 | GeneratorInterface::Tcp(tcp_interface) => match tcp_interface { 74 | TcpGeneratorInterface::Resolve => GeneratorClient::Resolve, 75 | }, 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/app/eframe_app.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{self, Key}; 2 | 3 | use super::{PGenApp, PGenAppSavedState}; 4 | 5 | impl eframe::App for PGenApp { 6 | fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] { 7 | visuals.window_fill().to_normalized_gamma_f32() 8 | } 9 | 10 | fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { 11 | if !self.requested_close && !self.allowed_to_close { 12 | let close_requested = ctx.input(|i| { 13 | i.viewport().close_requested() 14 | || i.key_pressed(Key::Q) 15 | || i.key_pressed(Key::Escape) 16 | }); 17 | 18 | if close_requested { 19 | self.close(); 20 | ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose); 21 | } 22 | } 23 | 24 | self.set_top_bar(ctx); 25 | self.set_right_panel(ctx); 26 | self.set_central_panel(ctx); 27 | 28 | self.check_responses(ctx); 29 | 30 | if self.requested_close && self.allowed_to_close { 31 | self.requested_close = false; 32 | log::info!("Cleared queue, closing app"); 33 | 34 | if let Some(storage) = frame.storage_mut() { 35 | save_config(self, storage); 36 | } 37 | 38 | ctx.send_viewport_cmd(egui::ViewportCommand::Close); 39 | } 40 | } 41 | 42 | fn save(&mut self, storage: &mut dyn eframe::Storage) { 43 | save_config(self, storage); 44 | } 45 | } 46 | 47 | fn save_config(app: &mut PGenApp, storage: &mut dyn eframe::Storage) { 48 | eframe::set_value( 49 | storage, 50 | eframe::APP_KEY, 51 | &PGenAppSavedState { 52 | state: app.state.clone(), 53 | editing_socket: app.editing_socket.clone(), 54 | generator_type: app.generator_type, 55 | generator_state: app.generator_state, 56 | cal_state: app.cal_state.clone(), 57 | }, 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/app/calibration/results_summary.rs: -------------------------------------------------------------------------------- 1 | use eframe::egui::{self, Ui}; 2 | 3 | use crate::calibration::ReadingResult; 4 | 5 | use super::CalibrationState; 6 | 7 | pub fn draw_results_summary_ui( 8 | ui: &mut Ui, 9 | cal_state: &mut CalibrationState, 10 | results: &[ReadingResult], 11 | ) { 12 | let target_rgb_to_xyz = cal_state.target_rgb_to_xyz_conv(); 13 | 14 | let minmax_y = cal_state.internal_gen.minmax_y(); 15 | let avg_delta_e2000 = ReadingResult::results_average_delta_e2000(results, target_rgb_to_xyz); 16 | let avg_delta_e2000_incl_lum = 17 | ReadingResult::results_average_delta_e2000_incl_luminance(results, target_rgb_to_xyz); 18 | let avg_gamma_str = if let Some(avg_gamma) = ReadingResult::results_average_gamma(results) { 19 | format!("{avg_gamma:.4}") 20 | } else { 21 | "N/A".to_string() 22 | }; 23 | 24 | ui.heading("Results"); 25 | ui.indent("cal_results_summary_indent", |ui| { 26 | if ui.button("Clear results").clicked() { 27 | cal_state.internal_gen.selected_idx = None; 28 | cal_state.internal_gen.list.iter_mut().for_each(|e| { 29 | e.result.take(); 30 | }) 31 | } 32 | egui::Grid::new("cal_results_summary_grid") 33 | .spacing([4.0, 4.0]) 34 | .show(ui, |ui| { 35 | if let Some((min_y, max_y)) = minmax_y { 36 | ui.label(format!("Y Min: {min_y:.6} nits")); 37 | 38 | ui.add_space(5.0); 39 | ui.label(format!("Y Max: {max_y:.6} nits")); 40 | ui.end_row(); 41 | } 42 | 43 | ui.label(format!("Average dE2000: {avg_delta_e2000:.4}")); 44 | ui.label(format!( 45 | "Average dE2000 w/ lum: {avg_delta_e2000_incl_lum:.4}" 46 | )); 47 | ui.end_row(); 48 | 49 | ui.label(format!("Average gamma: {avg_gamma_str}")); 50 | ui.end_row(); 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /src/app/external_generator_ui.rs: -------------------------------------------------------------------------------- 1 | use eframe::{ 2 | egui::{self, Context, Sense, Stroke, Ui}, 3 | epaint::Vec2, 4 | }; 5 | use strum::IntoEnumIterator; 6 | 7 | use crate::{external::ExternalJobCmd, generators::GeneratorClient}; 8 | 9 | use super::{PGenApp, status_color_active}; 10 | 11 | pub fn add_external_generator_ui(app: &mut PGenApp, ctx: &Context, ui: &mut Ui) { 12 | ui.horizontal(|ui| { 13 | ui.label("Generator client"); 14 | ui.add_enabled_ui(!app.generator_state.listening, |ui| { 15 | egui::ComboBox::from_id_salt(egui::Id::new("generator_client")) 16 | .selected_text(app.generator_state.client.as_ref()) 17 | .show_ui(ui, |ui| { 18 | for client in GeneratorClient::iter() { 19 | ui.selectable_value( 20 | &mut app.generator_state.client, 21 | client, 22 | client.as_ref(), 23 | ); 24 | } 25 | }); 26 | }); 27 | 28 | let generator_label = if app.generator_state.listening { 29 | "Stop generator client" 30 | } else { 31 | "Start generator client" 32 | }; 33 | let status_color = status_color_active(ctx, app.generator_state.listening); 34 | ui.add_enabled_ui(app.state.connected_state.connected, |ui| { 35 | if ui.button(generator_label).clicked() { 36 | let cmd = if app.generator_state.listening { 37 | ExternalJobCmd::StopGeneratorClient(app.generator_state.client) 38 | } else { 39 | ExternalJobCmd::StartGeneratorClient(app.generator_state.client) 40 | }; 41 | 42 | app.ctx.external_tx.try_send(cmd).ok(); 43 | } 44 | 45 | let (res, painter) = ui.allocate_painter(Vec2::new(16.0, 16.0), Sense::hover()); 46 | painter.circle(res.rect.center(), 8.0, status_color, Stroke::NONE); 47 | }); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: Artifacts 5 | 6 | env: 7 | RELEASE_BIN: pgen_client 8 | RELEASE_DIR: artifacts 9 | BUILD_PROFILE: release-deploy 10 | WINDOWS_TARGET: x86_64-pc-windows-msvc 11 | WINDOWS_ARM_TARGET: aarch64-pc-windows-msvc 12 | 13 | jobs: 14 | windows-binary: 15 | runs-on: windows-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: ./.github/actions/setup-release-env 20 | 21 | - name: Build 22 | run: | 23 | rustup target add ${{ env.WINDOWS_ARM_TARGET }} 24 | 25 | cargo build --profile ${{ env.BUILD_PROFILE }} 26 | cargo build --profile ${{ env.BUILD_PROFILE }} --target ${{ env.WINDOWS_ARM_TARGET }} 27 | 28 | - name: Create zipfile 29 | shell: bash 30 | run: | 31 | X86_64_ARCHIVE_FILE=${{ env.RELEASE_DIR }}/${{ env.ARCHIVE_PREFIX }}-${{ env.WINDOWS_TARGET }}.zip 32 | AARCH64_ARCHIVE_FILE=${{ env.RELEASE_DIR }}/${{ env.ARCHIVE_PREFIX }}-${{ env.WINDOWS_ARM_TARGET }}.zip 33 | 34 | mv ./target/${{ env.BUILD_PROFILE }}/${{ env.RELEASE_BIN }}.exe ./${{ env.RELEASE_BIN }}.exe 35 | 7z a ./${X86_64_ARCHIVE_FILE} ./${{ env.RELEASE_BIN }}.exe 36 | 37 | mv ./target/${{ env.WINDOWS_ARM_TARGET }}/${{ env.BUILD_PROFILE }}/${{ env.RELEASE_BIN }}.exe ./${{ env.RELEASE_BIN }}.exe 38 | 7z a ./${AARCH64_ARCHIVE_FILE} ./${{ env.RELEASE_BIN }}.exe 39 | 40 | - name: Upload artifacts 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: Windows artifacts 44 | path: ./${{ env.RELEASE_DIR }}/* 45 | 46 | create-release: 47 | needs: [windows-binary] 48 | runs-on: ubuntu-latest 49 | permissions: 50 | contents: write 51 | id-token: write 52 | attestations: write 53 | 54 | steps: 55 | - name: Download artifacts 56 | uses: actions/download-artifact@v4 57 | 58 | - name: Display structure of downloaded files 59 | run: ls -R 60 | 61 | - name: Attest 62 | uses: actions/attest-build-provenance@v3 63 | with: 64 | subject-path: | 65 | Windows artifacts/* 66 | 67 | - name: Create a draft release 68 | uses: softprops/action-gh-release@v2 69 | with: 70 | tag_name: ${{ env.RELEASE_PKG_VERSION }} 71 | draft: true 72 | files: | 73 | Windows artifacts/* 74 | -------------------------------------------------------------------------------- /src/app/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use eframe::{ 4 | egui, 5 | epaint::{Color32, ColorImage}, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | use tokio::sync::mpsc::{Receiver, Sender}; 9 | 10 | use crate::{ 11 | calibration::ReadingResult, 12 | external::ExternalJobCmd, 13 | generators::{GeneratorState, GeneratorType}, 14 | pgen::controller::{PGenControllerCmd, PGenControllerState}, 15 | }; 16 | 17 | mod calibration; 18 | pub mod eframe_app; 19 | mod external_generator_ui; 20 | mod internal_generator_ui; 21 | pub mod pgen_app; 22 | pub mod read_file_ops; 23 | pub mod utils; 24 | 25 | pub use pgen_app::PGenApp; 26 | 27 | pub use calibration::{CalibrationState, compute_cie_chromaticity_diagram_worker}; 28 | 29 | #[derive(Debug)] 30 | pub struct PGenAppContext { 31 | pub rx: Receiver, 32 | 33 | pub controller_tx: Sender, 34 | pub external_tx: Sender, 35 | } 36 | 37 | pub enum PGenAppUpdate { 38 | GeneratorListening(bool), 39 | InitialSetup { 40 | egui_ctx: eframe::egui::Context, 41 | saved_state: Box>, 42 | }, 43 | NewState(PGenControllerState), 44 | Processing, 45 | DoneProcessing, 46 | SpotreadStarted(bool), 47 | SpotreadRes(Option), 48 | CieDiagramReady(ColorImage), 49 | ReadFileResponse(ReadFileType, PathBuf), 50 | } 51 | 52 | #[derive(Debug, Clone, Copy)] 53 | pub enum ReadFileType { 54 | PatchList, 55 | } 56 | 57 | #[derive(Deserialize, Serialize)] 58 | pub struct PGenAppSavedState { 59 | pub state: PGenControllerState, 60 | pub editing_socket: (String, String), 61 | pub generator_type: GeneratorType, 62 | pub generator_state: GeneratorState, 63 | pub cal_state: CalibrationState, 64 | } 65 | 66 | fn status_color_active(ctx: &egui::Context, active: bool) -> Color32 { 67 | let dark_mode = ctx.style().visuals.dark_mode; 68 | if active { 69 | if dark_mode { 70 | Color32::DARK_GREEN 71 | } else { 72 | Color32::LIGHT_GREEN 73 | } 74 | } else if dark_mode { 75 | Color32::DARK_RED 76 | } else { 77 | Color32::LIGHT_RED 78 | } 79 | } 80 | 81 | impl ReadFileType { 82 | pub fn title(&self) -> &'static str { 83 | match self { 84 | Self::PatchList => "Patch list file", 85 | } 86 | } 87 | 88 | pub fn filters(&self) -> &'static [(&'static str, &'static [&'static str])] { 89 | match self { 90 | Self::PatchList => &[("CSV", &["csv"]), ("Text", &["txt"])], 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/calibration/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use deltae::LabValue; 4 | use kolor_64::{ColorSpace, Vec3}; 5 | use serde::{Deserialize, Serialize}; 6 | use strum::{AsRefStr, Display, EnumIter}; 7 | 8 | mod cct; 9 | mod luminance_eotf; 10 | mod reading_result; 11 | 12 | pub use cct::xyz_to_cct; 13 | pub use luminance_eotf::LuminanceEotf; 14 | pub use reading_result::ReadingResult; 15 | 16 | #[derive(Debug, Clone, Copy)] 17 | pub struct CalibrationTarget { 18 | pub min_y: f64, 19 | pub max_y: f64, 20 | pub colorspace: TargetColorspace, 21 | pub eotf: LuminanceEotf, 22 | pub max_hdr_mdl: f64, 23 | 24 | // Linear 25 | pub ref_rgb: Vec3, 26 | } 27 | 28 | #[derive( 29 | Display, AsRefStr, Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, EnumIter, 30 | )] 31 | pub enum TargetColorspace { 32 | #[default] 33 | #[strum(to_string = "Rec. 709")] 34 | Rec709, 35 | #[strum(to_string = "Display P3")] 36 | DisplayP3, 37 | #[strum(to_string = "Rec. 2020")] 38 | Rec2020, 39 | } 40 | 41 | #[derive(Debug, Clone, Copy, Deserialize, Serialize)] 42 | pub struct PatternInsertionConfig { 43 | pub enabled: bool, 44 | pub duration: Duration, 45 | pub level: f64, 46 | } 47 | 48 | pub const RGB_PRIMARIES: [[f64; 3]; 3] = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; 49 | pub const RGB_SECONDARIES: [[f64; 3]; 6] = [ 50 | [1.0, 0.0, 0.0], 51 | [0.0, 1.0, 0.0], 52 | [0.0, 0.0, 1.0], 53 | [1.0, 1.0, 0.0], 54 | [1.0, 0.0, 1.0], 55 | [0.0, 1.0, 1.0], 56 | ]; 57 | 58 | impl TargetColorspace { 59 | pub const fn to_kolor(&self) -> ColorSpace { 60 | match self { 61 | Self::Rec709 => kolor_64::spaces::BT_709, 62 | Self::DisplayP3 => kolor_64::spaces::DISPLAY_P3, 63 | Self::Rec2020 => kolor_64::spaces::BT_2020, 64 | } 65 | } 66 | } 67 | 68 | pub struct MyLab(pub Vec3); 69 | impl From for LabValue { 70 | fn from(lab: MyLab) -> Self { 71 | let (l, a, b) = lab.0.to_array().map(|e| e as f32).into(); 72 | LabValue { l, a, b } 73 | } 74 | } 75 | 76 | impl Default for CalibrationTarget { 77 | fn default() -> Self { 78 | Self { 79 | min_y: Default::default(), 80 | max_y: 100.0, 81 | max_hdr_mdl: 1000.0, 82 | colorspace: Default::default(), 83 | eotf: Default::default(), 84 | ref_rgb: Default::default(), 85 | } 86 | } 87 | } 88 | 89 | impl Default for PatternInsertionConfig { 90 | fn default() -> Self { 91 | Self { 92 | enabled: Default::default(), 93 | duration: Duration::from_secs(5), 94 | level: 0.15, 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use anyhow::{Result, bail}; 4 | use clap::Parser; 5 | use clap_verbosity_flag::{InfoLevel, Verbosity}; 6 | use eframe::egui::{self, TextStyle}; 7 | use tokio::sync::Mutex; 8 | 9 | use app::{PGenApp, PGenAppSavedState, PGenAppUpdate}; 10 | use pgen::controller::handler::PGenController; 11 | 12 | pub mod app; 13 | pub mod calibration; 14 | pub mod external; 15 | pub mod generators; 16 | pub mod pgen; 17 | pub mod spotread; 18 | pub mod utils; 19 | 20 | #[derive(Parser, Debug)] 21 | #[command(name = env!("CARGO_PKG_NAME"), about = "RPi PGenerator client", author = "quietvoid", version = env!("CARGO_PKG_VERSION"))] 22 | struct Opt { 23 | #[command(flatten)] 24 | verbose: Verbosity, 25 | } 26 | 27 | #[tokio::main(flavor = "multi_thread", worker_threads = 1)] 28 | async fn main() -> Result<()> { 29 | let opt = Opt::parse(); 30 | 31 | pretty_env_logger::formatted_timed_builder() 32 | .filter_module("pgen_client", opt.verbose.log_level_filter()) 33 | .init(); 34 | 35 | let (app_tx, app_rx) = tokio::sync::mpsc::channel(5); 36 | let (controller_tx, controller_rx) = tokio::sync::mpsc::channel(5); 37 | let controller = Arc::new(Mutex::new(PGenController::new(Some(app_tx.clone())))); 38 | 39 | let external_tx = external::start_external_jobs_worker( 40 | app_tx.clone(), 41 | controller_tx.clone(), 42 | controller.clone(), 43 | ); 44 | 45 | let app = PGenApp::new(app_rx, controller_tx, external_tx); 46 | 47 | pgen::controller::daemon::start_pgen_controller_worker(controller, controller_rx); 48 | app::compute_cie_chromaticity_diagram_worker(app_tx.clone()); 49 | 50 | let res = eframe::run_native( 51 | "pgen_client", 52 | eframe::NativeOptions::default(), 53 | Box::new(move |cc| { 54 | // Set the global theme, default to dark mode 55 | let mut global_visuals = egui::style::Visuals::dark(); 56 | global_visuals.window_shadow = egui::Shadow { 57 | offset: [6, 10], 58 | blur: 8, 59 | spread: 0, 60 | color: egui::Color32::from_black_alpha(25), 61 | }; 62 | cc.egui_ctx.set_visuals(global_visuals); 63 | 64 | let mut style = (*cc.egui_ctx.style()).clone(); 65 | style.text_styles.get_mut(&TextStyle::Body).unwrap().size = 16.0; 66 | style.text_styles.get_mut(&TextStyle::Button).unwrap().size = 16.0; 67 | cc.egui_ctx.set_style(style); 68 | 69 | let saved_state = cc.storage.and_then(|storage| { 70 | eframe::get_value::(storage, eframe::APP_KEY) 71 | }); 72 | 73 | app_tx 74 | .try_send(PGenAppUpdate::InitialSetup { 75 | egui_ctx: cc.egui_ctx.clone(), 76 | saved_state: Box::new(saved_state), 77 | }) 78 | .ok(); 79 | 80 | Ok(Box::new(app)) 81 | }), 82 | ); 83 | 84 | if let Err(e) = res { 85 | bail!("Failed starting egui window: {}", e); 86 | } 87 | 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /src/app/calibration/luminance_plot.rs: -------------------------------------------------------------------------------- 1 | use eframe::{ 2 | egui::{Layout, Ui}, 3 | emath::Align, 4 | epaint::Color32, 5 | }; 6 | use egui_plot::{Line, MarkerShape, Plot, Points}; 7 | 8 | use super::{CalibrationState, LuminanceEotf, ReadingResult}; 9 | 10 | pub fn draw_luminance_plot( 11 | ui: &mut Ui, 12 | results: &[ReadingResult], 13 | cal_state: &mut CalibrationState, 14 | ) { 15 | ui.horizontal(|ui| { 16 | ui.heading("Luminance"); 17 | ui.checkbox(&mut cal_state.show_luminance_plot, "Show"); 18 | 19 | if cal_state.show_luminance_plot { 20 | ui.with_layout(Layout::right_to_left(Align::Center), |ui| { 21 | ui.checkbox(&mut cal_state.oetf, "OETF"); 22 | }); 23 | } 24 | }); 25 | 26 | if cal_state.show_luminance_plot { 27 | let min = cal_state.min_normalized(); 28 | draw_plot(ui, results, min, cal_state); 29 | } 30 | } 31 | 32 | fn draw_plot(ui: &mut Ui, results: &[ReadingResult], min: f64, cal_state: &CalibrationState) { 33 | let target_eotf = cal_state.eotf; 34 | let oetf = cal_state.oetf; 35 | 36 | let dark_mode = ui.ctx().style().visuals.dark_mode; 37 | let ref_color = if dark_mode { 38 | Color32::GRAY 39 | } else { 40 | Color32::DARK_GRAY 41 | }; 42 | let lum_color = if dark_mode { 43 | Color32::YELLOW 44 | } else { 45 | Color32::from_rgb(255, 153, 0) 46 | }; 47 | 48 | let nits_scale = (target_eotf == LuminanceEotf::PQ).then(|| 10_000.0 / cal_state.max_hdr_mdl); 49 | let precision: u32 = 8; 50 | let max = 2_u32.pow(precision); 51 | let max_f = max as f64; 52 | let ref_points: Vec<[f64; 2]> = (0..max) 53 | .map(|i| { 54 | let fraction = i as f64 / max_f; 55 | let (x, y) = if let Some(nits_scale) = nits_scale { 56 | let mut y = target_eotf.value(fraction, oetf); 57 | if !oetf { 58 | y *= nits_scale; 59 | } 60 | 61 | (fraction, y.min(1.0)) 62 | } else { 63 | (fraction, target_eotf.value_bpc(min, fraction, oetf, false)) 64 | }; 65 | 66 | [x, y] 67 | }) 68 | .collect(); 69 | 70 | let ref_line = Line::new("Ref line", ref_points) 71 | .color(ref_color) 72 | .highlight(true) 73 | .style(egui_plot::LineStyle::Dashed { length: 10.0 }); 74 | 75 | let lum_points: Vec<[f64; 2]> = results 76 | .iter() 77 | .filter(|res| res.is_white_stimulus_reading()) 78 | .map(|res| { 79 | let x = res.target.ref_rgb[0]; 80 | let y = res.luminance(oetf); 81 | 82 | [x, y] 83 | }) 84 | .collect(); 85 | 86 | let lum_line = Line::new("Lum line", lum_points.clone()) 87 | .color(lum_color) 88 | .highlight(true); 89 | let lum_markers = Points::new("Lum points", lum_points) 90 | .shape(MarkerShape::Circle) 91 | .radius(2.5) 92 | .color(lum_color) 93 | .highlight(true); 94 | 95 | Plot::new("luminance_plot") 96 | .view_aspect(2.0) 97 | .allow_scroll(false) 98 | .clamp_grid(true) 99 | .show_background(false) 100 | .show(ui, |plot_ui| { 101 | plot_ui.line(ref_line); 102 | 103 | plot_ui.line(lum_line); 104 | plot_ui.points(lum_markers); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /src/pgen/controller/daemon.rs: -------------------------------------------------------------------------------- 1 | use futures::{FutureExt, stream::StreamExt}; 2 | use tokio::{sync::mpsc::Receiver, time::interval}; 3 | use tokio_stream::wrappers::ReceiverStream; 4 | 5 | use crate::app::PGenAppUpdate; 6 | 7 | use super::{PGenControllerCmd, PGenControllerHandle}; 8 | 9 | pub fn start_pgen_controller_worker( 10 | controller: PGenControllerHandle, 11 | controller_rx: Receiver, 12 | ) { 13 | tokio::spawn(async move { 14 | init_command_loop(controller, controller_rx).await; 15 | }); 16 | } 17 | 18 | async fn init_command_loop( 19 | controller_handle: PGenControllerHandle, 20 | controller_rx: Receiver, 21 | ) { 22 | let reconnect_period = std::time::Duration::from_secs(30 * 60); 23 | let mut reconnect_stream = interval(reconnect_period); 24 | 25 | let heartbeat_period = std::time::Duration::from_secs(30); 26 | let mut heartbeat_stream = interval(heartbeat_period); 27 | 28 | let mut rx = ReceiverStream::new(controller_rx).fuse(); 29 | 30 | loop { 31 | futures::select! { 32 | cmd = rx.select_next_some() => { 33 | let mut controller = controller_handle.lock().await; 34 | controller.ctx.app_tx.as_ref().and_then(|app_tx| app_tx.try_send(PGenAppUpdate::Processing).ok()); 35 | 36 | match cmd { 37 | PGenControllerCmd::SetGuiCallback(egui_ctx) => { 38 | controller.ctx.egui_ctx.replace(egui_ctx); 39 | } 40 | PGenControllerCmd::SetInitialState(state) => controller.set_initial_state(state).await, 41 | PGenControllerCmd::UpdateState(state) => controller.state = state, 42 | PGenControllerCmd::InitialConnect => controller.initial_connect().await, 43 | PGenControllerCmd::UpdateSocket(socket_addr) => controller.update_socket(socket_addr).await, 44 | PGenControllerCmd::Disconnect => controller.disconnect().await, 45 | PGenControllerCmd::TestPattern(config) => controller.send_pattern_from_cfg(config).await, 46 | PGenControllerCmd::SendCurrentPattern => controller.send_current_pattern().await, 47 | PGenControllerCmd::SetBlank => controller.set_blank().await, 48 | PGenControllerCmd::PGen(cmd) => { 49 | controller.pgen_command(cmd).await; 50 | }, 51 | PGenControllerCmd::RestartSoftware => controller.restart_pgenerator_software(true).await, 52 | PGenControllerCmd::ChangeDisplayMode(mode) => controller.change_display_mode(mode, true).await, 53 | PGenControllerCmd::UpdateDynamicRange(dynamic_range) => controller.update_dynamic_range(dynamic_range).await, 54 | PGenControllerCmd::MultipleSetConfCommands(commands) => { 55 | // Restart must be done manually to apply changes 56 | controller.send_multiple_set_conf_commands(commands).await 57 | }, 58 | } 59 | 60 | controller.ctx.app_tx.as_ref().and_then(|app_tx| app_tx.try_send(PGenAppUpdate::DoneProcessing).ok()); 61 | controller.update_ui(); 62 | } 63 | _ = heartbeat_stream.tick().fuse() => { 64 | let mut controller = controller_handle.lock().await; 65 | controller.send_heartbeat().await; 66 | } 67 | _ = reconnect_stream.tick().fuse() => { 68 | let mut controller = controller_handle.lock().await; 69 | controller.reconnect().await; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/pgen/pattern_config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use strum::{AsRefStr, Display, EnumIter}; 3 | 4 | use crate::utils::Rgb; 5 | 6 | use super::BitDepth; 7 | 8 | #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] 9 | pub struct PGenPatternConfig { 10 | pub limited_range: bool, 11 | pub bit_depth: BitDepth, 12 | pub patch_colour: Rgb, 13 | pub background_colour: Rgb, 14 | 15 | pub position: (u16, u16), 16 | pub preset_position: TestPatternPosition, 17 | pub patch_size: (u16, u16), 18 | pub preset_size: TestPatternSize, 19 | } 20 | 21 | #[derive( 22 | Display, AsRefStr, Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, EnumIter, 23 | )] 24 | pub enum TestPatternPosition { 25 | #[default] 26 | Center, 27 | #[strum(to_string = "Top left")] 28 | TopLeft, 29 | #[strum(to_string = "Top right")] 30 | TopRight, 31 | #[strum(to_string = "Bottom left")] 32 | BottomLeft, 33 | #[strum(to_string = "Bottom right")] 34 | BottomRight, 35 | } 36 | 37 | #[derive( 38 | Display, AsRefStr, Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, EnumIter, 39 | )] 40 | pub enum TestPatternSize { 41 | #[strum(to_string = "1% window")] 42 | Percent1, 43 | #[strum(to_string = "2% window")] 44 | Percent2, 45 | #[strum(to_string = "5% window")] 46 | Percent5, 47 | #[default] 48 | #[strum(to_string = "10% window")] 49 | Percent10, 50 | #[strum(to_string = "25% window")] 51 | Percent25, 52 | #[strum(to_string = "50% window")] 53 | Percent50, 54 | #[strum(to_string = "75% window")] 55 | Percent75, 56 | #[strum(to_string = "100% window")] 57 | Percent100, 58 | } 59 | 60 | impl TestPatternPosition { 61 | pub fn compute_position(&self, width: u16, height: u16, patch_size: (u16, u16)) -> (u16, u16) { 62 | match self { 63 | Self::Center => { 64 | let (w2, h2) = (width / 2, height / 2); 65 | let (pw2, ph2) = (patch_size.0 / 2, patch_size.1 / 2); 66 | 67 | (w2 - pw2, h2 - ph2) 68 | } 69 | Self::TopLeft => (0, 0), 70 | Self::TopRight => (width - patch_size.0, 0), 71 | Self::BottomLeft => (0, height - patch_size.1), 72 | Self::BottomRight => (width - patch_size.0, height - patch_size.1), 73 | } 74 | } 75 | } 76 | 77 | impl TestPatternSize { 78 | pub const fn float(&self) -> f64 { 79 | match self { 80 | Self::Percent1 => 0.01, 81 | Self::Percent2 => 0.02, 82 | Self::Percent5 => 0.05, 83 | Self::Percent10 => 0.1, 84 | Self::Percent25 => 0.25, 85 | Self::Percent50 => 0.5, 86 | Self::Percent75 => 0.75, 87 | Self::Percent100 => 1.0, 88 | } 89 | } 90 | 91 | pub fn patch_size_from_display_resolution(&self, width: u16, height: u16) -> (u16, u16) { 92 | let (width, height) = (width as f64, height as f64); 93 | let area = width * height; 94 | 95 | let patch_area = self.float() * area; 96 | let scale = (patch_area / area).sqrt(); 97 | 98 | ( 99 | (scale * width).round() as u16, 100 | (scale * height).round() as u16, 101 | ) 102 | } 103 | } 104 | 105 | impl Default for PGenPatternConfig { 106 | fn default() -> Self { 107 | Self { 108 | limited_range: false, 109 | position: (0, 0), 110 | patch_size: (0, 0), 111 | bit_depth: BitDepth::Ten, 112 | patch_colour: [128, 128, 128], 113 | background_colour: [0, 0, 0], 114 | preset_position: Default::default(), 115 | preset_size: Default::default(), 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/app/calibration/rgb_balance_plot.rs: -------------------------------------------------------------------------------- 1 | use eframe::{egui::Ui, epaint::Color32}; 2 | use egui_plot::{Line, MarkerShape, Plot, Points}; 3 | use kolor_64::Vec3; 4 | 5 | use crate::calibration::ReadingResult; 6 | 7 | use super::CalibrationState; 8 | 9 | const RED_MARKER_COLOR: Color32 = Color32::from_rgb(255, 26, 26); 10 | const GREEN_LINE_COLOR: Color32 = Color32::from_rgb(0, 230, 0); 11 | const GREEN_MARKER_COLOR: Color32 = Color32::from_rgb(0, 204, 0); 12 | const BLUE_MARKER_COLOR: Color32 = Color32::from_rgb(51, 51, 255); 13 | 14 | pub fn draw_rgb_balance_plot( 15 | ui: &mut Ui, 16 | cal_state: &mut CalibrationState, 17 | results: &[ReadingResult], 18 | ) { 19 | ui.horizontal(|ui| { 20 | ui.heading("RGB Balance"); 21 | ui.checkbox(&mut cal_state.show_rgb_balance_plot, "Show"); 22 | }); 23 | 24 | if cal_state.show_rgb_balance_plot { 25 | draw_plot(ui, results); 26 | } 27 | } 28 | 29 | fn draw_plot(ui: &mut Ui, results: &[ReadingResult]) { 30 | let dark_mode = ui.ctx().style().visuals.dark_mode; 31 | 32 | let ref_points: Vec<[f64; 2]> = (0..255).map(|i| [i as f64 / 255.0, 0.0]).collect(); 33 | let ref_color = if dark_mode { 34 | Color32::WHITE 35 | } else { 36 | Color32::BLACK 37 | }; 38 | let ref_line = Line::new("Reference", ref_points).color(ref_color); 39 | let rgb_points: Vec<(f64, Vec3)> = results 40 | .iter() 41 | .filter(|res| res.is_white_stimulus_reading() && res.target.ref_rgb.x > 0.01) 42 | .map(|res| { 43 | let ref_cmp = res.target.ref_rgb[0]; 44 | let x = (ref_cmp * 1e3).round() / 1e3; 45 | 46 | // Both RGB and min_y are already encoded in display gamma 47 | let y = res.gamma_normalized_rgb(); 48 | 49 | (x, y - 1.0) 50 | }) 51 | .collect(); 52 | 53 | let red_points: Vec<[f64; 2]> = rgb_points.iter().map(|(x, rgb)| [*x, rgb[0]]).collect(); 54 | let red_line = Line::new("Red", red_points.clone()) 55 | .color(Color32::RED) 56 | .highlight(true); 57 | let red_markers = Points::new("Red", red_points) 58 | .shape(MarkerShape::Circle) 59 | .radius(2.0) 60 | .color(RED_MARKER_COLOR) 61 | .highlight(true); 62 | 63 | let green_points: Vec<[f64; 2]> = rgb_points.iter().map(|(x, rgb)| [*x, rgb[1]]).collect(); 64 | let green_line = Line::new("Green", green_points.clone()) 65 | .color(GREEN_LINE_COLOR) 66 | .highlight(true); 67 | let green_markers = Points::new("Green", green_points) 68 | .shape(MarkerShape::Circle) 69 | .radius(2.0) 70 | .color(GREEN_MARKER_COLOR) 71 | .highlight(true); 72 | 73 | let (blue_color, blue_marker_color) = if dark_mode { 74 | ( 75 | Color32::from_rgb(77, 77, 255), 76 | Color32::from_rgb(102, 102, 255), 77 | ) 78 | } else { 79 | (Color32::BLUE, BLUE_MARKER_COLOR) 80 | }; 81 | let blue_points: Vec<[f64; 2]> = rgb_points.iter().map(|(x, rgb)| [*x, rgb[2]]).collect(); 82 | let blue_line = Line::new("Blue", blue_points.clone()) 83 | .color(blue_color) 84 | .highlight(true); 85 | let blue_markers = Points::new("Blue", blue_points) 86 | .shape(MarkerShape::Circle) 87 | .radius(2.0) 88 | .color(blue_marker_color) 89 | .highlight(true); 90 | 91 | Plot::new("rgb_balance_plot") 92 | .view_aspect(2.0) 93 | .allow_scroll(false) 94 | .clamp_grid(true) 95 | .show_background(false) 96 | .y_grid_spacer(egui_plot::uniform_grid_spacer(|_| [0.025, 0.10, 0.5])) 97 | .show(ui, |plot_ui| { 98 | plot_ui.line(ref_line); 99 | 100 | plot_ui.line(red_line); 101 | plot_ui.points(red_markers); 102 | plot_ui.line(green_line); 103 | plot_ui.points(green_markers); 104 | plot_ui.line(blue_line); 105 | plot_ui.points(blue_markers); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /src/generators/resolve.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::time::Duration; 3 | 4 | use tokio::sync::mpsc::Sender; 5 | use tokio::{net::TcpStream, time::timeout}; 6 | use yaserde::YaDeserialize; 7 | 8 | use crate::pgen::BitDepth; 9 | use crate::pgen::{controller::PGenControllerCmd, pattern_config::PGenPatternConfig}; 10 | use crate::utils::Rgb; 11 | 12 | const RESOLVE_INTERFACE_ADDRESS: &str = "127.0.0.1:20002"; 13 | 14 | pub async fn resolve_connect_tcp_stream() -> io::Result { 15 | timeout(Duration::from_secs(5), async { 16 | TcpStream::connect(RESOLVE_INTERFACE_ADDRESS).await 17 | }) 18 | .await? 19 | } 20 | 21 | pub async fn handle_resolve_tcp_stream_message(tcp_stream: &mut TcpStream) -> io::Result { 22 | let mut header = [0; 4]; 23 | let n = tcp_stream.try_read(&mut header)?; 24 | if n != 4 { 25 | return Err(io::Error::other("Resolve: invalid header")); 26 | } 27 | 28 | let msg_len = u32::from_be_bytes(header) as usize; 29 | let mut msg = vec![0_u8; msg_len]; 30 | let n = tcp_stream.try_read(msg.as_mut())?; 31 | if n != msg_len { 32 | return Err(io::Error::other("Resolve: Failed reading packet")); 33 | } 34 | 35 | // Shouldn't fail 36 | let msg = String::from_utf8(msg).unwrap(); 37 | 38 | Ok(msg) 39 | } 40 | 41 | pub async fn handle_resolve_pattern_message(controller_tx: &Sender, msg: &str) { 42 | let pattern = yaserde::de::from_str::(msg); 43 | 44 | match pattern { 45 | Ok(pattern) => { 46 | log::debug!("Resolve pattern received: {pattern:?}"); 47 | let config = pattern.to_pgen(); 48 | let cmd = PGenControllerCmd::TestPattern(config); 49 | controller_tx.try_send(cmd).ok(); 50 | } 51 | Err(e) => log::error!("{e}"), 52 | } 53 | } 54 | 55 | #[derive(Debug, Default, YaDeserialize)] 56 | pub struct ResolvePattern { 57 | color: ResolveColor, 58 | background: ResolveColor, 59 | #[allow(dead_code)] 60 | geometry: ResolveGeometry, 61 | } 62 | 63 | #[derive(Debug, Default, YaDeserialize)] 64 | struct ResolveColor { 65 | #[yaserde(attribute = true)] 66 | red: u16, 67 | #[yaserde(attribute = true)] 68 | green: u16, 69 | #[yaserde(attribute = true)] 70 | blue: u16, 71 | #[yaserde(attribute = true)] 72 | bits: u8, 73 | } 74 | 75 | #[derive(Debug, Default, YaDeserialize)] 76 | #[allow(dead_code)] 77 | struct ResolveGeometry { 78 | #[yaserde(attribute = true)] 79 | x: f32, 80 | #[yaserde(attribute = true)] 81 | y: f32, 82 | #[yaserde(attribute = true, rename = "cx")] 83 | w: f32, 84 | #[yaserde(attribute = true, rename = "cy")] 85 | h: f32, 86 | } 87 | 88 | impl ResolvePattern { 89 | pub fn to_pgen(&self) -> PGenPatternConfig { 90 | PGenPatternConfig { 91 | bit_depth: BitDepth::from_repr(self.color.bits as usize).unwrap(), 92 | patch_colour: self.color.to_array(), 93 | background_colour: self.background.to_array(), 94 | ..Default::default() 95 | } 96 | } 97 | } 98 | 99 | impl ResolveColor { 100 | pub fn to_array(&self) -> Rgb { 101 | [self.red, self.green, self.blue] 102 | } 103 | } 104 | 105 | impl ResolveGeometry { 106 | #[allow(dead_code)] 107 | pub fn to_array(&self) -> [f32; 4] { 108 | [self.x, self.y, self.w, self.h] 109 | } 110 | } 111 | 112 | #[cfg(test)] 113 | mod tests { 114 | use super::ResolvePattern; 115 | 116 | #[test] 117 | fn parse_xml_string() { 118 | let msg = r#" 119 | 120 | 121 | 122 | "#; 123 | 124 | let pattern: ResolvePattern = yaserde::de::from_str(msg).unwrap(); 125 | assert_eq!(pattern.color.to_array(), [235, 235, 235]); 126 | assert_eq!(pattern.color.bits, 10); 127 | assert_eq!(pattern.background.to_array(), [16, 16, 16]); 128 | assert_eq!(pattern.background.bits, 10); 129 | assert_eq!(pattern.geometry.to_array(), [0.0, 0.0, 1920.0, 1080.0]); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/pgen/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use strum::{AsRefStr, Display, EnumIter, FromRepr}; 3 | 4 | pub mod client; 5 | pub mod commands; 6 | pub mod controller; 7 | pub mod pattern_config; 8 | 9 | #[derive( 10 | Display, 11 | AsRefStr, 12 | Debug, 13 | Default, 14 | Clone, 15 | Copy, 16 | Deserialize, 17 | Serialize, 18 | PartialEq, 19 | Eq, 20 | EnumIter, 21 | FromRepr, 22 | )] 23 | pub enum BitDepth { 24 | #[strum(to_string = "8-bit")] 25 | Eight = 8, 26 | #[default] 27 | #[strum(to_string = "10-bit")] 28 | Ten = 10, 29 | } 30 | 31 | #[derive( 32 | Display, 33 | AsRefStr, 34 | Debug, 35 | Default, 36 | Clone, 37 | Copy, 38 | Deserialize, 39 | Serialize, 40 | PartialEq, 41 | Eq, 42 | EnumIter, 43 | FromRepr, 44 | )] 45 | pub enum DynamicRange { 46 | #[default] 47 | #[strum(to_string = "SDR")] 48 | Sdr, 49 | #[strum(to_string = "HDR")] 50 | Hdr, 51 | #[strum(to_string = "DoVi")] 52 | Dovi, 53 | } 54 | 55 | #[derive( 56 | Display, 57 | AsRefStr, 58 | Debug, 59 | Default, 60 | Clone, 61 | Copy, 62 | Deserialize, 63 | Serialize, 64 | PartialEq, 65 | Eq, 66 | EnumIter, 67 | FromRepr, 68 | )] 69 | pub enum ColorFormat { 70 | #[default] 71 | #[strum(to_string = "RGB")] 72 | Rgb = 0, 73 | YCbCr444, 74 | YCbCr422, 75 | } 76 | 77 | #[derive( 78 | Display, 79 | AsRefStr, 80 | Debug, 81 | Default, 82 | Clone, 83 | Copy, 84 | Deserialize, 85 | Serialize, 86 | PartialEq, 87 | Eq, 88 | EnumIter, 89 | FromRepr, 90 | )] 91 | pub enum QuantRange { 92 | Limited = 1, 93 | #[default] 94 | Full, 95 | } 96 | 97 | #[derive( 98 | Display, 99 | AsRefStr, 100 | Debug, 101 | Default, 102 | Clone, 103 | Copy, 104 | Deserialize, 105 | Serialize, 106 | PartialEq, 107 | Eq, 108 | EnumIter, 109 | FromRepr, 110 | )] 111 | pub enum Colorimetry { 112 | #[default] 113 | Default = 0, 114 | #[strum(to_string = "BT.709 (YCC)")] 115 | Bt709Ycc = 2, 116 | #[strum(to_string = "BT.2020 (RGB)")] 117 | Bt2020Rgb = 9, 118 | } 119 | 120 | #[derive(Debug, Default, Clone, Deserialize, Serialize)] 121 | pub struct HdrMetadata { 122 | pub eotf: HdrEotf, 123 | pub primaries: Primaries, 124 | pub max_mdl: u16, 125 | pub min_mdl: u16, 126 | pub maxcll: u16, 127 | pub maxfall: u16, 128 | } 129 | 130 | #[derive( 131 | Display, 132 | AsRefStr, 133 | Debug, 134 | Default, 135 | Clone, 136 | Copy, 137 | Deserialize, 138 | Serialize, 139 | PartialEq, 140 | Eq, 141 | EnumIter, 142 | FromRepr, 143 | )] 144 | pub enum HdrEotf { 145 | #[strum(to_string = "Gamma (SDR)")] 146 | GammaSdr = 0, 147 | #[strum(to_string = "Gamma (HDR)")] 148 | GammaHdr = 1, 149 | #[default] 150 | #[strum(to_string = "ST.2084 / PQ")] 151 | Pq = 2, 152 | #[strum(to_string = "Hybrid log-gamma / HLG")] 153 | Hlg = 3, 154 | } 155 | 156 | #[derive( 157 | Display, 158 | AsRefStr, 159 | Debug, 160 | Default, 161 | Clone, 162 | Copy, 163 | Deserialize, 164 | Serialize, 165 | PartialEq, 166 | Eq, 167 | EnumIter, 168 | FromRepr, 169 | )] 170 | pub enum Primaries { 171 | #[strum(to_string = "Rec.709")] 172 | Rec709 = 0, 173 | #[strum(to_string = "Rec.2020 / D65")] 174 | Rec2020 = 1, 175 | #[default] 176 | #[strum(to_string = "P3 / D65")] 177 | DisplayP3 = 2, 178 | #[strum(to_string = "DCI-P3 (Theater)")] 179 | DciP3 = 3, 180 | #[strum(to_string = "P3 D60 (ACES Cinema)")] 181 | P3D60 = 4, 182 | } 183 | 184 | #[derive( 185 | Display, 186 | AsRefStr, 187 | Debug, 188 | Default, 189 | Clone, 190 | Copy, 191 | Deserialize, 192 | Serialize, 193 | PartialEq, 194 | Eq, 195 | EnumIter, 196 | FromRepr, 197 | )] 198 | pub enum DoviMapMode { 199 | #[strum(to_string = "Verify / Absolute")] 200 | Absolute = 1, 201 | #[default] 202 | #[strum(to_string = "Calibrate / Relative")] 203 | Relative = 2, 204 | } 205 | 206 | impl From for QuantRange { 207 | fn from(v: bool) -> Self { 208 | if v { Self::Limited } else { Self::Full } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /src/calibration/cct.rs: -------------------------------------------------------------------------------- 1 | use kolor_64::Vec3; 2 | 3 | /* 4 | * Port of XYZtoCorColorTemp.c to Rust 5 | * Author: Bruce Justin Lindbloom 6 | * Copyright (c) 2003 Bruce Justin Lindbloom. All rights reserved. 7 | */ 8 | 9 | /// reciprocal temperature (K) 10 | const RECIPROCAL_TEMP: [f64; 31] = [ 11 | f64::MIN, 12 | 10.0e-6, 13 | 20.0e-6, 14 | 30.0e-6, 15 | 40.0e-6, 16 | 50.0e-6, 17 | 60.0e-6, 18 | 70.0e-6, 19 | 80.0e-6, 20 | 90.0e-6, 21 | 100.0e-6, 22 | 125.0e-6, 23 | 150.0e-6, 24 | 175.0e-6, 25 | 200.0e-6, 26 | 225.0e-6, 27 | 250.0e-6, 28 | 275.0e-6, 29 | 300.0e-6, 30 | 325.0e-6, 31 | 350.0e-6, 32 | 375.0e-6, 33 | 400.0e-6, 34 | 425.0e-6, 35 | 450.0e-6, 36 | 475.0e-6, 37 | 500.0e-6, 38 | 525.0e-6, 39 | 550.0e-6, 40 | 575.0e-6, 41 | 600.0e-6, 42 | ]; 43 | 44 | // UVT LUT 45 | const UVT: [Vec3; 31] = [ 46 | Vec3::new(0.18006, 0.26352, -0.24341), 47 | Vec3::new(0.18066, 0.26589, -0.25479), 48 | Vec3::new(0.18133, 0.26846, -0.26876), 49 | Vec3::new(0.18208, 0.27119, -0.28539), 50 | Vec3::new(0.18293, 0.27407, -0.30470), 51 | Vec3::new(0.18388, 0.27709, -0.32675), 52 | Vec3::new(0.18494, 0.28021, -0.35156), 53 | Vec3::new(0.18611, 0.28342, -0.37915), 54 | Vec3::new(0.18740, 0.28668, -0.40955), 55 | Vec3::new(0.18880, 0.28997, -0.44278), 56 | Vec3::new(0.19032, 0.29326, -0.47888), 57 | Vec3::new(0.19462, 0.30141, -0.58204), 58 | Vec3::new(0.19962, 0.30921, -0.70471), 59 | Vec3::new(0.20525, 0.31647, -0.84901), 60 | Vec3::new(0.21142, 0.32312, -1.0182), 61 | Vec3::new(0.21807, 0.32909, -1.2168), 62 | Vec3::new(0.22511, 0.33439, -1.4512), 63 | Vec3::new(0.23247, 0.33904, -1.7298), 64 | Vec3::new(0.24010, 0.34308, -2.0637), 65 | Vec3::new(0.24792, 0.34655, -2.4681), /* Note: 0.24792 is a corrected value for the error found in W&S as 0.24702 */ 66 | Vec3::new(0.25591, 0.34951, -2.9641), 67 | Vec3::new(0.26400, 0.35200, -3.5814), 68 | Vec3::new(0.27218, 0.35407, -4.3633), 69 | Vec3::new(0.28039, 0.35577, -5.3762), 70 | Vec3::new(0.28863, 0.35714, -6.7262), 71 | Vec3::new(0.29685, 0.35823, -8.5955), 72 | Vec3::new(0.30505, 0.35907, -11.324), 73 | Vec3::new(0.31320, 0.35968, -15.628), 74 | Vec3::new(0.32129, 0.36011, -23.325), 75 | Vec3::new(0.32931, 0.36038, -40.770), 76 | Vec3::new(0.33724, 0.36051, -116.45), 77 | ]; 78 | 79 | #[inline(always)] 80 | fn lerp(v: f64, rhs: f64, t: f64) -> f64 { 81 | v + (rhs - v) * t 82 | } 83 | 84 | pub fn xyz_to_cct(xyz: Vec3) -> Option { 85 | let mut di = 0.0; 86 | 87 | if (xyz[0] < 1.0e-20) && (xyz[1] < 1.0e-20) && (xyz[2] < 1.0e-20) { 88 | /* protect against possible divide-by-zero failure */ 89 | return None; 90 | } 91 | 92 | let us = (4.0 * xyz.x) / (xyz.x + 15.0 * xyz.y + 3.0 * xyz.z); 93 | let vs = (6.0 * xyz.y) / (xyz.x + 15.0 * xyz.y + 3.0 * xyz.z); 94 | let mut dm = 0.0; 95 | 96 | let mut i = 0_usize; 97 | for _ in 0..31 { 98 | let uvt = UVT[i]; 99 | di = (vs - uvt.y) - uvt.z * (us - uvt.x); 100 | 101 | if (i > 0) && (((di < 0.0) && (dm >= 0.0)) || ((di >= 0.0) && (dm < 0.0))) { 102 | /* found lines bounding (us, vs) : i-1 and i */ 103 | break; 104 | } 105 | 106 | dm = di; 107 | i += 1; 108 | } 109 | 110 | if i == 31 { 111 | /* bad XYZ input, color temp would be less than minimum of 1666.7 degrees, or too far towards blue */ 112 | return None; 113 | } 114 | 115 | di /= (1.0 + UVT[i].z * UVT[i].z).sqrt(); 116 | dm /= (1.0 + UVT[i - 1].z * UVT[i - 1].z).sqrt(); 117 | 118 | /* p = interpolation parameter, 0.0 : i-1, 1.0 : i */ 119 | let mut p = dm / (dm - di); 120 | p = 1.0 / (lerp(RECIPROCAL_TEMP[i - 1], RECIPROCAL_TEMP[i], p)); 121 | 122 | Some(p) 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use kolor_64::{ 128 | Vec3, 129 | details::{color::WhitePoint, transform::xyY_to_XYZ}, 130 | }; 131 | 132 | use crate::calibration::cct::xyz_to_cct; 133 | 134 | #[test] 135 | fn xyz_d65_to_cct() { 136 | let xyz = xyY_to_XYZ(Vec3::new(0.3127, 0.329, 1.0), WhitePoint::D65); 137 | let cct = xyz_to_cct(xyz).unwrap(); 138 | assert_eq!(cct, 6503.707184795284); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/calibration/luminance_eotf.rs: -------------------------------------------------------------------------------- 1 | use kolor_64::Vec3; 2 | use serde::{Deserialize, Serialize}; 3 | use strum::{AsRefStr, Display, EnumIter}; 4 | 5 | #[derive( 6 | Display, AsRefStr, Debug, Default, Deserialize, Serialize, Copy, Clone, PartialEq, Eq, EnumIter, 7 | )] 8 | pub enum LuminanceEotf { 9 | #[default] 10 | #[strum(to_string = "Gamma 2.2")] 11 | Gamma22, 12 | #[strum(to_string = "Gamma 2.4")] 13 | Gamma24, 14 | #[strum(to_string = "ST.2084 / PQ")] 15 | PQ, 16 | } 17 | 18 | impl LuminanceEotf { 19 | const GAMMA_2_2: f64 = 2.2; 20 | const GAMMA_2_2_INV: f64 = 1.0 / Self::GAMMA_2_2; 21 | const GAMMA_2_4: f64 = 2.4; 22 | const GAMMA_2_4_INV: f64 = 1.0 / Self::GAMMA_2_4; 23 | 24 | pub fn value(&self, v: f64, oetf: bool) -> f64 { 25 | if oetf { 26 | if *self == Self::PQ { 27 | // PQ 0-1 28 | v 29 | } else { 30 | self.oetf(v) 31 | } 32 | } else { 33 | self.eotf(v) 34 | } 35 | } 36 | 37 | pub fn value_bpc(&self, min: f64, v: f64, oetf: bool, linear_min: bool) -> f64 { 38 | let min = if *self == Self::PQ { 39 | 0.0 40 | } else if linear_min { 41 | min 42 | } else { 43 | // Decode min to linear 44 | self.oetf(min) 45 | }; 46 | 47 | if oetf { 48 | self.oetf_bpc(min, v) 49 | } else { 50 | self.eotf_bpc(min, v) 51 | } 52 | } 53 | 54 | pub fn convert_vec(&self, v: Vec3, oetf: bool) -> Vec3 { 55 | if oetf { 56 | match self { 57 | Self::Gamma22 => v.powf(Self::GAMMA_2_2_INV), 58 | Self::Gamma24 => v.powf(Self::GAMMA_2_4_INV), 59 | Self::PQ => v.to_array().map(Self::linear_to_pq).into(), 60 | } 61 | } else { 62 | match self { 63 | Self::Gamma22 => v.powf(Self::GAMMA_2_2), 64 | Self::Gamma24 => v.powf(Self::GAMMA_2_4), 65 | Self::PQ => v.to_array().map(Self::pq_to_linear).into(), 66 | } 67 | } 68 | } 69 | 70 | pub fn eotf(&self, v: f64) -> f64 { 71 | match self { 72 | Self::Gamma22 => v.powf(Self::GAMMA_2_2), 73 | Self::Gamma24 => v.powf(Self::GAMMA_2_4), 74 | Self::PQ => Self::pq_to_linear(v), 75 | } 76 | } 77 | 78 | fn eotf_bpc(&self, min: f64, v: f64) -> f64 { 79 | let max = 1.0 - min; 80 | let v = ((v - min) / max).max(0.0); 81 | 82 | (self.eotf(v) * max) + min 83 | } 84 | 85 | pub fn oetf(&self, v: f64) -> f64 { 86 | match self { 87 | Self::Gamma22 => v.powf(Self::GAMMA_2_2_INV), 88 | Self::Gamma24 => v.powf(Self::GAMMA_2_4_INV), 89 | Self::PQ => Self::linear_to_pq(v), 90 | } 91 | } 92 | 93 | fn oetf_bpc(&self, min: f64, v: f64) -> f64 { 94 | let max = 1.0 - min; 95 | (self.oetf(v) * max) + min 96 | } 97 | 98 | const ST2084_M1: f64 = 2610.0 / 16384.0; 99 | const ST2084_M2: f64 = (2523.0 / 4096.0) * 128.0; 100 | const ST2084_C1: f64 = 3424.0 / 4096.0; 101 | const ST2084_C2: f64 = (2413.0 / 4096.0) * 32.0; 102 | const ST2084_C3: f64 = (2392.0 / 4096.0) * 32.0; 103 | fn pq_to_linear(x: f64) -> f64 { 104 | if x > 0.0 { 105 | let xpow = x.powf(1.0 / Self::ST2084_M2); 106 | let num = (xpow - Self::ST2084_C1).max(0.0); 107 | let den = (Self::ST2084_C2 - Self::ST2084_C3 * xpow).max(f64::NEG_INFINITY); 108 | 109 | (num / den).powf(1.0 / Self::ST2084_M1) 110 | } else { 111 | 0.0 112 | } 113 | } 114 | 115 | fn linear_to_pq(v: f64) -> f64 { 116 | let num = Self::ST2084_C1 + Self::ST2084_C2 * v.powf(Self::ST2084_M1); 117 | let denom = 1.0 + Self::ST2084_C3 * v.powf(Self::ST2084_M1); 118 | 119 | (num / denom).powf(Self::ST2084_M2) 120 | } 121 | 122 | pub const fn mean(&self) -> f64 { 123 | match self { 124 | Self::Gamma22 => Self::GAMMA_2_2, 125 | Self::Gamma24 => Self::GAMMA_2_4, 126 | Self::PQ => 5.0, 127 | } 128 | } 129 | 130 | pub fn gamma(v_in: f64, v_out: f64) -> f64 { 131 | // Avoid division by zero 132 | let gamma = (v_out.ln() - 1e-7) / (v_in.ln() - 1e-7); 133 | (gamma * 1e3).round() / 1e3 134 | } 135 | 136 | pub fn gamma_around_zero(&self, v_in: f64, v_out: f64) -> f64 { 137 | Self::gamma(v_in, v_out) - self.mean() 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/generators/internal.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use serde::{Deserialize, Serialize}; 3 | use strum::{AsRefStr, Display, EnumIter}; 4 | 5 | use crate::{ 6 | calibration::{PatternInsertionConfig, RGB_PRIMARIES, RGB_SECONDARIES, ReadingResult}, 7 | pgen::pattern_config::PGenPatternConfig, 8 | utils::{Rgb, get_rgb_real_range}, 9 | }; 10 | 11 | #[derive(Default, Debug, Clone, Deserialize, Serialize)] 12 | pub struct InternalGenerator { 13 | pub started: bool, 14 | pub auto_advance: bool, 15 | pub preset: PatchListPreset, 16 | pub pattern_insertion_cfg: PatternInsertionConfig, 17 | 18 | /// Patch list 19 | pub list: Vec, 20 | /// Selected patch from list 21 | pub selected_idx: Option, 22 | pub read_selected_continuously: bool, 23 | } 24 | 25 | #[derive(Default, Debug, Clone, Deserialize, Serialize)] 26 | pub struct InternalPattern { 27 | pub rgb: Rgb, 28 | 29 | #[serde(skip)] 30 | pub result: Option, 31 | } 32 | 33 | #[derive( 34 | Display, AsRefStr, Default, Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, EnumIter, 35 | )] 36 | pub enum PatchListPreset { 37 | #[default] 38 | Basic, 39 | 40 | Primaries, 41 | Secondaries, 42 | 43 | #[strum(to_string = "23 pts greyscale")] 44 | Greyscale, 45 | #[strum(to_string = "Saturation sweep")] 46 | SaturationSweep, 47 | #[strum(to_string = "Min/max brightness")] 48 | MinMax, 49 | } 50 | 51 | impl InternalGenerator { 52 | pub fn load_preset(&mut self, config: &PGenPatternConfig) { 53 | let (min, real_max) = get_rgb_real_range(config.limited_range, config.bit_depth as u8); 54 | let (min, real_max) = (min as f64, real_max as f64); 55 | 56 | self.list.clear(); 57 | 58 | let float_rgb = self.preset.rgb_float_list(); 59 | let scaled_rgb = float_rgb.into_iter().map(|float_rgb| { 60 | let rgb = float_rgb.map(|c| ((c * real_max) + min).round() as u16); 61 | InternalPattern { 62 | rgb, 63 | ..Default::default() 64 | } 65 | }); 66 | self.list.extend(scaled_rgb); 67 | } 68 | 69 | pub fn selected_patch(&self) -> Option<&InternalPattern> { 70 | self.selected_idx.and_then(|i| self.list.get(i)) 71 | } 72 | 73 | pub fn selected_patch_mut(&mut self) -> Option<&mut InternalPattern> { 74 | self.selected_idx.and_then(|i| self.list.get_mut(i)) 75 | } 76 | 77 | pub fn results(&self) -> Vec { 78 | self.list.iter().filter_map(|e| e.result).collect() 79 | } 80 | 81 | pub fn minmax_y(&self) -> Option<(f64, f64)> { 82 | self.results() 83 | .iter() 84 | .map(|res| res.xyy[2]) 85 | .minmax_by(|a, b| a.total_cmp(b)) 86 | .into_option() 87 | } 88 | } 89 | 90 | impl PatchListPreset { 91 | pub fn rgb_float_list(&self) -> Vec<[f64; 3]> { 92 | match self { 93 | Self::Basic => { 94 | let mut list = Vec::with_capacity(5); 95 | list.push([0.0, 0.0, 0.0]); 96 | list.extend(RGB_PRIMARIES); 97 | list.push([1.0, 1.0, 1.0]); 98 | 99 | list 100 | } 101 | Self::Primaries => RGB_PRIMARIES.to_vec(), 102 | Self::Secondaries => RGB_SECONDARIES.to_vec(), 103 | Self::Greyscale => { 104 | let mut list = Vec::with_capacity(23); 105 | list.extend(&[ 106 | [0.0, 0.0, 0.0], 107 | [0.025, 0.025, 0.025], 108 | [0.05, 0.05, 0.05], 109 | [0.075, 0.075, 0.075], 110 | ]); 111 | 112 | let start = 0.1; 113 | let step = 0.5; 114 | let rest = (0..19).map(|i| { 115 | let v = ((i as f64 / 10.0) * step) + start; 116 | let v = (v * 100.0).round() / 100.0; 117 | 118 | [v, v, v] 119 | }); 120 | list.extend(rest); 121 | 122 | list 123 | } 124 | Self::SaturationSweep => { 125 | let mut list = Vec::with_capacity(RGB_SECONDARIES.len() * 4); 126 | 127 | let points = 4; 128 | let step = 1.0 / points as f32; 129 | RGB_SECONDARIES.into_iter().for_each(|cmp| { 130 | let (h, _, v) = ecolor::hsv_from_rgb(cmp.map(|c| c as f32)); 131 | 132 | // In order of less sat to full sat 133 | let sweep = (1..=points).map(|i| { 134 | let new_sat = i as f32 * step; 135 | ecolor::rgb_from_hsv((h, new_sat, v)).map(|e| e as f64) 136 | }); 137 | list.extend(sweep); 138 | }); 139 | 140 | list 141 | } 142 | Self::MinMax => { 143 | vec![[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]] 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/app/calibration/gamma_tracking_plot.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use eframe::{ 4 | egui::{self, Layout, Ui}, 5 | emath::Align, 6 | epaint::Color32, 7 | }; 8 | use egui_plot::{GridMark, Line, MarkerShape, Plot, PlotPoint, Points}; 9 | use strum::IntoEnumIterator; 10 | 11 | use super::{CalibrationState, LuminanceEotf, ReadingResult}; 12 | 13 | pub fn draw_gamma_tracking_plot( 14 | ui: &mut Ui, 15 | results: &[ReadingResult], 16 | cal_state: &mut CalibrationState, 17 | ) { 18 | ui.horizontal(|ui| { 19 | ui.heading("Gamma"); 20 | ui.checkbox(&mut cal_state.show_gamma_plot, "Show"); 21 | 22 | if cal_state.show_gamma_plot { 23 | let old_eotf = cal_state.eotf; 24 | ui.with_layout(Layout::right_to_left(Align::Center), |ui| { 25 | egui::ComboBox::from_id_salt(egui::Id::new("cal_luminance_eotf")) 26 | .selected_text(cal_state.eotf.as_ref()) 27 | .show_ui(ui, |ui| { 28 | ui.set_min_width(115.0); 29 | for eotf in LuminanceEotf::iter() { 30 | ui.selectable_value(&mut cal_state.eotf, eotf, eotf.as_ref()); 31 | } 32 | }); 33 | }); 34 | if old_eotf != cal_state.eotf { 35 | cal_state.update_patterns_target(); 36 | } 37 | } 38 | }); 39 | 40 | if cal_state.show_gamma_plot { 41 | draw_plot(ui, results, cal_state); 42 | } 43 | } 44 | 45 | fn draw_plot(ui: &mut Ui, results: &[ReadingResult], cal_state: &CalibrationState) { 46 | let min = cal_state.min_normalized(); 47 | let target_eotf = cal_state.eotf; 48 | 49 | let dark_mode = ui.ctx().style().visuals.dark_mode; 50 | let ref_pq_color = if dark_mode { 51 | Color32::GRAY 52 | } else { 53 | Color32::DARK_GRAY 54 | }; 55 | let ref_color = if dark_mode { 56 | Color32::from_rgb(0, 255, 255) 57 | } else { 58 | Color32::from_rgb(0, 179, 179) 59 | }; 60 | let lum_color = if dark_mode { 61 | Color32::YELLOW 62 | } else { 63 | Color32::from_rgb(255, 153, 0) 64 | }; 65 | 66 | let is_pq = target_eotf == LuminanceEotf::PQ; 67 | let max_pq = is_pq.then(|| target_eotf.oetf(cal_state.max_hdr_mdl / 10_000.0)); 68 | let ref_pq_line = is_pq.then(|| { 69 | Line::new("Ref PQ line", vec![[0.0, 0.0], [1.0, 1.0]]) 70 | .color(ref_pq_color) 71 | .style(egui_plot::LineStyle::Dashed { length: 10.0 }) 72 | }); 73 | 74 | let precision: u32 = 8; 75 | let max = 2_u32.pow(precision); 76 | let max_f = max as f64; 77 | let ref_points: Vec<[f64; 2]> = (0..max) 78 | .filter_map(|i| { 79 | let x = i as f64 / max_f; 80 | if x > 0.01 { 81 | let y = if let Some(max_pq) = max_pq { 82 | x.min(max_pq) 83 | } else { 84 | let v_out = target_eotf.value_bpc(min, x, false, false); 85 | target_eotf.gamma_around_zero(x, v_out) 86 | }; 87 | 88 | Some([x, y]) 89 | } else { 90 | None 91 | } 92 | }) 93 | .collect(); 94 | 95 | let ref_line = Line::new("Ref line", ref_points) 96 | .color(ref_color) 97 | .highlight(true); 98 | 99 | let lum_points: Vec<[f64; 2]> = results 100 | .iter() 101 | .filter(|res| res.is_white_stimulus_reading() && res.not_zero_or_one_rgb()) 102 | .map(|res| { 103 | let x = res.target.ref_rgb[0]; 104 | let y = if is_pq { 105 | target_eotf.oetf(res.xyz.y / 10_000.0) 106 | } else { 107 | res.gamma_around_zero().unwrap() 108 | }; 109 | 110 | [x, y] 111 | }) 112 | .collect(); 113 | 114 | let lum_line = Line::new("Lum line", lum_points.clone()) 115 | .color(lum_color) 116 | .highlight(true); 117 | let lum_markers = Points::new("Lum points", lum_points) 118 | .shape(MarkerShape::Circle) 119 | .radius(2.5) 120 | .color(lum_color) 121 | .highlight(true); 122 | 123 | let mut plot = Plot::new("gamma_tracking_plot") 124 | .view_aspect(2.0) 125 | .show_background(false) 126 | .allow_scroll(false) 127 | .clamp_grid(true); 128 | 129 | if !is_pq { 130 | let gamma_mean = target_eotf.mean(); 131 | let gamma_fmt = move |mark: GridMark, _range: &RangeInclusive| { 132 | format!("{:.3}", mark.value + gamma_mean) 133 | }; 134 | let gamma_label_fmt = move |_s: &str, point: &PlotPoint| { 135 | format!("x = {:.4}\ny = {:.4}", point.x, point.y + gamma_mean) 136 | }; 137 | 138 | plot = plot 139 | .y_axis_formatter(gamma_fmt) 140 | .label_formatter(gamma_label_fmt) 141 | .y_grid_spacer(egui_plot::uniform_grid_spacer(move |_| { 142 | [0.025, 0.075, gamma_mean.round() * 0.1] 143 | })); 144 | } 145 | 146 | plot.show(ui, |plot_ui| { 147 | if let Some(ref_pq_line) = ref_pq_line { 148 | plot_ui.line(ref_pq_line); 149 | } 150 | 151 | plot_ui.line(ref_line); 152 | 153 | plot_ui.line(lum_line); 154 | plot_ui.points(lum_markers); 155 | }); 156 | } 157 | -------------------------------------------------------------------------------- /src/generators/tcp_generator_client.rs: -------------------------------------------------------------------------------- 1 | use std::{io::ErrorKind, sync::Arc}; 2 | 3 | use futures::StreamExt; 4 | use serde::{Deserialize, Serialize}; 5 | use tokio::sync::mpsc::Sender; 6 | use tokio::{io::AsyncWriteExt, net::TcpStream, sync::RwLock}; 7 | use tokio_stream::wrappers::ReceiverStream; 8 | 9 | use crate::app::PGenAppUpdate; 10 | use crate::external::ExternalJobCmd; 11 | use crate::pgen::controller::PGenControllerCmd; 12 | 13 | use super::resolve::{ 14 | handle_resolve_pattern_message, handle_resolve_tcp_stream_message, resolve_connect_tcp_stream, 15 | }; 16 | use super::{GeneratorClientCmd, GeneratorInterface}; 17 | 18 | #[derive(Debug, Default, Deserialize, Serialize, Clone, Copy)] 19 | pub enum TcpGeneratorInterface { 20 | #[default] 21 | Resolve, 22 | } 23 | 24 | pub struct TcpGeneratorClient { 25 | pub stream: TcpStream, 26 | pub interface: TcpGeneratorInterface, 27 | pub buf: Vec, 28 | running: bool, 29 | 30 | controller_tx: Sender, 31 | external_tx: Sender, 32 | } 33 | pub type GeneratorClientHandle = Arc>; 34 | 35 | pub async fn start_tcp_generator_client( 36 | app_tx: Sender, 37 | controller_tx: Sender, 38 | external_tx: Sender, 39 | interface: TcpGeneratorInterface, 40 | ) -> anyhow::Result> { 41 | // Try initial connection first before spawning loop task 42 | let res = match interface { 43 | TcpGeneratorInterface::Resolve => resolve_connect_tcp_stream().await, 44 | }; 45 | 46 | if let Err(e) = res { 47 | log::error!("{interface:?}: Failed connecting to TCP server: {e}"); 48 | return Err(e.into()); 49 | } 50 | 51 | let (client_tx, client_rx) = tokio::sync::mpsc::channel(5); 52 | let mut client_rx = ReceiverStream::new(client_rx).fuse(); 53 | 54 | tokio::spawn(async move { 55 | let stream = res.unwrap(); 56 | let mut client = TcpGeneratorClient { 57 | stream, 58 | interface, 59 | buf: vec![0; 512], 60 | running: true, 61 | controller_tx, 62 | external_tx, 63 | }; 64 | 65 | loop { 66 | tokio::select! { 67 | Ok(_) = client.stream.readable() => { 68 | if !client.running { 69 | break; 70 | } 71 | if let Some(msg) = client.read_message().await { 72 | client.try_send_pattern(&msg).await; 73 | } 74 | } 75 | 76 | msg = client_rx.select_next_some() => { 77 | log::trace!("{interface:?}: Received client command {msg:?}"); 78 | match msg { 79 | GeneratorClientCmd::Shutdown => { 80 | client.shutdown().await; 81 | app_tx.try_send(PGenAppUpdate::GeneratorListening(false)).ok(); 82 | break; 83 | }, 84 | } 85 | } 86 | } 87 | } 88 | }); 89 | 90 | Ok(client_tx) 91 | } 92 | 93 | impl TcpGeneratorClient { 94 | pub async fn read_message(&mut self) -> Option { 95 | let res = match self.interface { 96 | TcpGeneratorInterface::Resolve => { 97 | handle_resolve_tcp_stream_message(&mut self.stream).await 98 | } 99 | }; 100 | 101 | match res { 102 | Ok(msg) => Some(msg), 103 | Err(e) => { 104 | self.handle_error(e).await; 105 | 106 | None 107 | } 108 | } 109 | } 110 | 111 | pub async fn try_send_pattern(&self, msg: &str) { 112 | match self.interface { 113 | TcpGeneratorInterface::Resolve => { 114 | handle_resolve_pattern_message(&self.controller_tx, msg).await; 115 | } 116 | } 117 | } 118 | 119 | pub async fn shutdown(&mut self) { 120 | self.stream.shutdown().await.ok(); 121 | self.running = false; 122 | } 123 | 124 | async fn reconnect(&mut self) -> bool { 125 | self.shutdown().await; 126 | 127 | match self.interface { 128 | TcpGeneratorInterface::Resolve => { 129 | if let Ok(stream) = resolve_connect_tcp_stream().await { 130 | self.stream = stream; 131 | self.running = true; 132 | return true; 133 | } 134 | } 135 | } 136 | 137 | log::error!("{:?}: Failed reconnecting TCP connection", self.interface); 138 | self.send_generator_stopped(); 139 | 140 | false 141 | } 142 | 143 | async fn handle_error(&mut self, e: std::io::Error) { 144 | match e.kind() { 145 | ErrorKind::UnexpectedEof | ErrorKind::Other => { 146 | self.shutdown().await; 147 | self.send_generator_stopped(); 148 | } 149 | ErrorKind::WouldBlock => {} 150 | _ => { 151 | log::error!("{e:?}"); 152 | self.reconnect().await; 153 | } 154 | } 155 | } 156 | 157 | fn send_generator_stopped(&self) { 158 | let client = GeneratorInterface::Tcp(self.interface).client(); 159 | self.external_tx 160 | .try_send(ExternalJobCmd::StopGeneratorClient(client)) 161 | .ok(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/pgen/controller/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::{IpAddr, Ipv4Addr, SocketAddr}, 3 | sync::Arc, 4 | }; 5 | 6 | use anyhow::{Result, anyhow}; 7 | use itertools::Itertools; 8 | use serde::{Deserialize, Serialize}; 9 | use tokio::sync::{Mutex, mpsc::Sender}; 10 | 11 | use crate::app::PGenAppUpdate; 12 | 13 | pub mod daemon; 14 | pub mod handler; 15 | 16 | pub use handler::PGenControllerHandle; 17 | 18 | use super::{ 19 | BitDepth, ColorFormat, Colorimetry, DoviMapMode, DynamicRange, HdrMetadata, QuantRange, 20 | client::{ConnectState, PGenClient}, 21 | commands::{PGenCommand, PGenSetConfCommand}, 22 | pattern_config::PGenPatternConfig, 23 | }; 24 | 25 | #[derive(Debug)] 26 | pub struct PGenControllerContext { 27 | pub(crate) client: Arc>, 28 | 29 | // For updating the GUI 30 | pub app_tx: Option>, 31 | pub(crate) egui_ctx: Option, 32 | } 33 | 34 | #[derive(Debug, Deserialize, Serialize, Clone)] 35 | pub struct PGenControllerState { 36 | pub socket_addr: SocketAddr, 37 | 38 | #[serde(skip)] 39 | pub connected_state: ConnectState, 40 | 41 | #[serde(skip)] 42 | pub pgen_info: Option, 43 | pub pattern_config: PGenPatternConfig, 44 | } 45 | 46 | #[derive(Debug, Default, Clone, Deserialize)] 47 | pub struct PGenInfo { 48 | pub version: String, 49 | pub pid: String, 50 | 51 | pub current_display_mode: DisplayMode, 52 | pub display_modes: Vec, 53 | pub output_config: PGenOutputConfig, 54 | } 55 | 56 | #[derive(Debug, Default, Clone, Deserialize)] 57 | pub struct PGenOutputConfig { 58 | pub format: ColorFormat, 59 | pub bit_depth: BitDepth, 60 | pub quant_range: QuantRange, 61 | pub colorimetry: Colorimetry, 62 | pub dynamic_range: DynamicRange, 63 | pub hdr_meta: HdrMetadata, 64 | 65 | pub dovi_map_mode: DoviMapMode, 66 | } 67 | 68 | #[derive(Debug, Default, Copy, Clone, Deserialize, Serialize, PartialEq)] 69 | pub struct DisplayMode { 70 | pub id: usize, 71 | pub resolution: (u16, u16), 72 | pub refresh_rate: f32, 73 | } 74 | 75 | #[derive(Debug, Clone)] 76 | pub enum PGenControllerCmd { 77 | SetGuiCallback(eframe::egui::Context), 78 | SetInitialState(PGenControllerState), 79 | UpdateState(PGenControllerState), 80 | UpdateSocket(SocketAddr), 81 | InitialConnect, 82 | Disconnect, 83 | TestPattern(PGenPatternConfig), 84 | SendCurrentPattern, 85 | SetBlank, 86 | PGen(PGenCommand), 87 | RestartSoftware, 88 | ChangeDisplayMode(DisplayMode), 89 | MultipleSetConfCommands(Vec), 90 | UpdateDynamicRange(DynamicRange), 91 | } 92 | 93 | impl PGenControllerState { 94 | pub fn default_socket_addr() -> SocketAddr { 95 | SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 85) 96 | } 97 | 98 | pub fn set_pattern_size_and_pos_from_resolution(&mut self) { 99 | if let Some(pgen_info) = self.pgen_info.as_ref() { 100 | let (width, height) = pgen_info.current_display_mode.resolution; 101 | 102 | let patch_size = self 103 | .pattern_config 104 | .preset_size 105 | .patch_size_from_display_resolution(width, height); 106 | let position = self 107 | .pattern_config 108 | .preset_position 109 | .compute_position(width, height, patch_size); 110 | 111 | self.pattern_config.patch_size = patch_size; 112 | self.pattern_config.position = position; 113 | } 114 | } 115 | 116 | pub fn is_dovi_mode(&self) -> bool { 117 | self.pgen_info 118 | .as_ref() 119 | .is_some_and(|e| e.output_config.dynamic_range == DynamicRange::Dovi) 120 | } 121 | } 122 | 123 | impl DisplayMode { 124 | pub fn try_from_str(line: &str) -> Result { 125 | let mut chars = line.chars(); 126 | 127 | let id = chars 128 | .take_while_ref(|c| *c != '[') 129 | .collect::() 130 | .parse::()?; 131 | 132 | // [ 133 | chars.next(); 134 | 135 | let resolution = chars 136 | .take_while_ref(|c| !c.is_whitespace()) 137 | .collect::() 138 | .split('x') 139 | .filter_map(|dim| dim.trim_end().parse::().ok()) 140 | .next_tuple() 141 | .ok_or_else(|| anyhow!("Failed parsing display resolution"))?; 142 | 143 | // space 144 | chars.next(); 145 | 146 | let refresh_rate = chars 147 | .take_while(|c| *c != 'H') 148 | .collect::() 149 | .trim() 150 | .parse::()?; 151 | 152 | Ok(Self { 153 | id, 154 | resolution, 155 | refresh_rate, 156 | }) 157 | } 158 | } 159 | 160 | impl Default for PGenControllerState { 161 | fn default() -> Self { 162 | Self { 163 | socket_addr: PGenControllerState::default_socket_addr(), 164 | connected_state: Default::default(), 165 | pgen_info: Default::default(), 166 | pattern_config: Default::default(), 167 | } 168 | } 169 | } 170 | 171 | impl std::fmt::Display for DisplayMode { 172 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 173 | f.write_fmt(format_args!( 174 | "{}: {}x{} {}Hz", 175 | self.id, self.resolution.0, self.resolution.1, self.refresh_rate 176 | )) 177 | } 178 | } 179 | 180 | #[cfg(test)] 181 | mod test { 182 | use super::DisplayMode; 183 | 184 | #[test] 185 | fn parse_display_mode_str() { 186 | let line = "13[1920x1080 59.94Hz 148.35MHz phsync,pvsync]"; 187 | 188 | let mode = DisplayMode::try_from_str(line).unwrap(); 189 | assert_eq!(mode.id, 13); 190 | assert_eq!(mode.resolution, (1920, 1080)); 191 | assert_eq!(mode.refresh_rate, 59.94); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/app/calibration/mod.rs: -------------------------------------------------------------------------------- 1 | use eframe::{ 2 | egui::{Context, ScrollArea, TextureOptions, Ui}, 3 | epaint::{ColorImage, TextureHandle}, 4 | }; 5 | use kolor_64::ColorConversion; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | mod cie_diagram_plot; 9 | mod gamma_tracking_plot; 10 | mod luminance_plot; 11 | mod results_summary; 12 | mod rgb_balance_plot; 13 | 14 | use cie_diagram_plot::draw_cie_diagram_plot; 15 | use gamma_tracking_plot::draw_gamma_tracking_plot; 16 | use luminance_plot::draw_luminance_plot; 17 | use rgb_balance_plot::draw_rgb_balance_plot; 18 | 19 | use crate::{ 20 | calibration::{LuminanceEotf, ReadingResult, TargetColorspace}, 21 | generators::internal::InternalGenerator, 22 | }; 23 | 24 | pub use cie_diagram_plot::compute_cie_chromaticity_diagram_worker; 25 | use results_summary::draw_results_summary_ui; 26 | 27 | use super::PGenApp; 28 | 29 | #[derive(Clone, Deserialize, Serialize)] 30 | pub struct CalibrationState { 31 | pub spotread_started: bool, 32 | pub spotread_cli_args: Vec<(String, Option)>, 33 | pub spotread_tmp_args: (String, Option), 34 | 35 | pub target_csp: TargetColorspace, 36 | 37 | pub min_y: f64, 38 | pub max_y: f64, 39 | pub max_hdr_mdl: f64, 40 | 41 | // Luminance calibration 42 | pub eotf: LuminanceEotf, 43 | pub oetf: bool, 44 | 45 | pub internal_gen: InternalGenerator, 46 | 47 | #[serde(skip)] 48 | pub cie_texture: Option, 49 | 50 | pub show_rgb_balance_plot: bool, 51 | pub show_gamma_plot: bool, 52 | pub show_luminance_plot: bool, 53 | pub show_cie_diagram: bool, 54 | pub show_deviation_percent: bool, 55 | } 56 | 57 | pub(crate) fn add_calibration_ui(app: &mut PGenApp, ui: &mut Ui) { 58 | ScrollArea::vertical().show(ui, |ui| { 59 | let results = app.cal_state.internal_gen.results(); 60 | 61 | if !results.is_empty() { 62 | draw_results_summary_ui(ui, &mut app.cal_state, &results); 63 | ui.separator(); 64 | } 65 | 66 | draw_rgb_balance_plot(ui, &mut app.cal_state, &results); 67 | ui.separator(); 68 | 69 | draw_gamma_tracking_plot(ui, &results, &mut app.cal_state); 70 | ui.separator(); 71 | 72 | draw_luminance_plot(ui, &results, &mut app.cal_state); 73 | ui.separator(); 74 | 75 | draw_cie_diagram_plot(ui, &mut app.cal_state, &results); 76 | ui.add_space(10.0); 77 | }); 78 | } 79 | 80 | pub(crate) fn handle_spotread_result(app: &mut PGenApp, result: Option) { 81 | let internal_gen = &mut app.cal_state.internal_gen; 82 | if let Some(result) = result { 83 | if let Some(patch) = internal_gen.selected_patch_mut() { 84 | patch.result = Some(result); 85 | } 86 | 87 | let last_idx = internal_gen.list.len() - 1; 88 | let can_advance = 89 | internal_gen.auto_advance && internal_gen.selected_idx.is_some_and(|i| i < last_idx); 90 | let continuous_selected = 91 | !internal_gen.auto_advance && internal_gen.read_selected_continuously; 92 | 93 | let idx = can_advance 94 | .then_some(internal_gen.selected_idx.as_mut()) 95 | .flatten(); 96 | if let Some(idx) = idx { 97 | *idx += 1; 98 | } 99 | 100 | // Keep going if it wasn't stopped manually 101 | if internal_gen.started && (can_advance || continuous_selected) { 102 | app.calibration_send_measure_selected_patch(); 103 | } else { 104 | internal_gen.started = false; 105 | app.set_blank(); 106 | } 107 | } else { 108 | // Something went wrong and we got no result, stop calibration 109 | internal_gen.started = false; 110 | app.set_blank(); 111 | } 112 | } 113 | 114 | impl CalibrationState { 115 | pub fn initial_setup(&mut self) { 116 | self.spotread_started = false; 117 | self.internal_gen.started = false; 118 | 119 | self.min_y = self.min_y.clamp(0.0, 1.0); 120 | if self.max_y <= 0.0 { 121 | self.max_y = 100.0; 122 | } 123 | } 124 | 125 | pub fn set_cie_texture(&mut self, ctx: &Context, image: ColorImage) { 126 | self.cie_texture.get_or_insert_with(|| { 127 | ctx.load_texture("cie_xy_diagram_tex", image, TextureOptions::NEAREST) 128 | }); 129 | } 130 | 131 | pub fn target_rgb_to_xyz_conv(&self) -> ColorConversion { 132 | ColorConversion::new(self.target_csp.to_kolor(), kolor_64::spaces::CIE_XYZ) 133 | } 134 | 135 | pub fn update_patterns_target(&mut self) { 136 | self.internal_gen 137 | .list 138 | .iter_mut() 139 | .filter_map(|e| e.result.as_mut()) 140 | .for_each(|res| { 141 | res.target.min_y = self.min_y; 142 | res.target.max_y = self.max_y; 143 | res.target.max_hdr_mdl = self.max_hdr_mdl; 144 | res.target.eotf = self.eotf; 145 | res.target.colorspace = self.target_csp; 146 | 147 | res.set_or_update_calculated_values(); 148 | }); 149 | } 150 | 151 | pub fn min_normalized(&self) -> f64 { 152 | self.min_y / self.max_y 153 | } 154 | } 155 | 156 | impl Default for CalibrationState { 157 | fn default() -> Self { 158 | Self { 159 | spotread_started: false, 160 | spotread_cli_args: Default::default(), 161 | spotread_tmp_args: Default::default(), 162 | 163 | min_y: Default::default(), 164 | max_y: 100.0, 165 | max_hdr_mdl: 1000.0, 166 | target_csp: Default::default(), 167 | eotf: LuminanceEotf::Gamma22, 168 | oetf: true, 169 | 170 | internal_gen: Default::default(), 171 | cie_texture: Default::default(), 172 | show_rgb_balance_plot: true, 173 | show_gamma_plot: true, 174 | show_luminance_plot: true, 175 | show_cie_diagram: true, 176 | show_deviation_percent: false, 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | use kolor_64::Vec3; 4 | 5 | use crate::pgen::{BitDepth, pattern_config::PGenPatternConfig}; 6 | 7 | pub type Rgb = [u16; 3]; 8 | 9 | pub fn compute_rgb_range(limited_range: bool, depth: u8) -> RangeInclusive { 10 | let depth = depth as u32; 11 | let min_rgb_value = if limited_range { 12 | 16 * 2_u16.pow(depth - 8) 13 | } else { 14 | 0 15 | }; 16 | 17 | let max_rgb_value = if limited_range { 18 | let val = if depth == 10 { 219 * 4 } else { 219 }; 19 | val + min_rgb_value 20 | } else { 21 | 2_u16.pow(depth) - 1 22 | }; 23 | 24 | min_rgb_value..=max_rgb_value 25 | } 26 | 27 | pub fn scale_rgb_into_range( 28 | val: f32, 29 | depth: u8, 30 | limited_range: bool, 31 | prev_limited_range: bool, 32 | ) -> f32 { 33 | if prev_limited_range != limited_range { 34 | let limited_max = 219.0; 35 | let (min, max, limited_max) = if depth == 8 { 36 | (16.0, 255.0, limited_max) 37 | } else { 38 | (64.0, 1023.0, limited_max * 4.0) 39 | }; 40 | 41 | if prev_limited_range && !limited_range { 42 | // Limited to Full 43 | (val - min) / limited_max * max 44 | } else { 45 | // Full to Limited 46 | ((val / max) * limited_max) + min 47 | } 48 | } else { 49 | val 50 | } 51 | } 52 | 53 | pub fn scale_8b_rgb_to_10b( 54 | c: u16, 55 | diff: f32, 56 | depth: u8, 57 | limited_range: bool, 58 | prev_limited_range: bool, 59 | ) -> u16 { 60 | let orig_val = c as f32; 61 | let mut val = scale_rgb_into_range(orig_val, 8, limited_range, prev_limited_range); 62 | 63 | if depth > 8 { 64 | // Exception to map old range max to new range 65 | if u8::MAX as f32 - orig_val <= f32::EPSILON { 66 | val = 2.0_f32.powf(depth as f32) - 1.0; 67 | } 68 | 69 | val *= 2.0_f32.powf(diff); 70 | val.round() as u16 71 | } else { 72 | val.round() as u16 73 | } 74 | } 75 | 76 | // Converts for 10 bit otherwise casts to u8 77 | pub fn rgb_10b_to_8b(depth: u8, rgb: Rgb) -> [u8; 3] { 78 | if depth > 8 { 79 | rgb.map(|c| (c / 4) as u8) 80 | } else { 81 | rgb.map(|c| c as u8) 82 | } 83 | } 84 | 85 | pub fn scale_pattern_config_rgb_values( 86 | pattern_config: &mut PGenPatternConfig, 87 | depth: u8, 88 | prev_depth: u8, 89 | limited_range: bool, 90 | prev_limited_range: bool, 91 | ) { 92 | if depth == prev_depth && limited_range == prev_limited_range { 93 | return; 94 | } 95 | 96 | let diff = depth.abs_diff(prev_depth) as f32; 97 | if prev_depth == 8 { 98 | // 8 bit to 10 bit 99 | pattern_config 100 | .patch_colour 101 | .iter_mut() 102 | .chain(pattern_config.background_colour.iter_mut()) 103 | .for_each(|c| { 104 | *c = scale_8b_rgb_to_10b(*c, diff, depth, limited_range, prev_limited_range) 105 | }); 106 | } else { 107 | // 10 bit to 8 bit 108 | pattern_config 109 | .patch_colour 110 | .iter_mut() 111 | .chain(pattern_config.background_colour.iter_mut()) 112 | .for_each(|c| { 113 | let mut val = *c as f32 / 2.0_f32.powf(diff); 114 | val = scale_rgb_into_range(val, depth, limited_range, prev_limited_range); 115 | 116 | *c = val.round() as u16; 117 | }); 118 | } 119 | 120 | pattern_config.bit_depth = BitDepth::from_repr(depth as usize).unwrap(); 121 | pattern_config.limited_range = limited_range; 122 | } 123 | 124 | /// Returns the min as well max - min as the real max value 125 | pub fn get_rgb_real_range(limited_range: bool, bit_depth: u8) -> (u16, u16) { 126 | let rgb_range = compute_rgb_range(limited_range, bit_depth); 127 | let min = *rgb_range.start(); 128 | let real_max = *rgb_range.end() - min; 129 | 130 | (min, real_max) 131 | } 132 | 133 | pub fn rgb_to_float(rgb: Rgb, limited_range: bool, bit_depth: u8) -> Vec3 { 134 | let (min, real_max) = get_rgb_real_range(limited_range, bit_depth); 135 | let real_max = real_max as f64; 136 | 137 | rgb.map(|c| (c - min) as f64 / real_max).into() 138 | } 139 | 140 | pub fn pattern_cfg_set_colour_from_float_level(config: &mut PGenPatternConfig, level: f64) { 141 | let (min, real_max) = get_rgb_real_range(config.limited_range, config.bit_depth as u8); 142 | let (min, real_max) = (min as f64, real_max as f64); 143 | let rounded_level = (level * 1e3).round() / 1e3; 144 | let rgb = [rounded_level, rounded_level, rounded_level] 145 | .map(|c| ((c * real_max) + min).round() as u16); 146 | 147 | config.patch_colour = rgb; 148 | config.background_colour = rgb; 149 | } 150 | 151 | pub fn round_colour(rgb: Vec3) -> Vec3 { 152 | (rgb * 1e6).round() / 1e6 153 | } 154 | 155 | pub fn normalize_float_rgb_components(rgb: Vec3) -> Vec3 { 156 | let max = rgb.max_element(); 157 | if max > 0.0 { 158 | let normalized = (rgb * (1.0 / max)).clamp(Vec3::ZERO, Vec3::ONE); 159 | 160 | round_colour(normalized) 161 | } else { 162 | rgb 163 | } 164 | } 165 | 166 | #[cfg(test)] 167 | mod tests { 168 | use crate::{ 169 | pgen::{BitDepth, pattern_config::PGenPatternConfig}, 170 | utils::scale_pattern_config_rgb_values, 171 | }; 172 | 173 | #[test] 174 | fn test_8bit_override() { 175 | let mut new_pattern_cfg = PGenPatternConfig { 176 | patch_colour: [514, 512, 512], 177 | background_colour: [64, 64, 64], 178 | bit_depth: BitDepth::Ten, 179 | ..Default::default() 180 | }; 181 | 182 | let prev_depth = new_pattern_cfg.bit_depth as u8; 183 | scale_pattern_config_rgb_values(&mut new_pattern_cfg, 8, prev_depth, false, false); 184 | 185 | assert_eq!(new_pattern_cfg.patch_colour, [129, 128, 128]); 186 | assert_eq!(new_pattern_cfg.background_colour, [16, 16, 16]); 187 | assert_eq!(new_pattern_cfg.bit_depth, BitDepth::Eight); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pgen_client - PGenerator client 2 | 3 | Utility to control a Raspberry Pi with a PGenerator installation. 4 | Includes both manual & automatic calibration features. 5 | 6 | Built on top of [egui](https://github.com/emilk/egui), [kolor](https://github.com/BoxDragon/kolor) and [ArgyllCMS](https://www.argyllcms.com). 7 | 8 | ## Features 9 | - Settings to configure the PGenerator similar to `DeviceControl`. 10 | - Allows configuring the output colour format, HDR mode as well as metadata. 11 | - Ability to send test patterns manually to the device. 12 | - Can be used as an external pattern generator for DisplayCAL, through the Davinci Resolve interface. 13 | - An internal pattern generator is available for manual calibration and measuring patch lists. 14 | - The internal generator uses `ArgyllCMS`' `spotread` utility to measure colour patches. 15 | 16 | ### Building 17 | Dependencies: 18 | - Linux: see [eframe](https://github.com/emilk/egui/tree/master/crates/eframe) dependencies. 19 | 20 | ```bash 21 | cargo build --release 22 | ``` 23 | 24 |   25 | 26 | ## Device 27 | The Raspberry Pi must be setup with an installation of [PGenerator](https://www.avsforum.com/threads/dedicated-raspberry-pi-pgenerator-thread-set-up-configuration-updates-special-features-general-usage-tips.3167475). 28 | `pgen_client` was only tested with a Raspberry Pi 4B device. Some features may not be working on older devices. 29 | 30 | ### Configuring the PGenerator output 31 | 32 | First, the program communicates to the `PGenerator` device through TCP over the network. 33 | So you will need to start by figuring out the IP address to connect to. 34 | 35 | You should then be able to connect to the default port, `85`. 36 | 37 | Configurations: 38 | - `Display mode`: the resolution/refresh rate combination to use for the display. 39 | - `Color format`: RGB or YCbCr is possible, whether it works is dependent on the display. 40 | - `Quant range`: Full/Limited range for the display output. Also requires the display to support the selected option. 41 | - `Bit depth`: Sets the output bit depth for the HDMI data. 42 | - `Colorimetry`: Sets the HDMI colorimetry flag. This is used by the display to interpret the pixels correctly. 43 | - `Dynamic range`: Allows switching the output mode to `SDR`, `HDR10` or `Dolby Vision`. 44 | - This is handled by the `PGenerator` software. The display must support the selected mode. 45 | 46 | `HDR metadata / DRM infoframe` is for the metadata signaling in HDMI for HDR output. It is only used for the `HDR` dynamic range mode. 47 | The `HDR` mode can also switch to `HLG` if the `HLG` EOTF is selected. 48 | 49 | With the exception of `Display mode` and `Dynamic range` configurations: 50 | - All configuration changes require that the `PGenerator` software be restarted before they are applied to the output. 51 | This can be done with either the `Restart PGenerator software` or the `Set AVI/DRM infoframe` buttons. 52 | 53 | 54 | 55 | 56 | 57 | ### Configuring the test patterns 58 | 59 | Once the `PGenerator` device is properly connected, test patterns can be displayed. 60 | The most important settings here are: 61 | - `Patch precision`: Whether to use 8 bit or 10 bit patches. 62 | - Changing the pattern depth may reconfigure the output to a different depth. 63 | - It would be reconfigured every time a different precision pattern is sent. 64 | - `Patch size`: Size the patches take on the display, in % windows. 65 | - `Position`: How the patches are positioned on the display. This can be a preset position or specific pixel coordinates. 66 | 67 | With both internal/external pattern generators, the patches are sent at the configured size/position in `pgen_client`. 68 | 69 | **Info about Quant range and the pattern limited range config**: 70 | - When the quant range is set to `Full`: 71 | - Display may be setup with Auto/Full range. Patches must be sent as full range. 72 | - Setting the display to Limited range input, patches may be sent with limited range enabled. 73 | - When the quant range is set to `Limited`: 74 | - Patches must always be sent as full range, so Limited range must be disabled. 75 | - Display must be set to Auto/Limited, never Full. 76 | 77 | For patch and background colours, they are either selected manually or through a pattern generator as described below. 78 | Patterns can be sent manually to test the configuration. 79 | 80 |   81 | 82 | ## External pattern generator 83 | 84 | Currently only supports displaying 10 bit patterns from DisplayCAL. 85 | DisplayCAL must be configured with a `Resolve` display. 86 | 87 | **Instructions**: 88 | 1. To connect, start a calibration. 89 | 2. Select the `External` pattern generator and click the `Start generator client` button. 90 | 3. DisplayCAL should start sending test patterns to the display. 91 | 92 |   93 | 94 | ## Internal pattern generator 95 | `pgen_client` can be used for simple manual calibration. 96 | It supports basic presets as well as the ability to load custom CSV patch lists. 97 | Usage is targeted at more advanced users that know how to interpret the measurements data. 98 | 99 | `ArgyllCMS` must be installed on the system and the executables present in `PATH`. 100 | 101 | > [!WARNING] 102 | > I cannot guarantee that the displayed measurement data is accurate or even correct. 103 | > My knowledge of colour math is limited and I have not done extensive verification. 104 | > Do consider double checking results with other calibration software such as `DisplayCAL` or `HCFR`. 105 | 106 | **Instructions**: 107 | 1. Select the `Internal` pattern generator. 108 | 1. Set up the `spotread` CLI arguments and start `spotread`. 109 | 2. Set the min/max target brightness as well as target primaries for the calibration. 110 | 3. Load a patch list to measure. 111 | 4. Measure all patches or select a single one and measure it. 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/external.rs: -------------------------------------------------------------------------------- 1 | use futures::StreamExt; 2 | use tokio::sync::mpsc::Sender; 3 | use tokio_stream::wrappers::ReceiverStream; 4 | 5 | use crate::{ 6 | app::{PGenAppUpdate, ReadFileType}, 7 | generators::{ 8 | GeneratorClient, GeneratorClientCmd, GeneratorInterface, start_tcp_generator_client, 9 | }, 10 | pgen::controller::{PGenControllerCmd, PGenControllerHandle}, 11 | spotread::{SpotreadCmd, SpotreadReadingConfig, start_spotread_worker}, 12 | }; 13 | 14 | #[derive(Debug, Clone)] 15 | pub enum ExternalJobCmd { 16 | StartGeneratorClient(GeneratorClient), 17 | StopGeneratorClient(GeneratorClient), 18 | 19 | // spotread 20 | StartSpotreadProcess(Vec<(String, Option)>), 21 | StopSpotreadProcess, 22 | SpotreadMeasure(SpotreadReadingConfig), 23 | SpotreadDoneMeasuring, 24 | 25 | ReadFile(ReadFileType), 26 | } 27 | 28 | pub fn start_external_jobs_worker( 29 | app_tx: Sender, 30 | controller_tx: Sender, 31 | controller_handle: PGenControllerHandle, 32 | ) -> Sender { 33 | let mut gen_client_tx = None; 34 | let mut spotread_tx = None; 35 | 36 | let (tx, rx) = tokio::sync::mpsc::channel(5); 37 | let mut rx = ReceiverStream::new(rx).fuse(); 38 | 39 | { 40 | let tx = tx.clone(); 41 | tokio::spawn(async move { 42 | loop { 43 | futures::select! { 44 | cmd = rx.select_next_some() => { 45 | app_tx.try_send(PGenAppUpdate::Processing).ok(); 46 | 47 | match cmd { 48 | ExternalJobCmd::StartGeneratorClient(client) => { 49 | log::trace!("Generator: Starting client {client:?}"); 50 | 51 | match client.interface() { 52 | GeneratorInterface::Tcp(tcp_interface) => { 53 | let client_fut = start_tcp_generator_client(app_tx.clone(), controller_tx.clone(), tx.clone(), tcp_interface); 54 | if let Ok(tx) = client_fut.await { 55 | gen_client_tx.replace(tx); 56 | } 57 | } 58 | }; 59 | 60 | if gen_client_tx.is_some() { 61 | app_tx.try_send(PGenAppUpdate::GeneratorListening(true)).ok(); 62 | } 63 | app_tx.try_send(PGenAppUpdate::DoneProcessing).ok(); 64 | }, 65 | ExternalJobCmd::StopGeneratorClient(client) => { 66 | if let Some(client_tx) = gen_client_tx.take() { 67 | log::trace!("Generator: Stopping client {client:?}"); 68 | client_tx.try_send(GeneratorClientCmd::Shutdown).ok(); 69 | } else { 70 | app_tx.try_send(PGenAppUpdate::GeneratorListening(false)).ok(); 71 | } 72 | app_tx.try_send(PGenAppUpdate::DoneProcessing).ok(); 73 | }, 74 | ExternalJobCmd::StartSpotreadProcess(cli_args) => { 75 | log::trace!("spotread: Starting process"); 76 | match start_spotread_worker(app_tx.clone(), tx.clone(), controller_handle.clone(), cli_args) { 77 | Ok(tx) => { 78 | spotread_tx.replace(tx); 79 | app_tx.try_send(PGenAppUpdate::SpotreadStarted(true)).ok(); 80 | } 81 | Err(e) => { 82 | log::error!("spotread: Start failed: {e}"); 83 | } 84 | } 85 | app_tx.try_send(PGenAppUpdate::DoneProcessing).ok(); 86 | } 87 | ExternalJobCmd::StopSpotreadProcess => { 88 | if let Some(tx) = spotread_tx.take() { 89 | log::trace!("spotread: Stopping process"); 90 | tx.try_send(SpotreadCmd::Exit).ok(); 91 | } else { 92 | app_tx.try_send(PGenAppUpdate::SpotreadStarted(false)).ok(); 93 | } 94 | app_tx.try_send(PGenAppUpdate::DoneProcessing).ok(); 95 | } 96 | ExternalJobCmd::SpotreadMeasure(config) => { 97 | if let Some(spotread_tx) = spotread_tx.as_ref() { 98 | spotread_tx.try_send(SpotreadCmd::DoReading(config)).ok(); 99 | } 100 | } 101 | ExternalJobCmd::SpotreadDoneMeasuring => { 102 | app_tx.try_send(PGenAppUpdate::DoneProcessing).ok(); 103 | } 104 | ExternalJobCmd::ReadFile(file_type) => { 105 | let title = file_type.title(); 106 | 107 | let mut dialog = rfd::FileDialog::new().set_title(title); 108 | for (filter_name, exts) in file_type.filters() { 109 | dialog = dialog.add_filter(*filter_name, exts); 110 | } 111 | 112 | if let Some(path) = dialog.pick_file() { 113 | app_tx.try_send(PGenAppUpdate::ReadFileResponse(file_type, path)).ok(); 114 | } 115 | 116 | app_tx.try_send(PGenAppUpdate::DoneProcessing).ok(); 117 | } 118 | } 119 | } 120 | } 121 | } 122 | }); 123 | } 124 | 125 | tx 126 | } 127 | -------------------------------------------------------------------------------- /src/app/calibration/cie_diagram_plot.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use ecolor::gamma_u8_from_linear_f32; 4 | use eframe::{ 5 | egui::{Color32, Spinner, Ui}, 6 | epaint::{ColorImage, Pos2, Rect, Stroke, Vec2}, 7 | }; 8 | use egui_plot::{MarkerShape, Plot, PlotImage, PlotPoint, PlotPoints, Points, Polygon}; 9 | use itertools::Itertools; 10 | use kolor_64::{ 11 | ColorConversion, 12 | details::{color::WhitePoint, transform::xyY_to_XYZ}, 13 | spaces::CIE_XYZ, 14 | }; 15 | use ndarray::{ 16 | Array, 17 | parallel::prelude::{IntoParallelRefIterator, ParallelIterator}, 18 | }; 19 | use tokio::sync::mpsc::Sender; 20 | 21 | use crate::{app::PGenAppUpdate, utils::normalize_float_rgb_components}; 22 | 23 | use super::{CalibrationState, ReadingResult}; 24 | 25 | const CIE_1931_2DEG_OBSERVER_DATASET: &str = include_str!(concat!( 26 | env!("CARGO_MANIFEST_DIR"), 27 | "/data/CIE_cc_1931_2deg.csv" 28 | )); 29 | static CIE_1931_DIAGRAM_POINTS: OnceLock> = OnceLock::new(); 30 | 31 | // Calculated from locis coordinates 32 | const XY_TOP_LEFT: Vec2 = Vec2::new(0.00364, 0.83409); 33 | const XY_BOTTOM_RIGHT: Vec2 = Vec2::new(0.73469, 0.00477); 34 | 35 | #[derive(Debug, Clone, Copy)] 36 | pub struct SpectralLocusPoint { 37 | _wavelength: u16, 38 | x: f64, 39 | y: f64, 40 | } 41 | 42 | pub fn draw_cie_diagram_plot( 43 | ui: &mut Ui, 44 | cal_state: &mut CalibrationState, 45 | results: &[ReadingResult], 46 | ) { 47 | ui.horizontal(|ui| { 48 | ui.heading("Chromaticity xy"); 49 | ui.checkbox(&mut cal_state.show_cie_diagram, "Show"); 50 | }); 51 | 52 | if cal_state.show_cie_diagram { 53 | draw_diagram(ui, cal_state, results); 54 | } 55 | } 56 | 57 | fn draw_diagram(ui: &mut Ui, cal_state: &mut CalibrationState, results: &[ReadingResult]) { 58 | if let (Some(texture), Some(locis_points)) = ( 59 | cal_state.cie_texture.as_ref(), 60 | CIE_1931_DIAGRAM_POINTS.get(), 61 | ) { 62 | let dark_mode = ui.ctx().style().visuals.dark_mode; 63 | let locis_points: Vec<_> = locis_points.iter().map(|e| [e.x, e.y]).collect(); 64 | 65 | let curve_stroke_colour = if dark_mode { 66 | Color32::from_rgba_unmultiplied(255, 255, 255, 64) 67 | } else { 68 | Color32::from_rgba_unmultiplied(96, 96, 96, 96) 69 | }; 70 | let curve_poly = Polygon::new("Curve", PlotPoints::new(locis_points)) 71 | .fill_color(Color32::TRANSPARENT) 72 | .stroke(Stroke::new(4.0, curve_stroke_colour)); 73 | 74 | let img_size = Vec2::new(XY_BOTTOM_RIGHT.x, XY_TOP_LEFT.y); 75 | let img_center = img_size / 2.0; 76 | let center_point = PlotPoint::new(img_center.x, img_center.y); 77 | let image = 78 | PlotImage::new("Image", texture.id(), center_point, img_size).uv(Rect::from_two_pos( 79 | Pos2::new(0.0, 1.0 - XY_TOP_LEFT.y), 80 | Pos2::new(XY_BOTTOM_RIGHT.x, 1.0), 81 | )); 82 | 83 | let triangle_colour = if dark_mode { 84 | Color32::WHITE 85 | } else { 86 | Color32::GRAY 87 | }; 88 | let target_csp = cal_state.target_csp.to_kolor(); 89 | let target_primaries = target_csp.primaries().values().map(|xy| xy).to_vec(); 90 | let target_gamut_triangle = Polygon::new("Target Gamut", target_primaries) 91 | .stroke(Stroke::new(2.0, triangle_colour)) 92 | .fill_color(Color32::TRANSPARENT); 93 | 94 | let target_eotf = cal_state.eotf; 95 | let results_points = results.iter().map(|res| { 96 | let coords = [res.xyy[0], res.xyy[1]]; 97 | // OETF from assumed target 98 | let rgb_gamma = target_eotf.convert_vec(res.rgb, true); 99 | let rgb_gamma = normalize_float_rgb_components(rgb_gamma); 100 | let rgb = (rgb_gamma * 255.0).round().to_array().map(|c| c as u8); 101 | 102 | ( 103 | coords, 104 | Color32::from_rgb(rgb[0], rgb[1], rgb[2]).gamma_multiply(0.75), 105 | ) 106 | }); 107 | 108 | let target_rgb_to_xyz = cal_state.target_rgb_to_xyz_conv(); 109 | let results_targets = results 110 | .iter() 111 | .map(|res| create_target_box_for_result(res, target_rgb_to_xyz)); 112 | 113 | let target_box_colour = if dark_mode { 114 | Color32::GRAY 115 | } else { 116 | Color32::WHITE 117 | }; 118 | Plot::new("cie_diagram_plot") 119 | .data_aspect(1.0) 120 | .view_aspect(1.5) 121 | .allow_scroll(false) 122 | .show_grid(false) 123 | .clamp_grid(true) 124 | .show_background(false) 125 | .show(ui, |plot_ui| { 126 | plot_ui.image(image); 127 | plot_ui.polygon(curve_poly); 128 | plot_ui.polygon(target_gamut_triangle); 129 | 130 | for (center, xy_target) in results_targets { 131 | let poly = xy_target 132 | .stroke(Stroke::new(2.0, target_box_colour)) 133 | .fill_color(Color32::TRANSPARENT); 134 | let center_cross = Points::new("Center", center) 135 | .radius(12.0) 136 | .color(Color32::BLACK) 137 | .shape(MarkerShape::Cross); 138 | 139 | plot_ui.polygon(poly); 140 | plot_ui.points(center_cross); 141 | } 142 | 143 | for (res_coords, measured_colour) in results_points { 144 | let point_out = Points::new("Result out", res_coords) 145 | .radius(8.0) 146 | .color(Color32::GRAY); 147 | let point_in = Points::new("Result in", res_coords) 148 | .radius(5.0) 149 | .color(measured_colour); 150 | 151 | plot_ui.points(point_out); 152 | plot_ui.points(point_in); 153 | } 154 | }); 155 | } else { 156 | ui.add(Spinner::new().size(100.0)); 157 | } 158 | } 159 | 160 | pub fn compute_cie_chromaticity_diagram_worker(app_tx: Sender) { 161 | tokio::task::spawn(async move { 162 | let locis = CIE_1931_DIAGRAM_POINTS.get_or_init(|| { 163 | CIE_1931_2DEG_OBSERVER_DATASET 164 | .lines() 165 | .map(|line| { 166 | let mut split = line.split(','); 167 | 168 | let wavelength = split.next().and_then(|e| e.parse().ok()).unwrap(); 169 | let x = split.next().and_then(|e| e.parse().ok()).unwrap(); 170 | let y = split.next().and_then(|e| e.parse().ok()).unwrap(); 171 | 172 | SpectralLocusPoint { 173 | _wavelength: wavelength, 174 | x, 175 | y, 176 | } 177 | }) 178 | .collect() 179 | }); 180 | let locis_points: Vec<_> = locis.iter().map(|locus| [locus.x, locus.y]).collect(); 181 | 182 | let img = compute_cie_xy_diagram_image(&locis_points); 183 | app_tx.try_send(PGenAppUpdate::CieDiagramReady(img)).ok(); 184 | }); 185 | } 186 | 187 | fn compute_cie_xy_diagram_image(points: &[[f64; 2]]) -> ColorImage { 188 | let resolution = 4096; 189 | 190 | let x_points = Array::linspace(0.0, 1.0, resolution); 191 | let y_points = Array::linspace(1.0, 0.0, resolution); 192 | let grid_points = Array::from_iter(y_points.iter().cartesian_product(x_points.iter())); 193 | 194 | let xyz_conv = kolor_64::ColorConversion::new(CIE_XYZ, kolor_64::spaces::BT_709); 195 | let wp = WhitePoint::D65; 196 | 197 | let pixels: Vec = grid_points 198 | .par_iter() 199 | .copied() 200 | .map(|(y, x)| { 201 | let x = *x; 202 | let y = *y; 203 | 204 | if !point_in_or_on_convex_polygon(points, x, y) { 205 | return Color32::TRANSPARENT; 206 | } 207 | 208 | let xyy = [x, y, 1.0].into(); 209 | let xyz = xyY_to_XYZ(xyy, wp); 210 | 211 | let rgb = normalize_float_rgb_components(xyz_conv.convert(xyz)) 212 | .to_array() 213 | .map(|c| gamma_u8_from_linear_f32(c as f32)); 214 | 215 | Color32::from_rgb(rgb[0], rgb[1], rgb[2]).gamma_multiply(0.85) 216 | }) 217 | .collect(); 218 | 219 | ColorImage { 220 | size: [resolution, resolution], 221 | source_size: Vec2::new(resolution as f32, resolution as f32), 222 | pixels, 223 | } 224 | } 225 | 226 | fn point_in_or_on_convex_polygon(points: &[[f64; 2]], x: f64, y: f64) -> bool { 227 | let mut i = 0; 228 | let mut j = points.len() - 1; 229 | let mut result = false; 230 | 231 | loop { 232 | if i >= points.len() { 233 | break; 234 | } 235 | 236 | let (x1, y1) = points[i].into(); 237 | let (x2, y2) = points[j].into(); 238 | 239 | if (x == x1 && y == y1) || (x == x2 && y == y2) { 240 | return true; 241 | } 242 | 243 | if (y1 > y) != (y2 > y) && (x < (x2 - x1) * (y - y1) / (y2 - y1) + x1) { 244 | result = !result; 245 | } 246 | 247 | j = i; 248 | i += 1; 249 | } 250 | 251 | result 252 | } 253 | 254 | const TARGET_BOX_LENGTH: f64 = 0.0075; 255 | fn create_target_box_for_result( 256 | res: &'_ ReadingResult, 257 | target_rgb_to_xyz: ColorConversion, 258 | ) -> ([f64; 2], Polygon<'_>) { 259 | let xyy = res.ref_xyy_display_space(target_rgb_to_xyz); 260 | 261 | let x = xyy[0]; 262 | let y = xyy[1]; 263 | 264 | let poly = Polygon::new( 265 | "Target Box", 266 | vec![ 267 | [x + TARGET_BOX_LENGTH, y - TARGET_BOX_LENGTH], 268 | [x - TARGET_BOX_LENGTH, y - TARGET_BOX_LENGTH], 269 | [x - TARGET_BOX_LENGTH, y + TARGET_BOX_LENGTH], 270 | [x + TARGET_BOX_LENGTH, y + TARGET_BOX_LENGTH], 271 | ], 272 | ); 273 | 274 | ([x, y], poly) 275 | } 276 | -------------------------------------------------------------------------------- /src/pgen/commands.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | 3 | use strum::{AsRefStr, Display}; 4 | 5 | use super::{ 6 | BitDepth, ColorFormat, Colorimetry, DoviMapMode, DynamicRange, HdrEotf, Primaries, QuantRange, 7 | client::{ConnectState, PGenTestPattern}, 8 | controller::DisplayMode, 9 | }; 10 | 11 | #[derive(Debug, Clone)] 12 | pub enum PGenCommand { 13 | IsAlive, 14 | Connect, 15 | Quit, 16 | Shutdown, 17 | Reboot, 18 | UpdateSocket(SocketAddr), 19 | RestartSoftware, 20 | TestPattern(PGenTestPattern), 21 | MultipleGetConfCommands(&'static [PGenGetConfCommand]), 22 | MultipleSetConfCommands(Vec), 23 | } 24 | 25 | #[derive(Debug, Clone)] 26 | pub enum PGenCommandResponse { 27 | NotConnected, 28 | Busy, 29 | Ok(bool), 30 | Errored(String), 31 | Alive(bool), 32 | Connect(ConnectState), 33 | Quit(ConnectState), 34 | Shutdown(ConnectState), 35 | Reboot(ConnectState), 36 | MultipleGetConfRes(Vec<(PGenGetConfCommand, String)>), 37 | // true if the config was properly set 38 | MultipleSetConfRes(Vec<(PGenSetConfCommand, bool)>), 39 | } 40 | 41 | #[derive(Display, AsRefStr, Debug, Copy, Clone)] 42 | pub enum PGenGetConfCommand { 43 | #[strum(to_string = "GET_PGENERATOR_VERSION")] 44 | GetPGeneratorVersion, 45 | #[strum(to_string = "GET_PGENERATOR_IS_EXECUTED")] 46 | GetPGeneratorPid, 47 | #[strum(to_string = "GET_MODE")] 48 | GetCurrentMode, 49 | #[strum(to_string = "GET_MODES_AVAILABLE")] 50 | GetModesAvailable, 51 | 52 | #[strum(to_string = "GET_PGENERATOR_CONF_COLOR_FORMAT")] 53 | GetColorFormat, 54 | #[strum(to_string = "GET_PGENERATOR_CONF_MAX_BPC")] 55 | GetBitDepth, 56 | #[strum(to_string = "GET_PGENERATOR_CONF_RGB_QUANT_RANGE")] 57 | GetQuantRange, 58 | #[strum(to_string = "GET_PGENERATOR_CONF_COLORIMETRY")] 59 | GetColorimetry, 60 | #[strum(to_string = "GET_PGENERATOR_CONF_IS_SDR")] 61 | GetOutputIsSDR, 62 | #[strum(to_string = "GET_PGENERATOR_CONF_IS_HDR")] 63 | GetOutputIsHDR, 64 | #[strum(to_string = "GET_PGENERATOR_CONF_IS_LL_DOVI")] 65 | GetOutputIsLLDV, 66 | #[strum(to_string = "GET_PGENERATOR_CONF_IS_STD_DOVI")] 67 | GetOutputIsStdDovi, 68 | #[strum(to_string = "GET_PGENERATOR_CONF_DV_MAP_MODE")] 69 | GetDoviMapMode, 70 | 71 | // HDR metadata infoframe 72 | #[strum(to_string = "GET_PGENERATOR_CONF_EOTF")] 73 | GetHdrEotf, 74 | #[strum(to_string = "GET_PGENERATOR_CONF_PRIMARIES")] 75 | GetHdrPrimaries, 76 | #[strum(to_string = "GET_PGENERATOR_CONF_MAX_LUMA")] 77 | GetHdrMaxMdl, 78 | #[strum(to_string = "GET_PGENERATOR_CONF_MIN_LUMA")] 79 | GetHdrMinMdl, 80 | #[strum(to_string = "GET_PGENERATOR_CONF_MAX_CLL")] 81 | GetHdrMaxCLL, 82 | #[strum(to_string = "GET_PGENERATOR_CONF_MAX_FALL")] 83 | GetHdrMaxFALL, 84 | 85 | // Always returns "RGB Full", not used 86 | #[strum(to_string = "GET_OUTPUT_RANGE")] 87 | GetOutputRange, 88 | } 89 | 90 | #[derive(Display, AsRefStr, Debug, Copy, Clone)] 91 | pub enum PGenSetConfCommand { 92 | #[strum(to_string = "SET_MODE")] 93 | SetDisplayMode(DisplayMode), 94 | #[strum(to_string = "SET_PGENERATOR_CONF_COLOR_FORMAT")] 95 | SetColorFormat(ColorFormat), 96 | #[strum(to_string = "SET_PGENERATOR_CONF_MAX_BPC")] 97 | SetBitDepth(BitDepth), 98 | #[strum(to_string = "SET_PGENERATOR_CONF_RGB_QUANT_RANGE")] 99 | SetQuantRange(QuantRange), 100 | #[strum(to_string = "SET_PGENERATOR_CONF_COLORIMETRY")] 101 | SetColorimetry(Colorimetry), 102 | 103 | #[strum(to_string = "SET_PGENERATOR_CONF_IS_SDR")] 104 | SetOutputIsSDR(bool), 105 | #[strum(to_string = "SET_PGENERATOR_CONF_IS_HDR")] 106 | SetOutputIsHDR(bool), 107 | #[strum(to_string = "SET_PGENERATOR_CONF_IS_LL_DOVI")] 108 | SetOutputIsLLDV(bool), 109 | #[strum(to_string = "SET_PGENERATOR_CONF_IS_STD_DOVI")] 110 | SetOutputIsStdDovi(bool), 111 | #[strum(to_string = "SET_PGENERATOR_CONF_DV_STATUS")] 112 | SetDoviStatus(bool), 113 | #[strum(to_string = "SET_PGENERATOR_CONF_DV_INTERFACE")] 114 | SetDoviInterface(bool), 115 | #[strum(to_string = "SET_PGENERATOR_CONF_DV_MAP_MODE")] 116 | SetDoviMapMode(DoviMapMode), 117 | 118 | // HDR metadata infoframe 119 | #[strum(to_string = "SET_PGENERATOR_CONF_EOTF")] 120 | SetHdrEotf(HdrEotf), 121 | #[strum(to_string = "SET_PGENERATOR_CONF_PRIMARIES")] 122 | SetHdrPrimaries(Primaries), 123 | #[strum(to_string = "SET_PGENERATOR_CONF_MAX_LUMA")] 124 | SetHdrMaxMdl(u16), 125 | #[strum(to_string = "SET_PGENERATOR_CONF_MIN_LUMA")] 126 | SetHdrMinMdl(u16), 127 | #[strum(to_string = "SET_PGENERATOR_CONF_MAX_CLL")] 128 | SetHdrMaxCLL(u16), 129 | #[strum(to_string = "SET_PGENERATOR_CONF_MAX_FALL")] 130 | SetHdrMaxFALL(u16), 131 | } 132 | 133 | impl PGenGetConfCommand { 134 | pub const fn base_info_commands() -> &'static [Self] { 135 | &[ 136 | Self::GetPGeneratorVersion, 137 | Self::GetPGeneratorPid, 138 | Self::GetModesAvailable, 139 | Self::GetCurrentMode, 140 | Self::GetColorFormat, 141 | Self::GetBitDepth, 142 | Self::GetQuantRange, 143 | Self::GetColorimetry, 144 | Self::GetOutputIsSDR, 145 | Self::GetOutputIsHDR, 146 | Self::GetOutputIsLLDV, 147 | Self::GetOutputIsStdDovi, 148 | Self::GetDoviMapMode, 149 | Self::GetHdrEotf, 150 | Self::GetHdrPrimaries, 151 | Self::GetHdrMaxMdl, 152 | Self::GetHdrMinMdl, 153 | Self::GetHdrMaxCLL, 154 | Self::GetHdrMaxFALL, 155 | ] 156 | } 157 | 158 | pub fn split_command_result<'a>(&self, res: &'a str) -> Option<&'a str> { 159 | let cmd_str = self.as_ref(); 160 | res.find(cmd_str).map(|i| { 161 | // Ignore : 162 | &res[i + cmd_str.len() + 1..] 163 | }) 164 | } 165 | 166 | // true = limited, false = full 167 | pub fn parse_get_output_range(res: String) -> Option { 168 | Self::GetOutputRange 169 | .split_command_result(&res) 170 | .and_then(|res| res.split_whitespace().last().map(|range| range != "full")) 171 | } 172 | 173 | pub fn parse_bool_config(&self, res: String) -> bool { 174 | self.split_command_result(&res) 175 | .map(|res| res == "1") 176 | .unwrap_or(false) 177 | } 178 | 179 | pub fn parse_number_config(&self, res: String) -> Option { 180 | self.split_command_result(&res) 181 | .and_then(|res| res.parse::().ok()) 182 | } 183 | 184 | pub fn parse_string_config<'a>(&self, res: &'a str) -> &'a str { 185 | self.split_command_result(res).unwrap_or("Unknown") 186 | } 187 | } 188 | 189 | impl PGenSetConfCommand { 190 | pub const fn value(self) -> usize { 191 | match self { 192 | Self::SetDisplayMode(mode) => mode.id, 193 | Self::SetColorFormat(format) => format as usize, 194 | Self::SetBitDepth(bit_depth) => bit_depth as usize, 195 | Self::SetQuantRange(quant_range) => quant_range as usize, 196 | Self::SetColorimetry(colorimetry) => colorimetry as usize, 197 | Self::SetOutputIsSDR(is_sdr) => is_sdr as usize, 198 | Self::SetOutputIsHDR(is_hdr) => is_hdr as usize, 199 | Self::SetOutputIsLLDV(is_lldv) => is_lldv as usize, 200 | Self::SetOutputIsStdDovi(is_std_dovi) => is_std_dovi as usize, 201 | Self::SetDoviStatus(dovi_status) => dovi_status as usize, 202 | Self::SetDoviInterface(dovi_interface) => dovi_interface as usize, 203 | Self::SetDoviMapMode(dovi_map_mode) => dovi_map_mode as usize, 204 | Self::SetHdrEotf(eotf) => eotf as usize, 205 | Self::SetHdrPrimaries(primaries) => primaries as usize, 206 | Self::SetHdrMaxMdl(max_mdl) => max_mdl as usize, 207 | Self::SetHdrMinMdl(min_mdl) => min_mdl as usize, 208 | Self::SetHdrMaxCLL(maxcll) => maxcll as usize, 209 | Self::SetHdrMaxFALL(maxfall) => maxfall as usize, 210 | } 211 | } 212 | 213 | fn base_config_for_dynamic_range(dynamic_range: DynamicRange) -> (bool, bool, bool, Vec) { 214 | let is_sdr = dynamic_range == DynamicRange::Sdr; 215 | let is_hdr = dynamic_range == DynamicRange::Hdr; 216 | let is_dovi = dynamic_range == DynamicRange::Dovi; 217 | 218 | let commands = vec![ 219 | Self::SetOutputIsSDR(is_sdr), 220 | Self::SetOutputIsHDR(is_hdr || is_dovi), 221 | Self::SetOutputIsLLDV(is_dovi), 222 | Self::SetOutputIsStdDovi(is_dovi), 223 | Self::SetDoviStatus(is_dovi), 224 | Self::SetDoviInterface(is_dovi), 225 | ]; 226 | 227 | (is_sdr, is_hdr, is_dovi, commands) 228 | } 229 | 230 | const fn default_sdr_config() -> &'static [Self] { 231 | &[PGenSetConfCommand::SetColorimetry(Colorimetry::Bt709Ycc)] 232 | } 233 | 234 | const fn default_hdr_config() -> &'static [Self] { 235 | &[ 236 | Self::SetColorimetry(Colorimetry::Bt2020Rgb), 237 | Self::SetBitDepth(BitDepth::Ten), 238 | Self::SetHdrPrimaries(Primaries::DisplayP3), 239 | ] 240 | } 241 | 242 | const fn dovi_config() -> &'static [Self] { 243 | &[ 244 | Self::SetColorFormat(ColorFormat::Rgb), 245 | Self::SetQuantRange(QuantRange::Full), 246 | Self::SetBitDepth(BitDepth::Eight), 247 | Self::SetColorimetry(Colorimetry::Bt709Ycc), 248 | Self::SetDoviMapMode(DoviMapMode::Absolute), 249 | Self::SetHdrPrimaries(Primaries::Rec2020), 250 | ] 251 | } 252 | 253 | pub fn commands_for_dynamic_range(dynamic_range: DynamicRange) -> Vec { 254 | let (is_sdr, is_hdr, is_dovi, mut commands) = 255 | Self::base_config_for_dynamic_range(dynamic_range); 256 | 257 | // Set default configs 258 | if is_sdr { 259 | commands.extend(Self::default_sdr_config()); 260 | } else if is_hdr { 261 | commands.extend(Self::default_hdr_config()); 262 | } else if is_dovi { 263 | // Required params for DoVi 264 | commands.extend(Self::dovi_config()); 265 | } 266 | 267 | commands 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/spotread.rs: -------------------------------------------------------------------------------- 1 | use std::{iter::once, process::Stdio, time::Duration}; 2 | 3 | use anyhow::{Result, anyhow, bail}; 4 | use futures::{FutureExt, StreamExt}; 5 | use tokio::{ 6 | io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter, Lines}, 7 | process::{Child, ChildStderr, ChildStdin, ChildStdout, Command}, 8 | sync::mpsc::Sender, 9 | }; 10 | use tokio_stream::wrappers::ReceiverStream; 11 | 12 | use crate::{ 13 | app::PGenAppUpdate, 14 | calibration::{CalibrationTarget, PatternInsertionConfig, ReadingResult}, 15 | external::ExternalJobCmd, 16 | pgen::{controller::PGenControllerHandle, pattern_config::PGenPatternConfig}, 17 | utils::pattern_cfg_set_colour_from_float_level, 18 | }; 19 | 20 | const EXPECTED_INIT_LINE: &str = "Place instrument on spot to be measured"; 21 | const READING_READY_SUBSTR: &str = "take a reading:"; 22 | const READING_RESULT_SUBSTR: &str = "Result is XYZ"; 23 | 24 | #[derive(Debug)] 25 | struct SpotreadProc { 26 | child: Child, 27 | err_lines: Lines>, 28 | 29 | reader: BufReader, 30 | read_buf: Vec, 31 | can_take_reading: bool, 32 | writer: BufWriter, 33 | 34 | app_tx: Sender, 35 | } 36 | 37 | #[derive(Debug)] 38 | pub enum SpotreadCmd { 39 | DoReading(SpotreadReadingConfig), 40 | Exit, 41 | } 42 | 43 | #[derive(Debug, Clone, Copy)] 44 | pub struct SpotreadReadingConfig { 45 | pub target: CalibrationTarget, 46 | pub pattern_cfg: PGenPatternConfig, 47 | pub pattern_insertion_cfg: PatternInsertionConfig, 48 | } 49 | 50 | pub fn start_spotread_worker( 51 | app_tx: Sender, 52 | external_tx: Sender, 53 | controller_handle: PGenControllerHandle, 54 | cli_args: Vec<(String, Option)>, 55 | ) -> Result> { 56 | let (tx, rx) = tokio::sync::mpsc::channel(5); 57 | let mut rx = ReceiverStream::new(rx).fuse(); 58 | 59 | let mut spotread_proc = tokio::task::block_in_place(|| { 60 | let mut spotread_proc = SpotreadProc::new(app_tx.clone(), cli_args)?; 61 | let mut init_line = String::with_capacity(64); 62 | 63 | tokio::runtime::Handle::current().block_on(async { 64 | loop { 65 | futures::select! { 66 | err_line = spotread_proc.err_lines.next_line().fuse() => { 67 | if let Ok(Some(line)) = err_line 68 | && line.starts_with("Diagnostic") { 69 | log::error!("Something failed: {line}"); 70 | spotread_proc.exit_logged(false).await; 71 | 72 | app_tx.try_send(PGenAppUpdate::SpotreadStarted(false)).ok(); 73 | bail!("Failed starting spotread"); 74 | } 75 | } 76 | res = spotread_proc.reader.read_line(&mut init_line).fuse() => match res { 77 | Ok(_) => { 78 | if init_line.trim().contains(EXPECTED_INIT_LINE) { 79 | log::trace!("init line: {init_line:?}"); 80 | spotread_proc.read_until_take_reading_ready().await?; 81 | 82 | break; 83 | } 84 | }, 85 | Err(e) => { 86 | log::error!("init: {e}"); 87 | tokio::time::sleep(Duration::from_secs(1)).await; 88 | continue; 89 | } 90 | } 91 | } 92 | } 93 | 94 | Ok::(spotread_proc) 95 | }) 96 | })?; 97 | 98 | tokio::spawn(async move { 99 | loop { 100 | futures::select! { 101 | err_line = spotread_proc.err_lines.next_line().fuse() => { 102 | if let Ok(Some(line)) = err_line 103 | && line.starts_with("Diagnostic") { 104 | log::error!("Something failed: {line}"); 105 | spotread_proc.exit_logged(false).await; 106 | 107 | app_tx.try_send(PGenAppUpdate::SpotreadStarted(false)).ok(); 108 | break; 109 | } 110 | } 111 | msg = rx.select_next_some() => { 112 | match msg { 113 | SpotreadCmd::DoReading(SpotreadReadingConfig { target, pattern_cfg, pattern_insertion_cfg }) => { 114 | // ready process stdout before sending patch 115 | // because the result must be sent asap and flushing stdout would delay result handling 116 | spotread_proc.read_until_take_reading_ready().await.ok(); 117 | 118 | { 119 | let mut controller = controller_handle.lock().await; 120 | 121 | let wait_duration = if pattern_insertion_cfg.enabled { 122 | let mut inserted_pattern_cfg = pattern_cfg; 123 | pattern_cfg_set_colour_from_float_level(&mut inserted_pattern_cfg, pattern_insertion_cfg.level); 124 | 125 | controller.send_pattern_and_wait(inserted_pattern_cfg, pattern_insertion_cfg.duration).await; 126 | 127 | // Leave more time for the display to adjust after inserted pattern 128 | Duration::from_secs_f64(1.5) 129 | } else { 130 | Duration::from_secs_f64(0.5) 131 | }; 132 | 133 | controller.send_pattern_and_wait(pattern_cfg, wait_duration).await; 134 | } 135 | 136 | let res = tokio::time::timeout(Duration::from_secs(30), spotread_proc.try_measure(target)).await; 137 | 138 | match res { 139 | Ok(res) => { 140 | if let Err(e) = res { 141 | app_tx.try_send(PGenAppUpdate::SpotreadRes(None)).ok(); 142 | log::error!("Failed taking measure {e}"); 143 | } 144 | } 145 | Err(_) => { 146 | log::error!("Timed out trying to measure patch"); 147 | } 148 | } 149 | 150 | external_tx.try_send(ExternalJobCmd::SpotreadDoneMeasuring).ok(); 151 | }, 152 | SpotreadCmd::Exit => { 153 | log::trace!("requested exit"); 154 | spotread_proc.exit_logged(true).await; 155 | 156 | app_tx.try_send(PGenAppUpdate::SpotreadStarted(false)).ok(); 157 | break; 158 | } 159 | } 160 | } 161 | } 162 | } 163 | }); 164 | 165 | Ok(tx) 166 | } 167 | 168 | impl SpotreadProc { 169 | pub fn new( 170 | app_tx: Sender, 171 | cli_args: Vec<(String, Option)>, 172 | ) -> Result { 173 | let args_iter = cli_args 174 | .into_iter() 175 | .flat_map(|kv| once(kv.0).chain(once(kv.1.unwrap_or_default()))) 176 | .filter(|a| !a.is_empty()); 177 | 178 | let mut child = Command::new("spotread") 179 | .args(args_iter) 180 | .env("ARGYLL_NOT_INTERACTIVE", "1") 181 | .stdout(Stdio::piped()) 182 | .stdin(Stdio::piped()) 183 | .stderr(Stdio::piped()) 184 | .kill_on_drop(true) 185 | .spawn()?; 186 | 187 | let child_err = child 188 | .stderr 189 | .take() 190 | .ok_or_else(|| anyhow!("child did not have a handle to stderr"))?; 191 | let err_reader = BufReader::new(child_err); 192 | let err_lines = err_reader.lines(); 193 | 194 | let child_out = child 195 | .stdout 196 | .take() 197 | .ok_or_else(|| anyhow!("child did not have a handle to stdout"))?; 198 | let reader = BufReader::new(child_out); 199 | 200 | let child_in = child 201 | .stdin 202 | .take() 203 | .ok_or_else(|| anyhow!("child did not have a handle to stdin"))?; 204 | let writer = BufWriter::new(child_in); 205 | 206 | Ok(Self { 207 | child, 208 | err_lines, 209 | reader, 210 | read_buf: Vec::with_capacity(1024), 211 | writer, 212 | can_take_reading: false, 213 | app_tx, 214 | }) 215 | } 216 | 217 | async fn try_measure(&mut self, target: CalibrationTarget) -> Result<()> { 218 | self.read_until_take_reading_ready().await?; 219 | 220 | // Take reading by sending enter 221 | self.writer.write_all("\n".as_bytes()).await?; 222 | self.writer.flush().await?; 223 | 224 | let mut line = String::with_capacity(32); 225 | 226 | loop { 227 | line.clear(); 228 | 229 | self.reader.read_line(&mut line).await?; 230 | 231 | log::trace!("Raw output line: {line:?}"); 232 | let final_line = line.trim(); 233 | 234 | if final_line.is_empty() { 235 | continue; 236 | } 237 | 238 | if final_line.starts_with(READING_RESULT_SUBSTR) { 239 | let reading = ReadingResult::from_spotread_result(target, final_line)?; 240 | log::info!("{reading:?}"); 241 | 242 | self.app_tx 243 | .send(PGenAppUpdate::SpotreadRes(Some(reading))) 244 | .await 245 | .ok(); 246 | break; 247 | } else if final_line.starts_with("Spot read failed") { 248 | bail!(final_line.to_string()); 249 | } 250 | } 251 | 252 | self.can_take_reading = false; 253 | 254 | Ok(()) 255 | } 256 | 257 | pub async fn read_until_take_reading_ready(&mut self) -> Result<()> { 258 | if !self.can_take_reading { 259 | self.read_buf.clear(); 260 | 261 | loop { 262 | let buf = self.reader.fill_buf().await?; 263 | let len = buf.len(); 264 | 265 | self.read_buf.extend_from_slice(buf); 266 | self.reader.consume(len); 267 | 268 | let stdout = str::from_utf8(&self.read_buf)?; 269 | 270 | log::trace!("read_until_take_reading_ready[{len}] {stdout:?}"); 271 | 272 | if stdout.trim().ends_with(READING_READY_SUBSTR) { 273 | self.can_take_reading = true; 274 | 275 | log::debug!("ready to take reading"); 276 | 277 | break; 278 | } 279 | } 280 | } 281 | 282 | Ok(()) 283 | } 284 | 285 | async fn exit_logged(self, interactive: bool) { 286 | if let Err(e) = self.exit(interactive).await { 287 | log::error!("Failed exiting program: {e}"); 288 | } else { 289 | log::trace!("process successfully exited"); 290 | } 291 | } 292 | 293 | async fn exit(mut self, interactive: bool) -> Result<()> { 294 | if interactive { 295 | log::trace!("graceful interactive exit"); 296 | 297 | self.read_until_take_reading_ready().await?; 298 | 299 | let mut out = String::with_capacity(32); 300 | 301 | self.writer.write_all("q\r\n".as_bytes()).await?; 302 | self.writer.flush().await?; 303 | 304 | loop { 305 | self.reader.read_line(&mut out).await?; 306 | 307 | if !out.trim().is_empty() { 308 | break; 309 | } 310 | } 311 | 312 | self.writer.write_all("q\r\n".as_bytes()).await?; 313 | self.writer.flush().await?; 314 | 315 | log::trace!("exit output: {out:?}"); 316 | } 317 | 318 | log::trace!("waiting for process to exit"); 319 | let status = self.child.wait().await?; 320 | if status.success() { 321 | Ok(()) 322 | } else { 323 | bail!("process exited with status {status}"); 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/pgen/client.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::time::Duration; 3 | 4 | use anyhow::Result; 5 | use itertools::Itertools; 6 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 7 | use tokio::net::TcpStream; 8 | use tokio::time::timeout; 9 | 10 | use crate::utils::{Rgb, compute_rgb_range}; 11 | 12 | use super::ColorFormat; 13 | use super::commands::{PGenCommand, PGenCommandResponse, PGenGetConfCommand, PGenSetConfCommand}; 14 | use super::pattern_config::PGenPatternConfig; 15 | 16 | const PGEN_CMD_END_BYTE_STR: &str = "\x02\x0D"; 17 | const PGEN_CMD_END_BYTES: &[u8] = PGEN_CMD_END_BYTE_STR.as_bytes(); 18 | 19 | #[derive(Debug)] 20 | pub struct PGenClient { 21 | stream: Option, 22 | socket_addr: SocketAddr, 23 | response_buffer: Vec, 24 | 25 | pub connect_state: ConnectState, 26 | } 27 | 28 | #[derive(Debug, Default, Clone)] 29 | pub struct ConnectState { 30 | pub connected: bool, 31 | pub error: Option, 32 | } 33 | 34 | #[derive(Debug, Clone)] 35 | pub struct PGenTestPattern { 36 | pub format: ColorFormat, 37 | 38 | pub position: (u16, u16), 39 | pub patch_size: (u16, u16), 40 | 41 | pub bit_depth: u8, 42 | 43 | // In 10 bit range 44 | pub rgb: Rgb, 45 | pub bg_rgb: Rgb, 46 | } 47 | 48 | impl PGenClient { 49 | pub fn new(socket_addr: SocketAddr) -> Self { 50 | Self { 51 | stream: None, 52 | socket_addr, 53 | response_buffer: vec![0; 8192], 54 | connect_state: Default::default(), 55 | } 56 | } 57 | 58 | pub fn set_socket_address(&mut self, socket_addr: &SocketAddr) { 59 | self.connect_state.connected = false; 60 | self.socket_addr.set_ip(socket_addr.ip()); 61 | self.socket_addr.set_port(socket_addr.port()); 62 | } 63 | 64 | pub async fn update_socket_address_and_connect( 65 | &mut self, 66 | socket_addr: &SocketAddr, 67 | ) -> PGenCommandResponse { 68 | self.set_socket_address(socket_addr); 69 | self.connect().await 70 | } 71 | 72 | async fn send_tcp_command(&mut self, cmd: &str) -> Result { 73 | if self.stream.is_none() { 74 | return Ok(String::from("Not connected to TCP socket")); 75 | } 76 | 77 | log::debug!("Sending command {}", cmd); 78 | 79 | let stream = self.stream.as_mut().unwrap(); 80 | stream 81 | .write_all(format!("{cmd}{PGEN_CMD_END_BYTE_STR}").as_bytes()) 82 | .await?; 83 | 84 | let res_bytes = timeout(Duration::from_secs(10), async { 85 | let mut n = 0; 86 | let mut correct_end_bytes = false; 87 | 88 | while let Ok(read_bytes) = stream.read(&mut self.response_buffer[n..]).await { 89 | n += read_bytes; 90 | if read_bytes == 0 { 91 | break; 92 | } else { 93 | correct_end_bytes = 94 | matches!(&self.response_buffer[n - 2..n], PGEN_CMD_END_BYTES); 95 | if correct_end_bytes { 96 | break; 97 | } 98 | } 99 | } 100 | 101 | if correct_end_bytes { 102 | &self.response_buffer[..n - 2] 103 | } else { 104 | &self.response_buffer[..n] 105 | } 106 | }) 107 | .await?; 108 | 109 | let response = String::from_utf8_lossy(res_bytes).to_string(); 110 | log::trace!(" Response: {response}"); 111 | 112 | Ok(response) 113 | } 114 | 115 | async fn send_heartbeat(&mut self) -> PGenCommandResponse { 116 | let is_alive = if let Ok(res) = self.send_tcp_command("IS_ALIVE").await { 117 | res == "ALIVE" 118 | } else { 119 | false 120 | }; 121 | 122 | self.connect_state.connected = is_alive; 123 | 124 | PGenCommandResponse::Alive(is_alive) 125 | } 126 | 127 | pub async fn set_stream(&mut self) -> Result<()> { 128 | if self.stream.is_some() { 129 | self.disconnect().await; 130 | } 131 | 132 | let stream = timeout( 133 | Duration::from_secs(10), 134 | TcpStream::connect(self.socket_addr), 135 | ) 136 | .await??; 137 | self.stream = Some(stream); 138 | 139 | let stream = self.stream.as_mut().unwrap(); 140 | log::info!("Successfully connected to {}", &stream.peer_addr()?); 141 | 142 | Ok(()) 143 | } 144 | 145 | async fn connect(&mut self) -> PGenCommandResponse { 146 | self.connect_state.connected = false; 147 | self.connect_state.error = None; 148 | log::info!("Connecting to {}", self.socket_addr); 149 | 150 | let res = if !self.connect_state.connected { 151 | self.set_stream().await 152 | } else { 153 | log::trace!("Already connected, requesting heartbeat"); 154 | Ok(()) 155 | }; 156 | 157 | let PGenCommandResponse::Alive(is_alive) = self.send_heartbeat().await else { 158 | unreachable!() 159 | }; 160 | 161 | self.connect_state.connected = is_alive; 162 | if let Err(e) = res { 163 | self.connect_state.error = Some(e.to_string()); 164 | } 165 | 166 | PGenCommandResponse::Connect(self.connect_state.clone()) 167 | } 168 | 169 | async fn disconnect(&mut self) -> PGenCommandResponse { 170 | let res = if self.connect_state.connected { 171 | self.send_tcp_command("QUIT") 172 | .await 173 | .map(|res| !res.is_empty()) 174 | } else { 175 | log::trace!("Already disconnected"); 176 | Ok(false) 177 | }; 178 | 179 | match res { 180 | Ok(still_connected) => { 181 | self.connect_state.connected = still_connected; 182 | 183 | if still_connected { 184 | log::error!("Failed disconnecting connection"); 185 | } else { 186 | self.stream = None; 187 | } 188 | } 189 | Err(e) => self.connect_state.error = Some(e.to_string()), 190 | }; 191 | 192 | PGenCommandResponse::Quit(self.connect_state.clone()) 193 | } 194 | 195 | async fn shutdown_device(&mut self) -> PGenCommandResponse { 196 | let res = self 197 | .send_tcp_command("CMD:HALT") 198 | .await 199 | .map(|res| res == "OK:"); 200 | 201 | match res { 202 | Ok(res) => { 203 | if res { 204 | self.connect_state.connected = false; 205 | self.stream = None; 206 | } 207 | } 208 | Err(e) => self.connect_state.error = Some(e.to_string()), 209 | }; 210 | 211 | PGenCommandResponse::Shutdown(self.connect_state.clone()) 212 | } 213 | 214 | async fn reboot_device(&mut self) -> PGenCommandResponse { 215 | let res = self 216 | .send_tcp_command("CMD:REBOOT") 217 | .await 218 | .map(|res| res == "OK:"); 219 | 220 | match &res { 221 | Ok(res) => { 222 | if *res { 223 | self.connect_state.connected = false; 224 | self.stream = None; 225 | } 226 | } 227 | Err(e) => self.connect_state.error = Some(e.to_string()), 228 | }; 229 | 230 | PGenCommandResponse::Reboot(self.connect_state.clone()) 231 | } 232 | 233 | async fn restart_software(&mut self) -> PGenCommandResponse { 234 | PGenCommandResponse::Ok(self.send_tcp_command("RESTARTPGENERATOR:").await.is_ok()) 235 | } 236 | 237 | async fn send_test_pattern(&mut self, test_pattern: &PGenTestPattern) -> PGenCommandResponse { 238 | let rect = if test_pattern.bit_depth == 8 { 239 | "RECTANGLE" 240 | } else { 241 | "RECTANGLE10bit" 242 | }; 243 | 244 | let (w, h) = test_pattern.patch_size; 245 | let (x, y) = test_pattern.position; 246 | let mut bg_rgb = test_pattern.bg_rgb; 247 | let [r, g, b] = test_pattern.rgb; 248 | 249 | // Only RGB supports 10 bit backgrounds, 8 bit otherwise 250 | if test_pattern.bit_depth == 10 && !matches!(test_pattern.format, ColorFormat::Rgb) { 251 | bg_rgb.iter_mut().for_each(|c| { 252 | let cf = *c as f64 / 2.0_f64.powf(2.0); 253 | *c = cf.round() as u16; 254 | }); 255 | } 256 | 257 | let [bg_r, bg_b, bg_g] = bg_rgb; 258 | 259 | let cmd = format!("RGB={rect};{w},{h};0;{r},{g},{b};{bg_r},{bg_b},{bg_g};0,0,{x},{y};-1"); 260 | 261 | log::info!("Sent pattern RGB: [{r}, {g}, {b}], background: [{bg_r}, {bg_g}, {bg_b}]"); 262 | PGenCommandResponse::Ok(self.send_tcp_command(&cmd).await.is_ok()) 263 | } 264 | 265 | pub async fn send_multiple_get_conf_commands( 266 | &mut self, 267 | commands: &[PGenGetConfCommand], 268 | ) -> PGenCommandResponse { 269 | let commands_str = commands.iter().map(|c| c.as_ref()).join(":"); 270 | let cmd = format!("CMD:MULTIPLE:{commands_str}"); 271 | 272 | let res = self.send_tcp_command(&cmd).await; 273 | 274 | match res { 275 | Ok(res) => { 276 | // Skip `OK:\n` 277 | let command_results = res.split_terminator('\n').skip(1); 278 | let paired_results = command_results 279 | .zip(commands.iter().copied()) 280 | .map(|(res, c)| (c, res.to_owned())) 281 | .collect(); 282 | PGenCommandResponse::MultipleGetConfRes(paired_results) 283 | } 284 | Err(e) => { 285 | let err_str = e.to_string(); 286 | self.connect_state.error = Some(err_str.clone()); 287 | PGenCommandResponse::Errored(err_str) 288 | } 289 | } 290 | } 291 | 292 | pub async fn send_multiple_set_conf_commands( 293 | &mut self, 294 | commands: &[PGenSetConfCommand], 295 | ) -> PGenCommandResponse { 296 | let mut ret = Vec::with_capacity(commands.len()); 297 | 298 | for cmd in commands.iter().copied() { 299 | let cmd_str = format!("CMD:{}:{}", cmd.as_ref(), cmd.value()); 300 | let res = self 301 | .send_tcp_command(&cmd_str) 302 | .await 303 | .map(|res| res == "OK:"); 304 | match res { 305 | Ok(res) => ret.push((cmd, res)), 306 | Err(e) => { 307 | let err_str = e.to_string(); 308 | self.connect_state.error = Some(err_str.clone()); 309 | return PGenCommandResponse::Errored(err_str); 310 | } 311 | } 312 | } 313 | 314 | PGenCommandResponse::MultipleSetConfRes(ret) 315 | } 316 | 317 | pub async fn send_generic_command(&mut self, cmd: PGenCommand) -> PGenCommandResponse { 318 | if self.stream.is_none() && !matches!(cmd, PGenCommand::Connect) { 319 | match cmd { 320 | PGenCommand::UpdateSocket(socket_addr) => { 321 | self.set_socket_address(&socket_addr); 322 | return PGenCommandResponse::Ok(true); 323 | } 324 | _ => return PGenCommandResponse::NotConnected, 325 | } 326 | } 327 | 328 | match cmd { 329 | PGenCommand::IsAlive => self.send_heartbeat().await, 330 | PGenCommand::Connect => self.connect().await, 331 | PGenCommand::Quit => self.disconnect().await, 332 | PGenCommand::Shutdown => self.shutdown_device().await, 333 | PGenCommand::Reboot => self.reboot_device().await, 334 | PGenCommand::RestartSoftware => self.restart_software().await, 335 | PGenCommand::UpdateSocket(socket_addr) => { 336 | self.update_socket_address_and_connect(&socket_addr).await 337 | } 338 | PGenCommand::TestPattern(pattern) => self.send_test_pattern(&pattern).await, 339 | PGenCommand::MultipleGetConfCommands(commands) => { 340 | self.send_multiple_get_conf_commands(commands).await 341 | } 342 | PGenCommand::MultipleSetConfCommands(commands) => { 343 | self.send_multiple_set_conf_commands(&commands).await 344 | } 345 | } 346 | } 347 | } 348 | 349 | impl PGenTestPattern { 350 | pub fn from_config(format: ColorFormat, cfg: &PGenPatternConfig) -> Self { 351 | Self { 352 | format, 353 | position: cfg.position, 354 | patch_size: cfg.patch_size, 355 | bit_depth: cfg.bit_depth as u8, 356 | rgb: cfg.patch_colour, 357 | bg_rgb: cfg.background_colour, 358 | } 359 | } 360 | 361 | pub fn blank(format: ColorFormat, cfg: &PGenPatternConfig) -> Self { 362 | let rgb_range = compute_rgb_range(cfg.limited_range, cfg.bit_depth as u8); 363 | let rgb = [*rgb_range.start(); 3]; 364 | let bg_rgb = rgb; 365 | 366 | Self { 367 | format, 368 | position: cfg.position, 369 | patch_size: cfg.patch_size, 370 | bit_depth: cfg.bit_depth as u8, 371 | rgb, 372 | bg_rgb, 373 | } 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /data/CIE_cc_1931_2deg.csv: -------------------------------------------------------------------------------- 1 | 360,0.17556,0.00529,0.81915 2 | 361,0.17548,0.00529,0.81923 3 | 362,0.17540,0.00528,0.81932 4 | 363,0.17532,0.00527,0.81941 5 | 364,0.17524,0.00526,0.81950 6 | 365,0.17516,0.00526,0.81958 7 | 366,0.17509,0.00525,0.81966 8 | 367,0.17501,0.00524,0.81975 9 | 368,0.17494,0.00523,0.81983 10 | 369,0.17488,0.00522,0.81990 11 | 370,0.17482,0.00522,0.81996 12 | 371,0.17477,0.00523,0.82000 13 | 372,0.17472,0.00524,0.82004 14 | 373,0.17466,0.00524,0.82010 15 | 374,0.17459,0.00522,0.82019 16 | 375,0.17451,0.00518,0.82031 17 | 376,0.17441,0.00513,0.82046 18 | 377,0.17431,0.00507,0.82062 19 | 378,0.17422,0.00502,0.82076 20 | 379,0.17416,0.00498,0.82086 21 | 380,0.17411,0.00496,0.82093 22 | 381,0.17409,0.00496,0.82095 23 | 382,0.17407,0.00497,0.82096 24 | 383,0.17406,0.00498,0.82096 25 | 384,0.17404,0.00498,0.82098 26 | 385,0.17401,0.00498,0.82101 27 | 386,0.17397,0.00497,0.82106 28 | 387,0.17393,0.00494,0.82113 29 | 388,0.17389,0.00493,0.82118 30 | 389,0.17384,0.00492,0.82124 31 | 390,0.17380,0.00492,0.82128 32 | 391,0.17376,0.00492,0.82132 33 | 392,0.17370,0.00494,0.82136 34 | 393,0.17366,0.00494,0.82140 35 | 394,0.17361,0.00494,0.82145 36 | 395,0.17356,0.00492,0.82152 37 | 396,0.17351,0.00490,0.82159 38 | 397,0.17347,0.00486,0.82167 39 | 398,0.17342,0.00484,0.82174 40 | 399,0.17338,0.00481,0.82181 41 | 400,0.17334,0.00480,0.82186 42 | 401,0.17329,0.00479,0.82192 43 | 402,0.17324,0.00478,0.82198 44 | 403,0.17317,0.00478,0.82205 45 | 404,0.17310,0.00477,0.82213 46 | 405,0.17302,0.00478,0.82220 47 | 406,0.17293,0.00478,0.82229 48 | 407,0.17284,0.00479,0.82237 49 | 408,0.17275,0.00480,0.82245 50 | 409,0.17266,0.00480,0.82254 51 | 410,0.17258,0.00480,0.82262 52 | 411,0.17249,0.00480,0.82271 53 | 412,0.17239,0.00480,0.82281 54 | 413,0.17230,0.00480,0.82290 55 | 414,0.17219,0.00482,0.82299 56 | 415,0.17209,0.00483,0.82308 57 | 416,0.17198,0.00486,0.82316 58 | 417,0.17187,0.00489,0.82324 59 | 418,0.17174,0.00494,0.82332 60 | 419,0.17159,0.00501,0.82340 61 | 420,0.17141,0.00510,0.82349 62 | 421,0.17121,0.00521,0.82358 63 | 422,0.17099,0.00533,0.82368 64 | 423,0.17077,0.00547,0.82376 65 | 424,0.17054,0.00562,0.82384 66 | 425,0.17030,0.00579,0.82391 67 | 426,0.17005,0.00597,0.82398 68 | 427,0.16978,0.00618,0.82404 69 | 428,0.16950,0.00640,0.82410 70 | 429,0.16920,0.00664,0.82416 71 | 430,0.16888,0.00690,0.82422 72 | 431,0.16853,0.00718,0.82429 73 | 432,0.16815,0.00749,0.82436 74 | 433,0.16775,0.00782,0.82443 75 | 434,0.16733,0.00817,0.82450 76 | 435,0.16690,0.00855,0.82455 77 | 436,0.16645,0.00896,0.82459 78 | 437,0.16598,0.00940,0.82462 79 | 438,0.16548,0.00987,0.82465 80 | 439,0.16496,0.01035,0.82469 81 | 440,0.16441,0.01086,0.82473 82 | 441,0.16383,0.01138,0.82479 83 | 442,0.16321,0.01194,0.82485 84 | 443,0.16255,0.01252,0.82493 85 | 444,0.16185,0.01314,0.82501 86 | 445,0.16111,0.01379,0.82510 87 | 446,0.16031,0.01449,0.82520 88 | 447,0.15947,0.01523,0.82530 89 | 448,0.15857,0.01602,0.82541 90 | 449,0.15763,0.01684,0.82553 91 | 450,0.15664,0.01771,0.82565 92 | 451,0.15560,0.01861,0.82579 93 | 452,0.15452,0.01956,0.82592 94 | 453,0.15340,0.02055,0.82605 95 | 454,0.15222,0.02161,0.82617 96 | 455,0.15099,0.02274,0.82627 97 | 456,0.14969,0.02395,0.82636 98 | 457,0.14834,0.02525,0.82641 99 | 458,0.14693,0.02663,0.82644 100 | 459,0.14547,0.02812,0.82641 101 | 460,0.14396,0.02970,0.82634 102 | 461,0.14241,0.03139,0.82620 103 | 462,0.14080,0.03321,0.82599 104 | 463,0.13912,0.03520,0.82568 105 | 464,0.13737,0.03740,0.82523 106 | 465,0.13550,0.03988,0.82462 107 | 466,0.13351,0.04269,0.82380 108 | 467,0.13137,0.04588,0.82275 109 | 468,0.12909,0.04945,0.82146 110 | 469,0.12666,0.05343,0.81991 111 | 470,0.12412,0.05780,0.81808 112 | 471,0.12147,0.06259,0.81594 113 | 472,0.11870,0.06783,0.81347 114 | 473,0.11581,0.07358,0.81061 115 | 474,0.11278,0.07989,0.80733 116 | 475,0.10960,0.08684,0.80356 117 | 476,0.10626,0.09449,0.79925 118 | 477,0.10278,0.10286,0.79436 119 | 478,0.09913,0.11201,0.78886 120 | 479,0.09531,0.12194,0.78275 121 | 480,0.09129,0.13270,0.77601 122 | 481,0.08708,0.14432,0.76860 123 | 482,0.08268,0.15687,0.76045 124 | 483,0.07812,0.17042,0.75146 125 | 484,0.07344,0.18503,0.74153 126 | 485,0.06871,0.20072,0.73057 127 | 486,0.06399,0.21747,0.71854 128 | 487,0.05932,0.23525,0.70543 129 | 488,0.05467,0.25409,0.69124 130 | 489,0.05003,0.27400,0.67597 131 | 490,0.04539,0.29498,0.65963 132 | 491,0.04076,0.31698,0.64226 133 | 492,0.03620,0.33990,0.62390 134 | 493,0.03176,0.36360,0.60464 135 | 494,0.02749,0.38792,0.58459 136 | 495,0.02346,0.41270,0.56384 137 | 496,0.01970,0.43776,0.54254 138 | 497,0.01627,0.46295,0.52078 139 | 498,0.01318,0.48821,0.49861 140 | 499,0.01048,0.51340,0.47612 141 | 500,0.00817,0.53842,0.45341 142 | 501,0.00628,0.56307,0.43065 143 | 502,0.00487,0.58712,0.40801 144 | 503,0.00398,0.61045,0.38557 145 | 504,0.00364,0.63301,0.36335 146 | 505,0.00386,0.65482,0.34132 147 | 506,0.00464,0.67590,0.31946 148 | 507,0.00601,0.69612,0.29787 149 | 508,0.00799,0.71534,0.27667 150 | 509,0.01060,0.73341,0.25599 151 | 510,0.01387,0.75019,0.23594 152 | 511,0.01777,0.76561,0.21662 153 | 512,0.02224,0.77963,0.19813 154 | 513,0.02727,0.79211,0.18062 155 | 514,0.03282,0.80293,0.16425 156 | 515,0.03885,0.81202,0.14913 157 | 516,0.04533,0.81939,0.13528 158 | 517,0.05218,0.82516,0.12266 159 | 518,0.05932,0.82943,0.11125 160 | 519,0.06672,0.83227,0.10101 161 | 520,0.07430,0.83380,0.09190 162 | 521,0.08205,0.83409,0.08386 163 | 522,0.08994,0.83329,0.07677 164 | 523,0.09794,0.83159,0.07047 165 | 524,0.10602,0.82918,0.06480 166 | 525,0.11416,0.82621,0.05963 167 | 526,0.12235,0.82277,0.05488 168 | 527,0.13055,0.81893,0.05052 169 | 528,0.13870,0.81478,0.04652 170 | 529,0.14677,0.81040,0.04283 171 | 530,0.15472,0.80586,0.03942 172 | 531,0.16253,0.80124,0.03623 173 | 532,0.17024,0.79652,0.03324 174 | 533,0.17785,0.79169,0.03046 175 | 534,0.18539,0.78673,0.02788 176 | 535,0.19288,0.78163,0.02549 177 | 536,0.20031,0.77640,0.02329 178 | 537,0.20769,0.77105,0.02126 179 | 538,0.21503,0.76559,0.01938 180 | 539,0.22234,0.76002,0.01764 181 | 540,0.22962,0.75433,0.01605 182 | 541,0.23689,0.74852,0.01459 183 | 542,0.24413,0.74262,0.01325 184 | 543,0.25136,0.73661,0.01203 185 | 544,0.25858,0.73051,0.01091 186 | 545,0.26578,0.72432,0.00990 187 | 546,0.27296,0.71806,0.00898 188 | 547,0.28013,0.71172,0.00815 189 | 548,0.28729,0.70532,0.00739 190 | 549,0.29445,0.69884,0.00671 191 | 550,0.30160,0.69231,0.00609 192 | 551,0.30876,0.68571,0.00553 193 | 552,0.31592,0.67906,0.00502 194 | 553,0.32306,0.67237,0.00457 195 | 554,0.33021,0.66563,0.00416 196 | 555,0.33736,0.65885,0.00379 197 | 556,0.34451,0.65203,0.00346 198 | 557,0.35167,0.64517,0.00316 199 | 558,0.35881,0.63829,0.00290 200 | 559,0.36596,0.63138,0.00266 201 | 560,0.37310,0.62445,0.00245 202 | 561,0.38024,0.61750,0.00226 203 | 562,0.38738,0.61054,0.00208 204 | 563,0.39451,0.60357,0.00192 205 | 564,0.40163,0.59659,0.00178 206 | 565,0.40873,0.58961,0.00166 207 | 566,0.41583,0.58262,0.00155 208 | 567,0.42292,0.57563,0.00145 209 | 568,0.42999,0.56865,0.00136 210 | 569,0.43704,0.56167,0.00129 211 | 570,0.44406,0.55472,0.00122 212 | 571,0.45106,0.54777,0.00117 213 | 572,0.45804,0.54084,0.00112 214 | 573,0.46499,0.53393,0.00108 215 | 574,0.47190,0.52705,0.00105 216 | 575,0.47878,0.52020,0.00102 217 | 576,0.48561,0.51339,0.00100 218 | 577,0.49241,0.50661,0.00098 219 | 578,0.49915,0.49989,0.00096 220 | 579,0.50585,0.49321,0.00094 221 | 580,0.51249,0.48659,0.00092 222 | 581,0.51907,0.48003,0.00090 223 | 582,0.52560,0.47353,0.00087 224 | 583,0.53207,0.46709,0.00084 225 | 584,0.53846,0.46073,0.00081 226 | 585,0.54479,0.45443,0.00078 227 | 586,0.55103,0.44823,0.00074 228 | 587,0.55719,0.44210,0.00071 229 | 588,0.56327,0.43606,0.00067 230 | 589,0.56926,0.43010,0.00064 231 | 590,0.57515,0.42423,0.00062 232 | 591,0.58094,0.41846,0.00060 233 | 592,0.58665,0.41276,0.00059 234 | 593,0.59222,0.40719,0.00059 235 | 594,0.59766,0.40176,0.00058 236 | 595,0.60293,0.39650,0.00057 237 | 596,0.60803,0.39141,0.00056 238 | 597,0.61298,0.38648,0.00054 239 | 598,0.61778,0.38171,0.00051 240 | 599,0.62246,0.37705,0.00049 241 | 600,0.62704,0.37249,0.00047 242 | 601,0.63152,0.36803,0.00045 243 | 602,0.63590,0.36367,0.00043 244 | 603,0.64016,0.35943,0.00041 245 | 604,0.64427,0.35533,0.00040 246 | 605,0.64823,0.35140,0.00037 247 | 606,0.65203,0.34763,0.00034 248 | 607,0.65567,0.34402,0.00031 249 | 608,0.65917,0.34055,0.00028 250 | 609,0.66253,0.33722,0.00025 251 | 610,0.66576,0.33401,0.00023 252 | 611,0.66887,0.33092,0.00021 253 | 612,0.67186,0.32795,0.00019 254 | 613,0.67472,0.32509,0.00019 255 | 614,0.67746,0.32236,0.00018 256 | 615,0.68008,0.31975,0.00017 257 | 616,0.68258,0.31725,0.00017 258 | 617,0.68497,0.31486,0.00017 259 | 618,0.68725,0.31259,0.00016 260 | 619,0.68943,0.31041,0.00016 261 | 620,0.69151,0.30834,0.00015 262 | 621,0.69349,0.30637,0.00014 263 | 622,0.69539,0.30448,0.00013 264 | 623,0.69721,0.30267,0.00012 265 | 624,0.69894,0.30095,0.00011 266 | 625,0.70061,0.29930,0.00009 267 | 626,0.70219,0.29773,0.00008 268 | 627,0.70371,0.29622,0.00007 269 | 628,0.70516,0.29477,0.00007 270 | 629,0.70656,0.29338,0.00006 271 | 630,0.70792,0.29203,0.00005 272 | 631,0.70923,0.29072,0.00005 273 | 632,0.71050,0.28945,0.00005 274 | 633,0.71173,0.28823,0.00004 275 | 634,0.71290,0.28706,0.00004 276 | 635,0.71403,0.28593,0.00004 277 | 636,0.71512,0.28484,0.00004 278 | 637,0.71616,0.28380,0.00004 279 | 638,0.71716,0.28281,0.00003 280 | 639,0.71812,0.28185,0.00003 281 | 640,0.71903,0.28094,0.00003 282 | 641,0.71991,0.28006,0.00003 283 | 642,0.72075,0.27922,0.00003 284 | 643,0.72155,0.27842,0.00003 285 | 644,0.72232,0.27766,0.00002 286 | 645,0.72303,0.27695,0.00002 287 | 646,0.72370,0.27628,0.00002 288 | 647,0.72433,0.27566,0.00001 289 | 648,0.72491,0.27508,0.00001 290 | 649,0.72547,0.27453,0.00000 291 | 650,0.72599,0.27401,0.00000 292 | 651,0.72649,0.27351,0.00000 293 | 652,0.72698,0.27302,0.00000 294 | 653,0.72743,0.27257,0.00000 295 | 654,0.72786,0.27214,0.00000 296 | 655,0.72827,0.27173,0.00000 297 | 656,0.72866,0.27134,0.00000 298 | 657,0.72902,0.27098,0.00000 299 | 658,0.72936,0.27064,0.00000 300 | 659,0.72968,0.27032,0.00000 301 | 660,0.72997,0.27003,0.00000 302 | 661,0.73023,0.26977,0.00000 303 | 662,0.73047,0.26953,0.00000 304 | 663,0.73069,0.26931,0.00000 305 | 664,0.73090,0.26910,0.00000 306 | 665,0.73109,0.26891,0.00000 307 | 666,0.73128,0.26872,0.00000 308 | 667,0.73147,0.26853,0.00000 309 | 668,0.73165,0.26835,0.00000 310 | 669,0.73183,0.26817,0.00000 311 | 670,0.73199,0.26801,0.00000 312 | 671,0.73215,0.26785,0.00000 313 | 672,0.73230,0.26770,0.00000 314 | 673,0.73244,0.26756,0.00000 315 | 674,0.73258,0.26742,0.00000 316 | 675,0.73272,0.26728,0.00000 317 | 676,0.73286,0.26714,0.00000 318 | 677,0.73300,0.26700,0.00000 319 | 678,0.73314,0.26686,0.00000 320 | 679,0.73328,0.26672,0.00000 321 | 680,0.73342,0.26658,0.00000 322 | 681,0.73355,0.26645,0.00000 323 | 682,0.73368,0.26632,0.00000 324 | 683,0.73381,0.26619,0.00000 325 | 684,0.73394,0.26606,0.00000 326 | 685,0.73405,0.26595,0.00000 327 | 686,0.73414,0.26586,0.00000 328 | 687,0.73422,0.26578,0.00000 329 | 688,0.73429,0.26571,0.00000 330 | 689,0.73434,0.26566,0.00000 331 | 690,0.73439,0.26561,0.00000 332 | 691,0.73444,0.26556,0.00000 333 | 692,0.73448,0.26552,0.00000 334 | 693,0.73452,0.26548,0.00000 335 | 694,0.73456,0.26544,0.00000 336 | 695,0.73459,0.26541,0.00000 337 | 696,0.73462,0.26538,0.00000 338 | 697,0.73465,0.26535,0.00000 339 | 698,0.73467,0.26533,0.00000 340 | 699,0.73469,0.26531,0.00000 341 | 700,0.73469,0.26531,0.00000 342 | 701,0.73469,0.26531,0.00000 343 | 702,0.73469,0.26531,0.00000 344 | 703,0.73469,0.26531,0.00000 345 | 704,0.73469,0.26531,0.00000 346 | 705,0.73469,0.26531,0.00000 347 | 706,0.73469,0.26531,0.00000 348 | 707,0.73469,0.26531,0.00000 349 | 708,0.73469,0.26531,0.00000 350 | 709,0.73469,0.26531,0.00000 351 | 710,0.73469,0.26531,0.00000 352 | 711,0.73469,0.26531,0.00000 353 | 712,0.73469,0.26531,0.00000 354 | 713,0.73469,0.26531,0.00000 355 | 714,0.73469,0.26531,0.00000 356 | 715,0.73469,0.26531,0.00000 357 | 716,0.73469,0.26531,0.00000 358 | 717,0.73469,0.26531,0.00000 359 | 718,0.73469,0.26531,0.00000 360 | 719,0.73469,0.26531,0.00000 361 | 720,0.73469,0.26531,0.00000 362 | 721,0.73469,0.26531,0.00000 363 | 722,0.73469,0.26531,0.00000 364 | 723,0.73469,0.26531,0.00000 365 | 724,0.73469,0.26531,0.00000 366 | 725,0.73469,0.26531,0.00000 367 | 726,0.73469,0.26531,0.00000 368 | 727,0.73469,0.26531,0.00000 369 | 728,0.73469,0.26531,0.00000 370 | 729,0.73469,0.26531,0.00000 371 | 730,0.73469,0.26531,0.00000 372 | 731,0.73469,0.26531,0.00000 373 | 732,0.73469,0.26531,0.00000 374 | 733,0.73469,0.26531,0.00000 375 | 734,0.73469,0.26531,0.00000 376 | 735,0.73469,0.26531,0.00000 377 | 736,0.73469,0.26531,0.00000 378 | 737,0.73469,0.26531,0.00000 379 | 738,0.73469,0.26531,0.00000 380 | 739,0.73469,0.26531,0.00000 381 | 740,0.73469,0.26531,0.00000 382 | 741,0.73469,0.26531,0.00000 383 | 742,0.73469,0.26531,0.00000 384 | 743,0.73469,0.26531,0.00000 385 | 744,0.73469,0.26531,0.00000 386 | 745,0.73469,0.26531,0.00000 387 | 746,0.73469,0.26531,0.00000 388 | 747,0.73469,0.26531,0.00000 389 | 748,0.73469,0.26531,0.00000 390 | 749,0.73469,0.26531,0.00000 391 | 750,0.73469,0.26531,0.00000 392 | 751,0.73469,0.26531,0.00000 393 | 752,0.73469,0.26531,0.00000 394 | 753,0.73469,0.26531,0.00000 395 | 754,0.73469,0.26531,0.00000 396 | 755,0.73469,0.26531,0.00000 397 | 756,0.73469,0.26531,0.00000 398 | 757,0.73469,0.26531,0.00000 399 | 758,0.73469,0.26531,0.00000 400 | 759,0.73469,0.26531,0.00000 401 | 760,0.73469,0.26531,0.00000 402 | 761,0.73469,0.26531,0.00000 403 | 762,0.73469,0.26531,0.00000 404 | 763,0.73469,0.26531,0.00000 405 | 764,0.73469,0.26531,0.00000 406 | 765,0.73469,0.26531,0.00000 407 | 766,0.73469,0.26531,0.00000 408 | 767,0.73469,0.26531,0.00000 409 | 768,0.73469,0.26531,0.00000 410 | 769,0.73469,0.26531,0.00000 411 | 770,0.73469,0.26531,0.00000 412 | 771,0.73469,0.26531,0.00000 413 | 772,0.73469,0.26531,0.00000 414 | 773,0.73469,0.26531,0.00000 415 | 774,0.73469,0.26531,0.00000 416 | 775,0.73469,0.26531,0.00000 417 | 776,0.73469,0.26531,0.00000 418 | 777,0.73469,0.26531,0.00000 419 | 778,0.73469,0.26531,0.00000 420 | 779,0.73469,0.26531,0.00000 421 | 780,0.73469,0.26531,0.00000 422 | 781,0.73469,0.26531,0.00000 423 | 782,0.73469,0.26531,0.00000 424 | 783,0.73469,0.26531,0.00000 425 | 784,0.73469,0.26531,0.00000 426 | 785,0.73469,0.26531,0.00000 427 | 786,0.73469,0.26531,0.00000 428 | 787,0.73469,0.26531,0.00000 429 | 788,0.73469,0.26531,0.00000 430 | 789,0.73469,0.26531,0.00000 431 | 790,0.73469,0.26531,0.00000 432 | 791,0.73469,0.26531,0.00000 433 | 792,0.73469,0.26531,0.00000 434 | 793,0.73469,0.26531,0.00000 435 | 794,0.73469,0.26531,0.00000 436 | 795,0.73469,0.26531,0.00000 437 | 796,0.73469,0.26531,0.00000 438 | 797,0.73469,0.26531,0.00000 439 | 798,0.73469,0.26531,0.00000 440 | 799,0.73469,0.26531,0.00000 441 | 800,0.73469,0.26531,0.00000 442 | 801,0.73469,0.26531,0.00000 443 | 802,0.73469,0.26531,0.00000 444 | 803,0.73469,0.26531,0.00000 445 | 804,0.73469,0.26531,0.00000 446 | 805,0.73469,0.26531,0.00000 447 | 806,0.73469,0.26531,0.00000 448 | 807,0.73469,0.26531,0.00000 449 | 808,0.73469,0.26531,0.00000 450 | 809,0.73469,0.26531,0.00000 451 | 810,0.73469,0.26531,0.00000 452 | 811,0.73469,0.26531,0.00000 453 | 812,0.73469,0.26531,0.00000 454 | 813,0.73469,0.26531,0.00000 455 | 814,0.73469,0.26531,0.00000 456 | 815,0.73469,0.26531,0.00000 457 | 816,0.73469,0.26531,0.00000 458 | 817,0.73469,0.26531,0.00000 459 | 818,0.73469,0.26531,0.00000 460 | 819,0.73469,0.26531,0.00000 461 | 820,0.73469,0.26531,0.00000 462 | 821,0.73469,0.26531,0.00000 463 | 822,0.73469,0.26531,0.00000 464 | 823,0.73469,0.26531,0.00000 465 | 824,0.73469,0.26531,0.00000 466 | 825,0.73469,0.26531,0.00000 467 | 826,0.73469,0.26531,0.00000 468 | 827,0.73469,0.26531,0.00000 469 | 828,0.73469,0.26531,0.00000 470 | 829,0.73469,0.26531,0.00000 471 | 830,0.73469,0.26531,0.00000 472 | -------------------------------------------------------------------------------- /src/calibration/reading_result.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow, bail}; 2 | use deltae::{DEMethod::DE2000, Delta, DeltaE}; 3 | use itertools::Itertools; 4 | use kolor_64::{ 5 | ColorConversion, Vec3, 6 | details::{ 7 | color::WhitePoint, 8 | transform::{self, XYZ_to_CIELAB, XYZ_to_xyY}, 9 | }, 10 | }; 11 | use once_cell::sync::Lazy; 12 | use regex::Regex; 13 | 14 | use crate::utils::round_colour; 15 | 16 | use super::{CalibrationTarget, LuminanceEotf, MyLab, xyz_to_cct}; 17 | 18 | static RESULT_XYZ_REGEX: Lazy = 19 | Lazy::new(|| Regex::new(r"XYZ:\s(-?\d+\.\d+)\s(-?\d+\.\d+)\s(-?\d+\.\d+)").unwrap()); 20 | static RESULT_LAB_REGEX: Lazy = 21 | Lazy::new(|| Regex::new(r"Lab:\s(-?\d+\.\d+)\s(?-?\d+\.\d+)\s(?-?\d+\.\d+)").unwrap()); 22 | 23 | #[derive(Debug, Default, Clone, Copy)] 24 | pub struct ReadingResult { 25 | pub target: CalibrationTarget, 26 | // From sample, ArgyllCMS spotread value 27 | pub xyz: Vec3, 28 | pub argyll_lab: Vec3, 29 | 30 | // Calculated from XYZ, D65 31 | pub xyy: Vec3, 32 | pub lab: Vec3, 33 | pub cct: f64, 34 | 35 | // Gamma RGB relative to display peak 36 | // Calculated from the target primaries 37 | pub rgb: Vec3, 38 | } 39 | 40 | impl ReadingResult { 41 | pub fn from_spotread_result(target: CalibrationTarget, line: &str) -> Result { 42 | let res = RESULT_XYZ_REGEX.captures(line).and_then(|xyz_caps| { 43 | RESULT_LAB_REGEX 44 | .captures(line) 45 | .map(|lab_caps| (xyz_caps, lab_caps)) 46 | }); 47 | if res.is_none() { 48 | bail!("Failed parsing spotread result: {line}"); 49 | } 50 | 51 | let (xyz_caps, lab_caps) = res.unwrap(); 52 | 53 | let (x, y, z) = xyz_caps 54 | .extract::<3>() 55 | .1 56 | .iter() 57 | .filter_map(|e| e.parse::().ok()) 58 | .collect_tuple() 59 | .ok_or_else(|| anyhow!("expected 3 values for XYZ"))?; 60 | let (l, a, b) = lab_caps 61 | .extract::<3>() 62 | .1 63 | .iter() 64 | .filter_map(|e| e.parse::().ok()) 65 | .collect_tuple() 66 | .ok_or_else(|| anyhow!("expected 3 values for Lab"))?; 67 | 68 | let xyz = Vec3::new(x, y, z); 69 | let argyll_lab = Vec3::new(l, a, b); 70 | 71 | Ok(Self::from_argyll_results(target, xyz, argyll_lab)) 72 | } 73 | 74 | pub fn from_argyll_results(target: CalibrationTarget, xyz: Vec3, argyll_lab: Vec3) -> Self { 75 | let mut res = Self { 76 | target, 77 | xyz, 78 | argyll_lab, 79 | ..Default::default() 80 | }; 81 | res.set_or_update_calculated_values(); 82 | 83 | res 84 | } 85 | 86 | pub fn set_or_update_calculated_values(&mut self) { 87 | let xyy = transform::XYZ_to_xyY(self.xyz, WhitePoint::D65); 88 | self.xyy = round_colour(xyy); 89 | 90 | let lab = transform::XYZ_to_CIELAB(self.xyz / self.target.max_y, WhitePoint::D65); 91 | self.lab = round_colour(lab); 92 | 93 | self.cct = xyz_to_cct(self.xyz).unwrap_or_default(); 94 | 95 | // XYZ -> linear RGB, scaled to display peak 96 | let dst_csp = self.target.colorspace.to_kolor(); 97 | let rgb_conv = ColorConversion::new(kolor_64::spaces::CIE_XYZ, dst_csp); 98 | self.rgb = round_colour(rgb_conv.convert(self.xyz)); 99 | } 100 | 101 | pub fn target_min_normalized(&self) -> f64 { 102 | self.target.min_y / self.target.max_y 103 | } 104 | 105 | pub fn luminance(&self, oetf: bool) -> f64 { 106 | let target_eotf = self.target.eotf; 107 | let (min_y, max_y) = if target_eotf == LuminanceEotf::PQ { 108 | (0.0, self.target.max_hdr_mdl) 109 | } else { 110 | (self.target.min_y, self.target.max_y) 111 | }; 112 | 113 | if oetf { 114 | if target_eotf == LuminanceEotf::PQ { 115 | // PQ code 116 | target_eotf.oetf(self.xyy[2] / 10_000.0) 117 | } else { 118 | target_eotf.oetf(target_eotf.value(self.xyy[2] / max_y, true)) 119 | } 120 | } else { 121 | let y = self.xyy[2] / max_y; 122 | 123 | // Y, minY and maxY are all in display-gamma space 124 | // And we convert them to linear luminance, so min needs to be decoded to linear 125 | let min = target_eotf.oetf(min_y / max_y); 126 | let max = 1.0 - min; 127 | 128 | (y * max) + min 129 | } 130 | } 131 | 132 | pub fn gamma_normalized_rgb(&self) -> Vec3 { 133 | let actual_rgb = self.rgb; 134 | let sample_y = self.xyy[2]; 135 | 136 | if sample_y > 0.0 { 137 | actual_rgb / sample_y 138 | } else { 139 | actual_rgb 140 | } 141 | } 142 | 143 | pub fn gamma(&self) -> Option { 144 | if self.is_white_stimulus_reading() && self.not_zero_or_one_rgb() { 145 | let lum = self.luminance(false); 146 | 147 | let ref_stimulus = self.ref_rgb_linear_bpc().x; 148 | Some(LuminanceEotf::gamma(ref_stimulus, lum)) 149 | } else { 150 | None 151 | } 152 | } 153 | 154 | pub fn gamma_around_zero(&self) -> Option { 155 | self.gamma().map(|gamma| gamma - self.target.eotf.mean()) 156 | } 157 | 158 | // BPC applied to target ref RGB in linear space 159 | pub fn ref_rgb_linear_bpc(&self) -> Vec3 { 160 | let min = self.target.eotf.oetf(self.target_min_normalized()); 161 | let max = 1.0 - min; 162 | 163 | (self.target.ref_rgb * max) + min 164 | } 165 | 166 | // Encode linear RGB to target EOTF, need to be relative to the target display 167 | // The XYZ is scaled to current measured max Y 168 | pub fn ref_xyz_display_space( 169 | &self, 170 | target_rgb_to_xyz: ColorConversion, 171 | scale_to_y: bool, 172 | ) -> Vec3 { 173 | let is_pq = self.target.eotf == LuminanceEotf::PQ; 174 | let ref_rgb_clipped = if is_pq { 175 | // Clip to MDL PQ code, since ref display is expected to clip 176 | let max_pq = self.target.eotf.oetf(self.target.max_hdr_mdl / 10_000.0); 177 | self.target.ref_rgb.min(Vec3::new(max_pq, max_pq, max_pq)) 178 | } else { 179 | self.ref_rgb_linear_bpc() 180 | }; 181 | 182 | let mut ref_rgb = self.target.eotf.convert_vec(ref_rgb_clipped, false); 183 | 184 | // To nits 185 | if is_pq { 186 | ref_rgb *= 10_000.0; 187 | } 188 | 189 | let xyz = target_rgb_to_xyz.convert(ref_rgb); 190 | if !is_pq && scale_to_y { 191 | xyz * self.target.max_y 192 | } else { 193 | xyz 194 | } 195 | } 196 | 197 | // The Y is scaled to current measured max Y 198 | pub fn ref_xyy_display_space(&self, target_rgb_to_xyz: ColorConversion) -> Vec3 { 199 | let xyz = self.ref_xyz_display_space(target_rgb_to_xyz, true); 200 | XYZ_to_xyY(xyz, WhitePoint::D65) 201 | } 202 | 203 | pub fn ref_lab_display_space(&self, target_rgb_to_xyz: ColorConversion) -> Vec3 { 204 | let ref_xyz = self.ref_xyz_display_space(target_rgb_to_xyz, false); 205 | 206 | // Calculated L*a*b* is in D65 207 | XYZ_to_CIELAB(ref_xyz, WhitePoint::D65) 208 | } 209 | 210 | pub fn delta_e2000(&self, target_rgb_to_xyz: ColorConversion) -> DeltaE { 211 | let mut ref_lab = self.ref_lab_display_space(target_rgb_to_xyz); 212 | ref_lab.x = self.lab.x; 213 | 214 | MyLab(ref_lab).delta(MyLab(self.lab), DE2000) 215 | } 216 | 217 | pub fn delta_e2000_incl_luminance(&self, target_rgb_to_xyz: ColorConversion) -> DeltaE { 218 | let ref_lab = self.ref_lab_display_space(target_rgb_to_xyz); 219 | 220 | MyLab(ref_lab).delta(MyLab(self.lab), DE2000) 221 | } 222 | 223 | // All equal and not zero, means we're measuring white with stimulus 224 | pub fn is_white_stimulus_reading(&self) -> bool { 225 | let ref_red = self.target.ref_rgb.x; 226 | self.target.ref_rgb.to_array().iter().all(|e| *e == ref_red) 227 | } 228 | 229 | pub fn not_zero_or_one_rgb(&self) -> bool { 230 | (0.01..1.0).contains(&self.target.ref_rgb.x) 231 | } 232 | 233 | pub fn results_average_delta_e2000( 234 | results: &[Self], 235 | target_rgb_to_xyz: ColorConversion, 236 | ) -> f32 { 237 | let deltae_2000_sum: f32 = results 238 | .iter() 239 | .map(|e| *e.delta_e2000(target_rgb_to_xyz).value()) 240 | .sum(); 241 | 242 | deltae_2000_sum / results.len() as f32 243 | } 244 | 245 | pub fn results_average_delta_e2000_incl_luminance( 246 | results: &[Self], 247 | target_rgb_to_xyz: ColorConversion, 248 | ) -> f32 { 249 | let deltae_2000_sum: f32 = results 250 | .iter() 251 | .map(|e| *e.delta_e2000_incl_luminance(target_rgb_to_xyz).value()) 252 | .sum(); 253 | 254 | deltae_2000_sum / results.len() as f32 255 | } 256 | 257 | pub fn results_average_gamma(results: &[Self]) -> Option { 258 | let gamma_sum: f64 = results.iter().filter_map(|e| e.gamma()).sum(); 259 | 260 | Some(gamma_sum / results.len() as f64) 261 | } 262 | } 263 | 264 | #[cfg(test)] 265 | mod tests { 266 | use kolor_64::{ 267 | ColorConversion, Vec3, 268 | details::{color::WhitePoint, transform::XYZ_to_xyY}, 269 | spaces::CIE_XYZ, 270 | }; 271 | 272 | use crate::{ 273 | calibration::{LuminanceEotf, TargetColorspace}, 274 | utils::round_colour, 275 | }; 276 | 277 | use super::{CalibrationTarget, ReadingResult}; 278 | 279 | #[test] 280 | fn parse_reading_str() { 281 | let line = 282 | "Result is XYZ: 1.916894 2.645760 2.925977, D50 Lab: 18.565392 -13.538479 -6.117640"; 283 | let target = CalibrationTarget::default(); 284 | 285 | let reading = ReadingResult::from_spotread_result(target, line).unwrap(); 286 | assert_eq!(reading.xyz, Vec3::new(1.916894, 2.645_76, 2.925977)); 287 | assert_eq!( 288 | reading.argyll_lab, 289 | Vec3::new(18.565392, -13.538_479, -6.11764) 290 | ); 291 | } 292 | 293 | #[test] 294 | fn parse_white_reference_str() { 295 | let line = "Making result XYZ: 1.916894 2.645760 2.925977, D50 Lab: 18.565392 -13.538479 -6.117640 white reference."; 296 | let target = CalibrationTarget::default(); 297 | 298 | let reading = ReadingResult::from_spotread_result(target, line).unwrap(); 299 | assert_eq!(reading.xyz, Vec3::new(1.916894, 2.645_76, 2.925977)); 300 | assert_eq!( 301 | reading.argyll_lab, 302 | Vec3::new(18.565392, -13.538_479, -6.11764) 303 | ); 304 | } 305 | 306 | #[test] 307 | fn calculate_result_rgb() { 308 | let line = "Result is XYZ: 33.956292 19.408215 138.000457, D50 Lab: 51.161418 63.602645 -121.627088"; 309 | 310 | let target = CalibrationTarget { 311 | ref_rgb: Vec3::new(0.25024438, 0.25024438, 1.0), 312 | ..Default::default() 313 | }; 314 | 315 | let reading = ReadingResult::from_spotread_result(target, line).unwrap(); 316 | assert_eq!(reading.xyz, Vec3::new(33.956292, 19.408215, 138.000457)); 317 | assert_eq!( 318 | reading.argyll_lab, 319 | Vec3::new(51.161418, 63.602645, -121.627088) 320 | ); 321 | 322 | assert_eq!(reading.rgb, Vec3::new(11.403131, 9.232091, 143.827225)); 323 | } 324 | 325 | #[test] 326 | fn calculate_result_rgb_gray() { 327 | let line = 328 | "Result is XYZ: 5.509335 5.835576 5.835576, D50 Lab: 28.993788 -1.357676 -7.541553"; 329 | 330 | let target = CalibrationTarget { 331 | ref_rgb: Vec3::new(0.05, 0.05, 0.05), 332 | ..Default::default() 333 | }; 334 | 335 | let reading = ReadingResult::from_spotread_result(target, line).unwrap(); 336 | assert_eq!(reading.xyz, Vec3::new(5.509335, 5.835576, 5.835576)); 337 | assert_eq!( 338 | reading.argyll_lab, 339 | Vec3::new(28.993788, -1.357676, -7.541553) 340 | ); 341 | assert_eq!(reading.xyy, Vec3::new(0.320674, 0.339663, 5.835576)); 342 | assert_eq!(reading.lab, Vec3::new(28.993789, -0.434605, 2.169734)); 343 | 344 | assert_eq!(reading.rgb, Vec3::new(5.973441, 5.850096, 5.285468)); 345 | } 346 | 347 | #[test] 348 | fn ref_values_from_rgb() { 349 | // 5% stimulus 350 | let target = CalibrationTarget { 351 | ref_rgb: Vec3::new(0.5, 0.5, 0.5), 352 | ..Default::default() 353 | }; 354 | let target_rgb_to_xyz = ColorConversion::new(target.colorspace.to_kolor(), CIE_XYZ); 355 | 356 | let reading = ReadingResult { 357 | target, 358 | ..Default::default() 359 | }; 360 | assert_eq!(reading.ref_rgb_linear_bpc(), Vec3::new(0.5, 0.5, 0.5)); 361 | 362 | // EOTF encoded 363 | assert_eq!( 364 | reading.ref_xyz_display_space(target_rgb_to_xyz, true), 365 | Vec3::new(20.685804847401677, 21.763764082403103, 23.697039245842973) 366 | ); 367 | assert_eq!( 368 | reading.ref_xyy_display_space(target_rgb_to_xyz), 369 | Vec3::new(0.31272661468101204, 0.3290231303260619, 21.763764082403103) 370 | ); 371 | assert_eq!( 372 | reading.ref_lab_display_space(target_rgb_to_xyz), 373 | Vec3::new(53.77545209276276, 0.0, 0.0) 374 | ); 375 | } 376 | 377 | #[test] 378 | fn ref_values_from_rgb_with_bpc() { 379 | // 5% stimulus 380 | let target = CalibrationTarget { 381 | min_y: 0.1, 382 | ref_rgb: Vec3::new(0.5, 0.5, 0.5), 383 | ..Default::default() 384 | }; 385 | let target_rgb_to_xyz = ColorConversion::new(target.colorspace.to_kolor(), CIE_XYZ); 386 | 387 | let reading = ReadingResult { 388 | target, 389 | ..Default::default() 390 | }; 391 | 392 | assert_eq!( 393 | reading.ref_rgb_linear_bpc(), 394 | Vec3::new(0.5216438064054153, 0.5216438064054153, 0.5216438064054153) 395 | ); 396 | 397 | // EOTF encoded, scaled to max Y 398 | assert_eq!( 399 | reading.ref_xyz_display_space(target_rgb_to_xyz, true), 400 | Vec3::new(22.707082363307524, 23.890372513922078, 26.01255430433378) 401 | ); 402 | assert_eq!( 403 | reading.ref_xyy_display_space(target_rgb_to_xyz), 404 | Vec3::new(0.3127266146810121, 0.32902313032606184, 23.890372513922078) 405 | ); 406 | assert_eq!( 407 | reading.ref_lab_display_space(target_rgb_to_xyz), 408 | Vec3::new(55.97786542816817, 5.551115123125783e-14, 0.0) 409 | ); 410 | } 411 | 412 | #[test] 413 | fn delta_e2000_calc() { 414 | // 100% stimulus 415 | let target = CalibrationTarget { 416 | min_y: 0.13, 417 | max_y: 130.0, 418 | ref_rgb: Vec3::new(1.0, 1.0, 1.0), 419 | ..Default::default() 420 | }; 421 | let target_rgb_to_xyz = ColorConversion::new(target.colorspace.to_kolor(), CIE_XYZ); 422 | 423 | let xyz = Vec3::new(122.495956, 128.990751, 139.074044); 424 | let argyll_lab = Vec3::new(110.273101, -2.752364, -20.324487); 425 | let reading = ReadingResult::from_argyll_results(target, xyz, argyll_lab); 426 | 427 | assert_eq!(0.677219, *reading.delta_e2000(target_rgb_to_xyz).value()); 428 | assert_eq!( 429 | 0.6988424, 430 | *reading 431 | .delta_e2000_incl_luminance(target_rgb_to_xyz) 432 | .value() 433 | ); 434 | } 435 | 436 | #[test] 437 | fn test_xyz() { 438 | let target = CalibrationTarget { 439 | min_y: 0.132061, 440 | max_y: 129.072427, 441 | ref_rgb: Vec3::new(0.500489, 0.500489, 0.500489), 442 | ..Default::default() 443 | }; 444 | 445 | let xyz = Vec3::new(26.976765, 28.54357, 30.785474); 446 | let argyll_lab = Vec3::new(60.376676, -2.187671, -12.309911); 447 | let reading = ReadingResult::from_argyll_results(target, xyz, argyll_lab); 448 | 449 | let target_rgb_to_xyz = ColorConversion::new(target.colorspace.to_kolor(), CIE_XYZ); 450 | let ref_lab = reading.ref_lab_display_space(target_rgb_to_xyz); 451 | 452 | let de2000 = reading.delta_e2000(target_rgb_to_xyz); 453 | let de2000_incl_lum = reading.delta_e2000_incl_luminance(target_rgb_to_xyz); 454 | 455 | assert_eq!(ref_lab, Vec3::new(56.048075289473815, 0.0, 0.0)); 456 | assert_eq!(reading.lab, Vec3::new(54.148156, -0.569625, 0.382084)); 457 | 458 | assert_eq!(*de2000.value(), 0.91667163); 459 | assert_eq!(*de2000_incl_lum.value(), 2.0169756); 460 | } 461 | 462 | #[test] 463 | fn test_ref_xyz_pq_absolute() { 464 | let target = CalibrationTarget { 465 | min_y: 0.0, 466 | max_y: 800.0, 467 | max_hdr_mdl: 1000.0, 468 | ref_rgb: Vec3::new(0.5, 0.5, 0.5), 469 | colorspace: TargetColorspace::DisplayP3, 470 | eotf: LuminanceEotf::PQ, 471 | }; 472 | 473 | let xyz = Vec3::new(95.41516, 100.072455, 108.983916); 474 | let argyll_lab = Vec3::new(100.028009, -1.864209, -19.408698); 475 | let reading = ReadingResult::from_argyll_results(target, xyz, argyll_lab); 476 | 477 | let target_rgb_to_xyz = ColorConversion::new(target.colorspace.to_kolor(), CIE_XYZ); 478 | 479 | let target_xyz = round_colour(reading.ref_xyz_display_space(target_rgb_to_xyz, true)); 480 | let target_xyy = round_colour(XYZ_to_xyY(target_xyz, WhitePoint::D65)); 481 | 482 | assert_eq!(target_xyz, Vec3::new(87.676779, 92.245709, 100.439895)); 483 | assert_eq!(target_xyy, Vec3::new(0.312727, 0.329023, 92.245709)); 484 | } 485 | 486 | #[test] 487 | fn test_ref_xyz_pq_clip_max() { 488 | let target = CalibrationTarget { 489 | min_y: 0.0, 490 | max_y: 800.0, 491 | max_hdr_mdl: 800.0, 492 | ref_rgb: Vec3::new(0.950147, 0.950147, 0.950147), 493 | colorspace: TargetColorspace::DisplayP3, 494 | eotf: LuminanceEotf::PQ, 495 | }; 496 | 497 | let xyz = Vec3::new(754.483535, 793.981817, 864.330001); 498 | let argyll_lab = Vec3::new(215.416777, -4.833889, -38.650394); 499 | let reading = ReadingResult::from_argyll_results(target, xyz, argyll_lab); 500 | 501 | let target_rgb_to_xyz = ColorConversion::new(target.colorspace.to_kolor(), CIE_XYZ); 502 | 503 | let target_xyz = round_colour(reading.ref_xyz_display_space(target_rgb_to_xyz, true)); 504 | let target_xyy = round_colour(XYZ_to_xyY(target_xyz, WhitePoint::D65)); 505 | 506 | assert_eq!(target_xyz, Vec3::new(760.376, 800.0, 871.064)); 507 | assert_eq!(target_xyy, Vec3::new(0.312727, 0.329023, 800.0)); 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /src/app/internal_generator_ui.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use eframe::{ 4 | egui::{self, Context, Layout, RichText, Sense, TextEdit, Ui}, 5 | epaint::{Color32, Stroke, Vec2, vec2}, 6 | }; 7 | use egui_extras::{Column, TableBuilder}; 8 | use kolor_64::details::{color::WhitePoint, transform::XYZ_to_xyY}; 9 | use strum::IntoEnumIterator; 10 | 11 | use crate::{ 12 | calibration::{TargetColorspace, xyz_to_cct}, 13 | external::ExternalJobCmd, 14 | generators::internal::PatchListPreset, 15 | utils::rgb_10b_to_8b, 16 | }; 17 | 18 | use super::{ 19 | CalibrationState, PGenApp, ReadFileType, status_color_active, utils::is_dragvalue_finished, 20 | }; 21 | 22 | const PATCH_LIST_COLUMNS: &[&str] = &["#", "Patch", "Red", "Green", "Blue"]; 23 | 24 | pub fn add_internal_generator_ui(app: &mut PGenApp, ctx: &Context, ui: &mut Ui) { 25 | let cal_started = app.cal_state.internal_gen.started; 26 | 27 | ui.add_space(10.0); 28 | 29 | ui.heading("spotread CLI args"); 30 | ui.add_enabled_ui( 31 | !app.cal_state.spotread_started && !cal_started && !app.processing, 32 | |ui| { 33 | add_spotread_cli_args_ui(app, ui); 34 | }, 35 | ); 36 | 37 | ui.add_space(5.0); 38 | add_spotread_status_ui(app, ctx, ui); 39 | 40 | ui.add_space(10.0); 41 | add_target_config_ui(app, ui); 42 | 43 | ui.heading("Patch list"); 44 | ui.indent("patch_list_indent", |ui| { 45 | ui.horizontal(|ui| { 46 | let internal_gen = &mut app.cal_state.internal_gen; 47 | 48 | ui.label("Preset"); 49 | ui.add_enabled_ui(!cal_started, |ui| { 50 | egui::ComboBox::from_id_salt("patch_list_presets") 51 | .selected_text(internal_gen.preset.as_ref()) 52 | .width(200.0) 53 | .show_ui(ui, |ui| { 54 | for preset in PatchListPreset::iter() { 55 | ui.selectable_value(&mut internal_gen.preset, preset, preset.as_ref()); 56 | } 57 | }); 58 | if ui.button("Load").clicked() { 59 | internal_gen.load_preset(&app.state.pattern_config); 60 | } 61 | 62 | if ui.button("Load file").clicked() { 63 | app.ctx 64 | .external_tx 65 | .try_send(ExternalJobCmd::ReadFile(ReadFileType::PatchList)) 66 | .ok(); 67 | } 68 | }); 69 | }); 70 | 71 | ui.horizontal(|ui| { 72 | ui.add_enabled_ui(!cal_started, |ui| { 73 | ui.checkbox( 74 | &mut app.cal_state.internal_gen.pattern_insertion_cfg.enabled, 75 | "Full field pattern insertion", 76 | ); 77 | 78 | let mut duration = app 79 | .cal_state 80 | .internal_gen 81 | .pattern_insertion_cfg 82 | .duration 83 | .as_secs_f64(); 84 | ui.label("Duration"); 85 | ui.add( 86 | egui::DragValue::new(&mut duration) 87 | .update_while_editing(false) 88 | .suffix(" s") 89 | .max_decimals(2) 90 | .speed(0.01) 91 | .range(0.5..=10.0), 92 | ); 93 | app.cal_state.internal_gen.pattern_insertion_cfg.duration = 94 | Duration::from_secs_f64(duration); 95 | 96 | let mut level = app.cal_state.internal_gen.pattern_insertion_cfg.level * 100.0; 97 | ui.label("Level"); 98 | ui.add( 99 | egui::DragValue::new(&mut level) 100 | .update_while_editing(false) 101 | .suffix(" %") 102 | .max_decimals(2) 103 | .speed(0.1) 104 | .range(0.0..=100.0), 105 | ); 106 | app.cal_state.internal_gen.pattern_insertion_cfg.level = level / 100.0; 107 | }); 108 | }); 109 | 110 | ui.separator(); 111 | 112 | let avail_height = ui.available_height(); 113 | ui.horizontal(|ui| { 114 | ui.vertical(|ui| { 115 | ui.add_enabled_ui(!cal_started, |ui| { 116 | add_patch_list_table(app, ui, avail_height); 117 | }); 118 | }); 119 | 120 | ui.vertical(|ui| { 121 | add_patches_info_right_side(app, ui); 122 | }); 123 | }); 124 | }); 125 | } 126 | 127 | fn add_spotread_cli_args_ui(app: &mut PGenApp, ui: &mut Ui) { 128 | egui::Grid::new("spotread_cli_args_grid") 129 | .spacing([4.0, 4.0]) 130 | .show(ui, |ui| { 131 | ui.strong("Key"); 132 | ui.strong("Value"); 133 | ui.label(""); 134 | ui.end_row(); 135 | 136 | for i in 0..=app.cal_state.spotread_cli_args.len() { 137 | let real_row = i < app.cal_state.spotread_cli_args.len(); 138 | { 139 | let args = if real_row { 140 | &mut app.cal_state.spotread_cli_args[i] 141 | } else { 142 | &mut app.cal_state.spotread_tmp_args 143 | }; 144 | 145 | ui.add_sized(Vec2::new(75.0, 20.0), TextEdit::singleline(&mut args.0)); 146 | let value_res = ui.add_sized( 147 | Vec2::new(300.0, 20.0), 148 | TextEdit::singleline(args.1.get_or_insert_with(Default::default)), 149 | ); 150 | 151 | let is_enabled = { 152 | let tmp_args = &app.cal_state.spotread_tmp_args; 153 | real_row || (!tmp_args.0.is_empty() && tmp_args.1.is_some()) 154 | }; 155 | let add_value_changed = is_enabled && !real_row && value_res.lost_focus(); 156 | 157 | ui.add_enabled_ui(is_enabled, |ui| { 158 | let btn_label = if real_row { "Remove" } else { "Add" }; 159 | if add_value_changed || ui.button(btn_label).clicked() { 160 | if real_row { 161 | app.cal_state.spotread_cli_args.remove(i); 162 | } else { 163 | let tmp_args = &mut app.cal_state.spotread_tmp_args; 164 | app.cal_state.spotread_cli_args.push(tmp_args.clone()); 165 | 166 | tmp_args.0.clear(); 167 | tmp_args.1.take(); 168 | } 169 | } 170 | }); 171 | 172 | ui.end_row(); 173 | } 174 | } 175 | }); 176 | } 177 | 178 | fn add_spotread_status_ui(app: &mut PGenApp, ctx: &Context, ui: &mut Ui) { 179 | let spotread_started = app.cal_state.spotread_started; 180 | 181 | ui.horizontal(|ui| { 182 | let btn_label = if spotread_started { 183 | "Stop spotread" 184 | } else { 185 | "Start spotread" 186 | }; 187 | 188 | ui.add_enabled_ui(!app.processing, |ui| { 189 | if ui.button(btn_label).clicked() { 190 | app.cal_state.internal_gen.started = false; 191 | 192 | if spotread_started { 193 | app.ctx 194 | .external_tx 195 | .try_send(ExternalJobCmd::StopSpotreadProcess) 196 | .ok(); 197 | } else { 198 | app.ctx 199 | .external_tx 200 | .try_send(ExternalJobCmd::StartSpotreadProcess( 201 | app.cal_state.spotread_cli_args.to_owned(), 202 | )) 203 | .ok(); 204 | } 205 | } 206 | }); 207 | let status_color = status_color_active(ctx, spotread_started); 208 | let (res, painter) = ui.allocate_painter(Vec2::new(16.0, 16.0), Sense::hover()); 209 | painter.circle(res.rect.center(), 8.0, status_color, Stroke::NONE); 210 | 211 | // Show patch/list progress 212 | let current_idx = app 213 | .cal_state 214 | .internal_gen 215 | .started 216 | .then_some(app.cal_state.internal_gen.selected_idx) 217 | .flatten(); 218 | if let Some(idx) = current_idx { 219 | if app.cal_state.internal_gen.auto_advance { 220 | let num = (idx + 1) as f32; 221 | let count = app.cal_state.internal_gen.list.len() as f32; 222 | 223 | let progress = num / count; 224 | let pb = egui::ProgressBar::new(progress) 225 | .animate(true) 226 | .text(format!("Measuring: Patch {num} / {count}")); 227 | ui.add(pb); 228 | } else { 229 | ui.strong(format!("Measuring patch {idx}")); 230 | ui.add(egui::Spinner::new()); 231 | } 232 | } 233 | }); 234 | } 235 | 236 | fn add_target_config_ui(app: &mut PGenApp, ui: &mut Ui) { 237 | let cal_started = app.cal_state.internal_gen.started; 238 | 239 | ui.horizontal(|ui| { 240 | ui.label("Target brightness"); 241 | ui.add_enabled_ui(!cal_started, |ui| { 242 | ui.label("Min"); 243 | let min_y_res = ui.add( 244 | egui::DragValue::new(&mut app.cal_state.min_y) 245 | .update_while_editing(false) 246 | .suffix(" nits") 247 | .max_decimals(6) 248 | .speed(0.0001) 249 | .range(0.0..=5.0), 250 | ); 251 | if is_dragvalue_finished(min_y_res) { 252 | app.cal_state.update_patterns_target(); 253 | } 254 | 255 | ui.label("Max"); 256 | let max_y_res = ui.add( 257 | egui::DragValue::new(&mut app.cal_state.max_y) 258 | .update_while_editing(false) 259 | .suffix(" nits") 260 | .max_decimals(3) 261 | .speed(0.1) 262 | .range(25.0..=10_000.0), 263 | ); 264 | if is_dragvalue_finished(max_y_res) { 265 | app.cal_state.update_patterns_target(); 266 | } 267 | 268 | ui.label("Max HDR MDL"); 269 | let max_mdl_res = ui.add( 270 | egui::DragValue::new(&mut app.cal_state.max_hdr_mdl) 271 | .update_while_editing(false) 272 | .suffix(" nits") 273 | .max_decimals(3) 274 | .speed(1.0) 275 | .range(400.0..=10_000.0), 276 | ); 277 | if is_dragvalue_finished(max_mdl_res) { 278 | app.cal_state.update_patterns_target(); 279 | } 280 | }); 281 | }); 282 | 283 | ui.horizontal(|ui| { 284 | ui.label("Target primaries"); 285 | 286 | let old_csp = app.cal_state.target_csp; 287 | ui.add_enabled_ui(!cal_started, |ui| { 288 | egui::ComboBox::from_id_salt("target_colorspaces") 289 | .selected_text(app.cal_state.target_csp.as_ref()) 290 | .width(150.0) 291 | .show_ui(ui, |ui| { 292 | for csp in TargetColorspace::iter() { 293 | ui.selectable_value(&mut app.cal_state.target_csp, csp, csp.as_ref()); 294 | } 295 | }); 296 | }); 297 | if old_csp != app.cal_state.target_csp { 298 | app.cal_state.update_patterns_target(); 299 | } 300 | }); 301 | } 302 | 303 | fn add_patch_list_table(app: &mut PGenApp, ui: &mut Ui, avail_height: f32) { 304 | let bit_depth = app.state.pattern_config.bit_depth as u8; 305 | 306 | let internal_gen = &mut app.cal_state.internal_gen; 307 | let rows = &internal_gen.list; 308 | 309 | let patch_col = Column::auto().at_least(50.0); 310 | TableBuilder::new(ui) 311 | .striped(true) 312 | .column(Column::auto().at_least(25.0)) 313 | .column(patch_col) 314 | .column(patch_col) 315 | .column(patch_col) 316 | .column(patch_col) 317 | .resizable(true) 318 | .min_scrolled_height(300.0_f32.max(avail_height - 25.0)) 319 | .sense(Sense::click()) 320 | .header(20.0, |mut header| { 321 | for label in PATCH_LIST_COLUMNS.iter().copied() { 322 | header.col(|ui| { 323 | ui.strong(label); 324 | }); 325 | } 326 | }) 327 | .body(|body| { 328 | body.rows(20.0, rows.len(), |mut row| { 329 | let i = row.index(); 330 | row.set_selected(internal_gen.selected_idx.is_some_and(|si| i == si)); 331 | 332 | let patch = &rows[i]; 333 | 334 | let rgb_orig = patch.rgb; 335 | let rgb_8b = rgb_10b_to_8b(bit_depth, rgb_orig); 336 | let patch_colour = Color32::from_rgb(rgb_8b[0], rgb_8b[1], rgb_8b[2]); 337 | 338 | row.col(|ui| { 339 | ui.label(i.to_string()); 340 | }); 341 | row.col(|ui| { 342 | ui.add_space(2.0); 343 | let (rect, _) = ui.allocate_exact_size( 344 | vec2(ui.available_width(), ui.available_height() - 2.0), 345 | Sense::hover(), 346 | ); 347 | ui.painter().rect( 348 | rect, 349 | 0.0, 350 | patch_colour, 351 | Stroke::new(1.0, Color32::BLACK), 352 | egui::StrokeKind::Inside, 353 | ); 354 | }); 355 | for c in rgb_orig { 356 | row.col(|ui| { 357 | ui.label(format!("{c}")); 358 | }); 359 | } 360 | 361 | if row.response().clicked() { 362 | if internal_gen.selected_idx.is_some_and(|si| i == si) { 363 | internal_gen.selected_idx.take(); 364 | } else { 365 | internal_gen.selected_idx.replace(i); 366 | } 367 | } 368 | }) 369 | }); 370 | } 371 | 372 | fn add_patches_info_right_side(app: &mut PGenApp, ui: &mut Ui) { 373 | let pgen_connected = app.state.connected_state.connected; 374 | let cal_started = app.cal_state.internal_gen.started; 375 | 376 | let can_read_patches = { 377 | let internal_gen = &app.cal_state.internal_gen; 378 | 379 | pgen_connected 380 | && app.cal_state.spotread_started 381 | && !app.processing 382 | && !cal_started 383 | && !internal_gen.list.is_empty() 384 | }; 385 | let has_selected_patch = app.cal_state.internal_gen.selected_idx.is_some(); 386 | 387 | ui.horizontal(|ui| { 388 | ui.add_enabled_ui(can_read_patches, |ui| { 389 | if ui.button("Measure patches").clicked() { 390 | { 391 | let internal_gen = &mut app.cal_state.internal_gen; 392 | internal_gen.started = true; 393 | internal_gen.auto_advance = true; 394 | internal_gen.selected_idx = Some(0); 395 | } 396 | 397 | app.calibration_send_measure_selected_patch(); 398 | } 399 | if has_selected_patch { 400 | if ui.button("Measure selected patch").clicked() { 401 | let internal_gen = &mut app.cal_state.internal_gen; 402 | internal_gen.started = true; 403 | internal_gen.auto_advance = false; 404 | 405 | app.calibration_send_measure_selected_patch(); 406 | } 407 | ui.checkbox( 408 | &mut app.cal_state.internal_gen.read_selected_continuously, 409 | "Continuous", 410 | ); 411 | } 412 | }); 413 | }); 414 | 415 | let can_keep_reading = app.cal_state.internal_gen.auto_advance 416 | || app.cal_state.internal_gen.read_selected_continuously; 417 | let show_stop_btn = cal_started && can_keep_reading; 418 | if show_stop_btn && ui.button("Stop measuring").clicked() { 419 | let internal_gen = &mut app.cal_state.internal_gen; 420 | internal_gen.started = false; 421 | internal_gen.auto_advance = false; 422 | } 423 | 424 | let has_selected_patch_result = app 425 | .cal_state 426 | .internal_gen 427 | .selected_patch() 428 | .and_then(|e| e.result) 429 | .is_some(); 430 | if has_selected_patch_result { 431 | ui.separator(); 432 | add_selected_patch_results(ui, &mut app.cal_state); 433 | } 434 | } 435 | 436 | const XYY_RESULT_HEADERS: &[&str] = &["", "Target", "Actual", "Deviation"]; 437 | const XYY_RESULT_GRID: [(usize, &str); 3] = [(2, "Y"), (0, "x"), (1, "y")]; 438 | fn add_selected_patch_results(ui: &mut Ui, cal_state: &mut CalibrationState) { 439 | let res = cal_state 440 | .internal_gen 441 | .selected_patch() 442 | .and_then(|e| e.result.as_ref()) 443 | .unwrap(); 444 | 445 | let target_rgb_to_xyz = cal_state.target_rgb_to_xyz_conv(); 446 | let target_xyz = res.ref_xyz_display_space(target_rgb_to_xyz, true); 447 | let target_xyy = XYZ_to_xyY(target_xyz, WhitePoint::D65); 448 | 449 | let actual_xyy = res.xyy; 450 | let xyy_dev = actual_xyy - target_xyy; 451 | 452 | let label_size = 20.0; 453 | let text_size = label_size - 2.0; 454 | let value_col = Column::auto().at_least(100.0); 455 | let cell_layout = Layout::default() 456 | .with_main_align(egui::Align::Max) 457 | .with_cross_align(egui::Align::Max); 458 | TableBuilder::new(ui) 459 | .striped(true) 460 | .cell_layout(cell_layout) 461 | .column(Column::auto().at_least(40.0)) 462 | .column(value_col) 463 | .column(value_col) 464 | .column(value_col) 465 | .header(25.0, |mut header| { 466 | for label in XYY_RESULT_HEADERS.iter().copied() { 467 | header.col(|ui| { 468 | ui.label(label); 469 | }); 470 | } 471 | }) 472 | .body(|mut body| { 473 | for (cmp, label) in XYY_RESULT_GRID { 474 | body.row(25.0, |mut row| { 475 | let target_cmp = target_xyy[cmp]; 476 | let actual_cmp = actual_xyy[cmp]; 477 | 478 | let cmp_dev = xyy_dev[cmp]; 479 | let cmp_dev_str = if cal_state.show_deviation_percent { 480 | let cmp_dev_pct = (cmp_dev / target_cmp.abs()) * 100.0; 481 | format!("{cmp_dev_pct:.4} %") 482 | } else { 483 | format!("{cmp_dev:.4}") 484 | }; 485 | 486 | row.col(|ui| { 487 | ui.strong(RichText::new(label).size(label_size)); 488 | }); 489 | row.col(|ui| { 490 | ui.strong(RichText::new(format!("{target_cmp:.4}")).size(text_size)); 491 | }); 492 | row.col(|ui| { 493 | ui.strong(RichText::new(format!("{actual_cmp:.4}")).size(text_size)); 494 | }); 495 | row.col(|ui| { 496 | ui.strong(RichText::new(cmp_dev_str).size(text_size)); 497 | }); 498 | }); 499 | } 500 | 501 | // CCT is only relevant for greyscale readings 502 | if res.is_white_stimulus_reading() { 503 | let target_cct = xyz_to_cct(target_xyz).unwrap_or_default(); 504 | let actual_cct = res.cct; 505 | let cct_dev = actual_cct - target_cct; 506 | let cct_dev_str = if cal_state.show_deviation_percent { 507 | let cct_dev_pct = (cct_dev / target_cct.abs()) * 100.0; 508 | format!("{cct_dev_pct:.4} %") 509 | } else { 510 | format!("{cct_dev:.4}") 511 | }; 512 | 513 | body.row(25.0, |mut row| { 514 | row.col(|ui| { 515 | ui.strong(RichText::new("CCT").size(label_size)); 516 | }); 517 | row.col(|ui| { 518 | ui.strong(RichText::new(format!("{target_cct:.4}")).size(text_size)); 519 | }); 520 | row.col(|ui| { 521 | ui.strong(RichText::new(format!("{actual_cct:.4}")).size(text_size)); 522 | }); 523 | row.col(|ui| { 524 | ui.strong(RichText::new(cct_dev_str).size(text_size)); 525 | }); 526 | }); 527 | } 528 | }); 529 | 530 | ui.add_space(5.0); 531 | ui.checkbox(&mut cal_state.show_deviation_percent, "Deviation %"); 532 | 533 | ui.separator(); 534 | 535 | let actual_de2000 = res.delta_e2000(target_rgb_to_xyz); 536 | let actual_de2000_incl_lum = res.delta_e2000_incl_luminance(target_rgb_to_xyz); 537 | let actual_gamma_str = if let Some(actual_gamma) = res.gamma() { 538 | format!("{actual_gamma:.4}") 539 | } else { 540 | "N/A".to_string() 541 | }; 542 | 543 | egui::Grid::new("selected_patch_delta_grid") 544 | .spacing([20.0, 4.0]) 545 | .show(ui, |ui| { 546 | ui.strong(RichText::new("dE 2000").size(label_size)); 547 | ui.strong(RichText::new("dE 2000 (w/ lum)").size(label_size)); 548 | ui.strong(RichText::new("EOTF").size(label_size)); 549 | ui.end_row(); 550 | 551 | ui.strong(RichText::new(format!("{actual_de2000:.4}")).size(label_size)); 552 | ui.strong(RichText::new(format!("{actual_de2000_incl_lum:.4}")).size(label_size)); 553 | ui.strong(RichText::new(actual_gamma_str).size(label_size)); 554 | ui.end_row(); 555 | }); 556 | } 557 | -------------------------------------------------------------------------------- /src/pgen/controller/handler.rs: -------------------------------------------------------------------------------- 1 | use std::net::SocketAddr; 2 | use std::sync::Arc; 3 | use std::time::Duration; 4 | 5 | use base64::{Engine as _, engine::general_purpose::STANDARD}; 6 | use tokio::sync::Mutex; 7 | use tokio::sync::mpsc::Sender; 8 | 9 | use crate::app::PGenAppUpdate; 10 | use crate::pgen::commands::PGenSetConfCommand; 11 | use crate::pgen::pattern_config::PGenPatternConfig; 12 | use crate::pgen::{BitDepth, Colorimetry, DoviMapMode, HdrEotf, Primaries, QuantRange}; 13 | use crate::pgen::{ 14 | ColorFormat, DynamicRange, 15 | client::{PGenClient, PGenTestPattern}, 16 | commands::{PGenCommand, PGenCommandResponse, PGenGetConfCommand}, 17 | }; 18 | use crate::utils::scale_pattern_config_rgb_values; 19 | 20 | use super::{DisplayMode, PGenControllerContext, PGenControllerState}; 21 | 22 | pub type PGenControllerHandle = Arc>; 23 | 24 | #[derive(Debug)] 25 | pub struct PGenController { 26 | pub ctx: PGenControllerContext, 27 | pub state: PGenControllerState, 28 | pub previous_cfg: Option, 29 | } 30 | 31 | impl PGenController { 32 | pub fn new(app_tx: Option>) -> Self { 33 | let state = PGenControllerState::default(); 34 | let client = Arc::new(Mutex::new(PGenClient::new(state.socket_addr))); 35 | 36 | let ctx = PGenControllerContext { 37 | client, 38 | app_tx, 39 | egui_ctx: Default::default(), 40 | }; 41 | 42 | Self { 43 | ctx, 44 | state, 45 | previous_cfg: None, 46 | } 47 | } 48 | 49 | pub async fn set_initial_state(&mut self, state: PGenControllerState) { 50 | self.state = state; 51 | 52 | let res = { 53 | let mut client = self.ctx.client.lock().await; 54 | client 55 | .send_generic_command(PGenCommand::UpdateSocket(self.state.socket_addr)) 56 | .await 57 | }; 58 | 59 | self.handle_pgen_response(res); 60 | } 61 | 62 | pub fn update_ui(&self) { 63 | if let Some(egui_ctx) = self.ctx.egui_ctx.as_ref() { 64 | egui_ctx.request_repaint(); 65 | } 66 | } 67 | 68 | pub fn try_update_app_state(&self, state_updated: bool) { 69 | if let Some(app_tx) = state_updated.then_some(self.ctx.app_tx.as_ref()).flatten() { 70 | app_tx 71 | .try_send(PGenAppUpdate::NewState(self.state.clone())) 72 | .ok(); 73 | self.update_ui(); 74 | } 75 | } 76 | 77 | pub fn handle_pgen_response(&mut self, res: PGenCommandResponse) { 78 | let mut state_updated = 79 | !matches!(res, PGenCommandResponse::Busy | PGenCommandResponse::Ok(_)); 80 | if let PGenCommandResponse::Alive(is_alive) = res { 81 | state_updated = is_alive != self.state.connected_state.connected; 82 | }; 83 | 84 | match res { 85 | PGenCommandResponse::NotConnected => self.state.connected_state.connected = false, 86 | PGenCommandResponse::Busy | PGenCommandResponse::Ok(_) => (), 87 | PGenCommandResponse::Errored(e) => self.state.connected_state.error = Some(e), 88 | PGenCommandResponse::Alive(is_alive) => self.state.connected_state.connected = is_alive, 89 | PGenCommandResponse::Connect(state) 90 | | PGenCommandResponse::Quit(state) 91 | | PGenCommandResponse::Shutdown(state) 92 | | PGenCommandResponse::Reboot(state) => self.state.connected_state = state, 93 | PGenCommandResponse::MultipleGetConfRes(res) => { 94 | self.parse_multiple_get_conf_commands_res(res); 95 | } 96 | PGenCommandResponse::MultipleSetConfRes(res) => { 97 | self.parse_multiple_set_conf_commands_res(&res); 98 | } 99 | } 100 | 101 | self.try_update_app_state(state_updated); 102 | } 103 | 104 | pub async fn pgen_command(&mut self, cmd: PGenCommand) { 105 | log::trace!("Controller received command to execute: {:?}", cmd); 106 | 107 | let res = { 108 | let mut client = self.ctx.client.lock().await; 109 | client.send_generic_command(cmd).await 110 | }; 111 | self.handle_pgen_response(res); 112 | } 113 | 114 | pub async fn update_socket(&mut self, socket_addr: SocketAddr) { 115 | self.state.socket_addr = socket_addr; 116 | self.pgen_command(PGenCommand::UpdateSocket(socket_addr)) 117 | .await; 118 | } 119 | 120 | pub async fn send_heartbeat(&mut self) { 121 | if !self.state.connected_state.connected { 122 | return; 123 | } 124 | 125 | self.pgen_command(PGenCommand::IsAlive).await; 126 | } 127 | 128 | pub async fn initial_connect(&mut self) { 129 | self.pgen_command(PGenCommand::Connect).await; 130 | self.fetch_base_info().await; 131 | } 132 | 133 | pub async fn disconnect(&mut self) { 134 | if self.state.connected_state.connected { 135 | self.set_blank().await; 136 | self.pgen_command(PGenCommand::Quit).await; 137 | } 138 | } 139 | 140 | pub async fn reconnect(&mut self) { 141 | { 142 | let mut client = self.ctx.client.lock().await; 143 | 144 | // Don't auto connect 145 | if !client.connect_state.connected { 146 | return; 147 | } 148 | 149 | log::warn!("Reconnecting TCP socket stream"); 150 | let res = client.set_stream().await; 151 | match res { 152 | Ok(_) => { 153 | client.connect_state.connected = true; 154 | } 155 | Err(e) => client.connect_state.error = Some(e.to_string()), 156 | }; 157 | } 158 | 159 | self.fetch_base_info().await; 160 | } 161 | 162 | pub async fn fetch_base_info(&mut self) { 163 | if self.state.connected_state.connected { 164 | self.pgen_command(PGenCommand::MultipleGetConfCommands( 165 | PGenGetConfCommand::base_info_commands(), 166 | )) 167 | .await; 168 | } 169 | } 170 | 171 | pub async fn restart_pgenerator_software(&mut self, refetch: bool) { 172 | self.pgen_command(PGenCommand::RestartSoftware).await; 173 | self.set_blank().await; 174 | 175 | if refetch { 176 | self.fetch_base_info().await; 177 | } else { 178 | self.update_pgenerator_pid().await; 179 | } 180 | } 181 | 182 | async fn update_pgenerator_pid(&mut self) { 183 | self.pgen_command(PGenCommand::MultipleGetConfCommands(&[ 184 | PGenGetConfCommand::GetPGeneratorPid, 185 | ])) 186 | .await; 187 | } 188 | 189 | pub async fn change_display_mode(&mut self, mode: DisplayMode, get_pid: bool) { 190 | self.pgen_command(PGenCommand::MultipleSetConfCommands(vec![ 191 | PGenSetConfCommand::SetDisplayMode(mode), 192 | ])) 193 | .await; 194 | 195 | if get_pid { 196 | // Setting display mode restarts the PGenerator, get new PID 197 | self.update_pgenerator_pid().await; 198 | } 199 | 200 | self.set_blank().await; 201 | } 202 | 203 | async fn set_dolby_vision_mode(&mut self) { 204 | // Try finding a 1080p@60 display mode 205 | let valid_display_mode = self.state.pgen_info.as_ref().and_then(|info| { 206 | info.display_modes 207 | .iter() 208 | .find(|mode| mode.resolution == (1920, 1080) && mode.refresh_rate == 60.0) 209 | }); 210 | 211 | if let Some(valid_mode) = valid_display_mode.copied() { 212 | let needs_mode_switch = self 213 | .state 214 | .pgen_info 215 | .as_ref() 216 | .is_some_and(|info| info.current_display_mode != valid_mode); 217 | 218 | // Dolby Vision requires 8 bit patches 219 | self.state.pattern_config.bit_depth = BitDepth::Eight; 220 | 221 | if needs_mode_switch { 222 | self.change_display_mode(valid_mode, false).await; 223 | } 224 | 225 | // Set Dolby Vision configs 226 | let commands = PGenSetConfCommand::commands_for_dynamic_range(DynamicRange::Dovi); 227 | self.pgen_command(PGenCommand::MultipleSetConfCommands(commands)) 228 | .await; 229 | 230 | // Restart for changes to apply 231 | self.restart_pgenerator_software(false).await; 232 | self.try_update_app_state(true); 233 | } else { 234 | log::error!("Cannot set Dolby Vision, no 1080p display mode found"); 235 | } 236 | } 237 | 238 | pub async fn update_dynamic_range(&mut self, dynamic_range: DynamicRange) { 239 | if dynamic_range == DynamicRange::Dovi { 240 | self.set_dolby_vision_mode().await; 241 | } else { 242 | let commands = PGenSetConfCommand::commands_for_dynamic_range(dynamic_range); 243 | self.pgen_command(PGenCommand::MultipleSetConfCommands(commands)) 244 | .await; 245 | self.restart_pgenerator_software(false).await; 246 | } 247 | } 248 | 249 | pub fn get_color_format(&self) -> ColorFormat { 250 | self.state 251 | .pgen_info 252 | .as_ref() 253 | .map(|e| e.output_config.format) 254 | .unwrap_or_default() 255 | } 256 | 257 | async fn send_pattern_from_cfg_internal( 258 | &mut self, 259 | config: PGenPatternConfig, 260 | update_state: bool, 261 | ) { 262 | // Only send non repeated patterns 263 | let different_pattern = self.previous_cfg.map(|prev| prev != config).unwrap_or(true); 264 | 265 | if different_pattern { 266 | self.previous_cfg.replace(config); 267 | 268 | // Update current pattern and send it 269 | if update_state { 270 | self.state.pattern_config = config; 271 | self.try_update_app_state(true); 272 | } 273 | 274 | let pattern = PGenTestPattern::from_config(self.get_color_format(), &config); 275 | self.pgen_command(PGenCommand::TestPattern(pattern)).await; 276 | } 277 | } 278 | 279 | pub async fn send_pattern_from_cfg(&mut self, config: PGenPatternConfig) { 280 | let mut new_pattern_cfg = PGenPatternConfig { 281 | bit_depth: config.bit_depth, 282 | patch_colour: config.patch_colour, 283 | background_colour: config.background_colour, 284 | ..self.state.pattern_config 285 | }; 286 | 287 | if self.state.is_dovi_mode() && new_pattern_cfg.bit_depth != BitDepth::Eight { 288 | // Ensure DoVi patterns are 8 bit 289 | let prev_depth = new_pattern_cfg.bit_depth as u8; 290 | 291 | scale_pattern_config_rgb_values(&mut new_pattern_cfg, 8, prev_depth, false, false); 292 | } 293 | 294 | self.send_pattern_from_cfg_internal(new_pattern_cfg, true) 295 | .await; 296 | } 297 | 298 | pub async fn send_current_pattern(&mut self) { 299 | self.send_pattern_from_cfg_internal(self.state.pattern_config, false) 300 | .await; 301 | } 302 | 303 | pub async fn set_blank(&mut self) { 304 | let mut config = self.state.pattern_config; 305 | config.patch_colour = Default::default(); 306 | config.background_colour = Default::default(); 307 | 308 | // Blank should not reset pattern config 309 | self.send_pattern_from_cfg_internal(config, false).await; 310 | } 311 | 312 | pub async fn send_pattern_and_wait(&mut self, config: PGenPatternConfig, duration: Duration) { 313 | self.send_pattern_from_cfg(config).await; 314 | tokio::time::sleep(duration).await; 315 | } 316 | 317 | pub fn parse_multiple_get_conf_commands_res(&mut self, res: Vec<(PGenGetConfCommand, String)>) { 318 | let pgen_info = self.state.pgen_info.get_or_insert_with(Default::default); 319 | let out_cfg = &mut pgen_info.output_config; 320 | let mut changed_mode = false; 321 | 322 | for (cmd, res) in res { 323 | match cmd { 324 | PGenGetConfCommand::GetPGeneratorVersion => { 325 | pgen_info.version = cmd.parse_string_config(res.as_str()).to_owned(); 326 | } 327 | PGenGetConfCommand::GetPGeneratorPid => { 328 | pgen_info.pid = cmd.parse_string_config(res.as_str()).to_owned(); 329 | } 330 | PGenGetConfCommand::GetCurrentMode => { 331 | if let Ok(mode) = 332 | DisplayMode::try_from_str(cmd.parse_string_config(res.as_str())) 333 | { 334 | pgen_info.current_display_mode = mode; 335 | changed_mode = true; 336 | } 337 | } 338 | PGenGetConfCommand::GetModesAvailable => { 339 | let decoded_str = STANDARD 340 | .decode(cmd.parse_string_config(res.as_str())) 341 | .ok() 342 | .and_then(|e| String::from_utf8(e).ok()); 343 | if let Some(modes_str) = decoded_str { 344 | pgen_info.display_modes = modes_str 345 | .lines() 346 | .map(DisplayMode::try_from_str) 347 | .filter_map(Result::ok) 348 | .collect(); 349 | } 350 | } 351 | PGenGetConfCommand::GetColorFormat => { 352 | if let Some(format) = cmd 353 | .parse_number_config::(res) 354 | .and_then(ColorFormat::from_repr) 355 | { 356 | out_cfg.format = format; 357 | } 358 | } 359 | PGenGetConfCommand::GetBitDepth => { 360 | if let Some(bit_depth) = cmd 361 | .parse_number_config::(res) 362 | .and_then(BitDepth::from_repr) 363 | { 364 | out_cfg.bit_depth = bit_depth; 365 | } 366 | } 367 | PGenGetConfCommand::GetQuantRange => { 368 | if let Some(quant_range) = cmd 369 | .parse_number_config::(res) 370 | .and_then(QuantRange::from_repr) 371 | { 372 | out_cfg.quant_range = quant_range; 373 | self.state.pattern_config.limited_range = 374 | quant_range == QuantRange::Limited; 375 | } 376 | } 377 | PGenGetConfCommand::GetColorimetry => { 378 | if let Some(colorimetry) = cmd 379 | .parse_number_config::(res) 380 | .and_then(Colorimetry::from_repr) 381 | { 382 | out_cfg.colorimetry = colorimetry; 383 | } 384 | } 385 | PGenGetConfCommand::GetOutputRange => { 386 | if let Some(limited_range) = PGenGetConfCommand::parse_get_output_range(res) { 387 | out_cfg.quant_range = QuantRange::from(limited_range); 388 | } 389 | } 390 | PGenGetConfCommand::GetOutputIsSDR => { 391 | if cmd.parse_bool_config(res) { 392 | out_cfg.dynamic_range = DynamicRange::Sdr; 393 | } 394 | } 395 | PGenGetConfCommand::GetOutputIsHDR => { 396 | if cmd.parse_bool_config(res) { 397 | out_cfg.dynamic_range = DynamicRange::Hdr; 398 | } 399 | } 400 | PGenGetConfCommand::GetOutputIsLLDV => { 401 | if cmd.parse_bool_config(res) { 402 | out_cfg.dynamic_range = DynamicRange::Dovi; 403 | } 404 | } 405 | PGenGetConfCommand::GetOutputIsStdDovi => { 406 | if cmd.parse_bool_config(res) { 407 | out_cfg.dynamic_range = DynamicRange::Dovi; 408 | } 409 | } 410 | PGenGetConfCommand::GetDoviMapMode => { 411 | if let Some(dovi_map_mode) = cmd 412 | .parse_number_config(res) 413 | .and_then(DoviMapMode::from_repr) 414 | { 415 | out_cfg.dovi_map_mode = dovi_map_mode; 416 | } 417 | } 418 | PGenGetConfCommand::GetHdrEotf => { 419 | if let Some(eotf) = cmd 420 | .parse_number_config::(res) 421 | .and_then(HdrEotf::from_repr) 422 | { 423 | out_cfg.hdr_meta.eotf = eotf; 424 | } 425 | } 426 | PGenGetConfCommand::GetHdrPrimaries => { 427 | if let Some(primaries) = cmd 428 | .parse_number_config::(res) 429 | .and_then(Primaries::from_repr) 430 | { 431 | out_cfg.hdr_meta.primaries = primaries; 432 | } 433 | } 434 | PGenGetConfCommand::GetHdrMaxMdl => { 435 | if let Some(max_mdl) = cmd.parse_number_config::(res) { 436 | out_cfg.hdr_meta.max_mdl = max_mdl; 437 | } 438 | } 439 | PGenGetConfCommand::GetHdrMinMdl => { 440 | if let Some(min_mdl) = cmd.parse_number_config::(res) { 441 | out_cfg.hdr_meta.min_mdl = min_mdl; 442 | } 443 | } 444 | PGenGetConfCommand::GetHdrMaxCLL => { 445 | if let Some(maxcll) = cmd.parse_number_config::(res) { 446 | out_cfg.hdr_meta.maxcll = maxcll; 447 | } 448 | } 449 | PGenGetConfCommand::GetHdrMaxFALL => { 450 | if let Some(maxfall) = cmd.parse_number_config::(res) { 451 | out_cfg.hdr_meta.maxfall = maxfall; 452 | } 453 | } 454 | } 455 | } 456 | 457 | log::trace!("PGenerator info: {:?}", self.state.pgen_info); 458 | 459 | if changed_mode { 460 | self.state.set_pattern_size_and_pos_from_resolution(); 461 | } 462 | } 463 | 464 | pub async fn send_multiple_set_conf_commands(&mut self, commands: Vec) { 465 | self.pgen_command(PGenCommand::MultipleSetConfCommands(commands)) 466 | .await; 467 | } 468 | 469 | pub fn parse_multiple_set_conf_commands_res(&mut self, res: &[(PGenSetConfCommand, bool)]) { 470 | let mut changed_mode = false; 471 | 472 | if let Some(pgen_info) = self.state.pgen_info.as_mut() { 473 | let successful_sets = res.iter().filter_map(|(cmd, ok)| ok.then_some(*cmd)); 474 | 475 | for cmd in successful_sets { 476 | match cmd { 477 | PGenSetConfCommand::SetDisplayMode(mode) => { 478 | pgen_info.current_display_mode = mode; 479 | changed_mode = true; 480 | } 481 | PGenSetConfCommand::SetColorFormat(format) => { 482 | pgen_info.output_config.format = format; 483 | } 484 | PGenSetConfCommand::SetBitDepth(bit_depth) => { 485 | pgen_info.output_config.bit_depth = bit_depth; 486 | } 487 | PGenSetConfCommand::SetQuantRange(quant_range) => { 488 | pgen_info.output_config.quant_range = quant_range; 489 | self.state.pattern_config.limited_range = 490 | quant_range == QuantRange::Limited; 491 | } 492 | PGenSetConfCommand::SetColorimetry(colorimetry) => { 493 | pgen_info.output_config.colorimetry = colorimetry; 494 | } 495 | PGenSetConfCommand::SetOutputIsSDR(is_sdr) => { 496 | if is_sdr { 497 | pgen_info.output_config.dynamic_range = DynamicRange::Sdr; 498 | } 499 | } 500 | PGenSetConfCommand::SetOutputIsHDR(is_hdr) => { 501 | if is_hdr { 502 | pgen_info.output_config.dynamic_range = DynamicRange::Hdr; 503 | } 504 | } 505 | PGenSetConfCommand::SetOutputIsLLDV(is_lldv) => { 506 | if is_lldv { 507 | pgen_info.output_config.dynamic_range = DynamicRange::Dovi; 508 | } 509 | } 510 | PGenSetConfCommand::SetOutputIsStdDovi(is_std_dovi) => { 511 | if is_std_dovi { 512 | pgen_info.output_config.dynamic_range = DynamicRange::Dovi; 513 | } 514 | } 515 | PGenSetConfCommand::SetDoviStatus(_) 516 | | PGenSetConfCommand::SetDoviInterface(_) => {} 517 | PGenSetConfCommand::SetDoviMapMode(dovi_map_mode) => { 518 | pgen_info.output_config.dovi_map_mode = dovi_map_mode; 519 | } 520 | PGenSetConfCommand::SetHdrEotf(eotf) => { 521 | pgen_info.output_config.hdr_meta.eotf = eotf; 522 | } 523 | PGenSetConfCommand::SetHdrPrimaries(primaries) => { 524 | pgen_info.output_config.hdr_meta.primaries = primaries; 525 | } 526 | PGenSetConfCommand::SetHdrMaxMdl(max_mdl) => { 527 | pgen_info.output_config.hdr_meta.max_mdl = max_mdl; 528 | } 529 | PGenSetConfCommand::SetHdrMinMdl(min_mdl) => { 530 | pgen_info.output_config.hdr_meta.min_mdl = min_mdl; 531 | } 532 | PGenSetConfCommand::SetHdrMaxCLL(maxcll) => { 533 | pgen_info.output_config.hdr_meta.maxcll = maxcll; 534 | } 535 | PGenSetConfCommand::SetHdrMaxFALL(maxfall) => { 536 | pgen_info.output_config.hdr_meta.maxfall = maxfall; 537 | } 538 | } 539 | } 540 | } 541 | 542 | if changed_mode { 543 | self.state.set_pattern_size_and_pos_from_resolution(); 544 | } 545 | } 546 | } 547 | --------------------------------------------------------------------------------