commit 0a93e9c664d65d33391ba2ddae5313b9aa609ad7 Author: Tanishq Dubey Date: Thu Jun 5 21:05:56 2025 -0400 Commit V1.0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6864ad0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +*.jpg filter=lfs diff=lfs merge=lfs -binary +*.jpeg filter=lfs diff=lfs merge=lfs -binary +*.ARW filter=lfs diff=lfs merge=lfs -binary +*.DNG filter=lfs diff=lfs merge=lfs -binary +*.TIFF filter=lfs diff=lfs merge=lfs -binary +*.tiff filter=lfs diff=lfs merge=lfs -binary +*.tif filter=lfs diff=lfs merge=lfs -binary +*.TIF filter=lfs diff=lfs merge=lfs -binary +*.pdf filter=lfs diff=lfs merge=lfs -binary +*.JPG filter=lfs diff=lfs merge=lfs -binary \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f02a79d --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +tools/ +*.tiff +*.pp3 \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bb6a3a5 --- /dev/null +++ b/Makefile @@ -0,0 +1,162 @@ +# Makefile for image processing pipeline + +# --- Configuration --- +# Executables (assuming they are in the current directory or your PATH) +FILMCOLOR_EXE := ./filmcolor +FILMSCAN_EXE := ./filmscan +FILMGRAIN_EXE := ./filmgrain + +# Fixed Paths for this project +RAW_DIR := ./test_images/RAW +SIM_DATA_FILE := ./sim_data/portra_400.json +OUTPUT_BASE_DIR := ./test_images/v1.0output + +# Output subdirectory names (used to construct paths) +FILMCOLOR_SUBDIR := filmcolor +FILMSCAN_SUBDIR := filmscan +FILMGRAIN_RGB_SUBDIR := filmgrainrgb +FILMGRAIN_MONO_SUBDIR := filmgrainmono + +# --- Helper: Define all output directories --- +# These are the actual directory paths +DIR_FILMCOLOR := $(OUTPUT_BASE_DIR)/$(FILMCOLOR_SUBDIR) +DIR_FILMSCAN := $(OUTPUT_BASE_DIR)/$(FILMSCAN_SUBDIR) +DIR_FILMGRAIN_RGB := $(OUTPUT_BASE_DIR)/$(FILMGRAIN_RGB_SUBDIR) +DIR_FILMGRAIN_MONO:= $(OUTPUT_BASE_DIR)/$(FILMGRAIN_MONO_SUBDIR) + +ALL_OUTPUT_DIRS := $(DIR_FILMCOLOR) $(DIR_FILMSCAN) $(DIR_FILMGRAIN_RGB) $(DIR_FILMGRAIN_MONO) + +# --- Input Handling for single DNG processing --- +# INPUT_DNG_PATH is expected for targets like 'full_process' +# It's not used directly by 'run_all_test_process' but by the targets it calls if you invoke them manually. + +# Derive BASENAME if INPUT_DNG_PATH is provided (for single image processing) +# This block is evaluated if INPUT_DNG_PATH is set when make is invoked. +ifdef INPUT_DNG_PATH + # Check if the input DNG file exists (only if INPUT_DNG_PATH is provided) + ifeq ($(wildcard $(INPUT_DNG_PATH)),) + $(error Input DNG file not found: $(INPUT_DNG_PATH)) + endif + FILENAME_WITH_EXT_SINGLE := $(notdir $(INPUT_DNG_PATH)) + BASENAME_SINGLE := $(basename $(FILENAME_WITH_EXT_SINGLE)) # e.g., "09" from "09.DNG" + + # Define specific output files for the single INPUT_DNG_PATH + # These are used by 'full_process' target when INPUT_DNG_PATH is specified + SPECIFIC_FILMCOLOR_OUT := $(DIR_FILMCOLOR)/$(BASENAME_SINGLE).tiff + SPECIFIC_FILMSCAN_OUT := $(DIR_FILMSCAN)/$(BASENAME_SINGLE).tiff + SPECIFIC_FILMGRAIN_RGB_OUT := $(DIR_FILMGRAIN_RGB)/$(BASENAME_SINGLE).tiff + SPECIFIC_FILMGRAIN_MONO_OUT := $(DIR_FILMGRAIN_MONO)/$(BASENAME_SINGLE).tiff +endif + +# --- Batch Processing: Find all DNGs and define their targets --- +# Find all .DNG and .dng files in the RAW_DIR +DNG_FILES_IN_RAW := $(wildcard $(RAW_DIR)/*.DNG) $(wildcard $(RAW_DIR)/*.dng) + +# Extract unique basenames from these DNG files (e.g., "09", "another_image") +# $(notdir path/file.ext) -> file.ext +# $(basename file.ext) -> file (or file.part if original was file.part.ext) +# $(sort ... ) also removes duplicates +ALL_BASENAMES := $(sort $(foreach dng_file,$(DNG_FILES_IN_RAW),$(basename $(notdir $(dng_file))))) + +# Generate lists of all final output files for the run_all_test_process target +ALL_FINAL_RGB_OUTPUTS := $(foreach bn,$(ALL_BASENAMES),$(DIR_FILMGRAIN_RGB)/$(bn).tiff) +ALL_FINAL_MONO_OUTPUTS := $(foreach bn,$(ALL_BASENAMES),$(DIR_FILMGRAIN_MONO)/$(bn).tiff) +TARGETS_FOR_RUN_ALL := $(ALL_FINAL_RGB_OUTPUTS) $(ALL_FINAL_MONO_OUTPUTS) + + +# --- Targets --- +.DEFAULT_GOAL := help +.PHONY: all full_process run_all_test_process create_dirs help +.SECONDEXPANSION: # Allow use of $$(@F) etc. in static pattern rules if needed, though not strictly used here + +# Target to create all necessary output directories +# This is a prerequisite for the first processing step. +create_dirs: + @echo "Creating output directories if they don't exist..." + @mkdir -p $(ALL_OUTPUT_DIRS) + @echo "Output directories ensured: $(ALL_OUTPUT_DIRS)" + +# --- Static Pattern Rules for image processing steps --- +# These rules define how to build .tiff files in output directories from .DNG/.dng files in RAW_DIR +# The '%' is a wildcard that matches the basename of the file. + +# 1. Filmcolor (handles both .DNG and .dng inputs) +$(DIR_FILMCOLOR)/%.tiff: $(RAW_DIR)/%.DNG $(SIM_DATA_FILE) create_dirs + @echo "--- [1. Filmcolor] ---" + @echo " Input DNG: $<" + @echo " Sim Data: $(SIM_DATA_FILE)" + @echo " Output: $@" + $(FILMCOLOR_EXE) "$<" "$(SIM_DATA_FILE)" "$@" + +$(DIR_FILMCOLOR)/%.tiff: $(RAW_DIR)/%.dng $(SIM_DATA_FILE) create_dirs + @echo "--- [1. Filmcolor] ---" + @echo " Input dng: $<" + @echo " Sim Data: $(SIM_DATA_FILE)" + @echo " Output: $@" + $(FILMCOLOR_EXE) "$<" "$(SIM_DATA_FILE)" "$@" + +# 2. Filmscan +$(DIR_FILMSCAN)/%.tiff: $(DIR_FILMCOLOR)/%.tiff + @echo "--- [2. Filmscan] ---" + @echo " Input: $<" + @echo " Output: $@" + $(FILMSCAN_EXE) "$<" "$@" + +# 3. Filmgrain RGB +$(DIR_FILMGRAIN_RGB)/%.tiff: $(DIR_FILMSCAN)/%.tiff + @echo "--- [3. Filmgrain RGB] ---" + @echo " Input: $<" + @echo " Output: $@" + $(FILMGRAIN_EXE) "$<" "$@" + +# 4. Filmgrain Mono +$(DIR_FILMGRAIN_MONO)/%.tiff: $(DIR_FILMSCAN)/%.tiff + @echo "--- [4. Filmgrain Mono] ---" + @echo " Input: $<" + @echo " Output: $@" + $(FILMGRAIN_EXE) "$<" "$@" --mono + + +# --- Main User Targets --- + +# Process a single image specified by INPUT_DNG_PATH +full_process: $(SPECIFIC_FILMGRAIN_RGB_OUT) $(SPECIFIC_FILMGRAIN_MONO_OUT) +ifndef INPUT_DNG_PATH + $(error INPUT_DNG_PATH must be set for 'make full_process'. Usage: make full_process INPUT_DNG_PATH=/path/to/image.DNG) +endif + @echo "----------------------------------------------------" + @echo "SUCCESS: Full processing complete for $(BASENAME_SINGLE)" + @echo "RGB Output: $(SPECIFIC_FILMGRAIN_RGB_OUT)" + @echo "Mono Output: $(SPECIFIC_FILMGRAIN_MONO_OUT)" + @echo "----------------------------------------------------" + +# Process all DNG images in test_images/RAW/ +run_all_test_process: $(TARGETS_FOR_RUN_ALL) + @echo "====================================================" + @echo "SUCCESS: All test images in $(RAW_DIR) processed." + @echo "Processed $(words $(ALL_BASENAMES)) images: $(ALL_BASENAMES)" + @echo "====================================================" + +all: + @echo "Common targets: 'make full_process INPUT_DNG_PATH=...', 'make run_all_test_process'" + + +# Help message +help: + @echo "Makefile for Image Processing Pipeline" + @echo "" + @echo "Usage: make [INPUT_DNG_PATH=]" + @echo "" + @echo "Main Targets:" + @echo " full_process - Run the entire pipeline for a single image." + @echo " Requires: INPUT_DNG_PATH=./test_images/RAW/your_image.DNG" + @echo " run_all_test_process - Run the entire pipeline for ALL .DNG/.dng images" + @echo " found in $(RAW_DIR)/" + @echo "" + @echo "Other Targets:" + @echo " create_dirs - Ensure all output directories exist." + @echo " help - Show this help message." + @echo "" + @echo "Examples:" + @echo " make full_process INPUT_DNG_PATH=./test_images/RAW/09.DNG" + @echo " make run_all_test_process" diff --git a/README.md b/README.md new file mode 100644 index 0000000..c800ce5 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# filmsim + +This is an exploration into a few things: + - An LLM based project (I do minimal coding) + - Film simulation + - Real life film capture + +This project seeks to create a fast, "batteries included", film simulation package that walks the user through a film simulation. + +Currently we have the following pipeline components: + - `filmcolor` - Takes in a digital color positive (picture from your digital camera) and outputs a simulated film negative based on the film stock chosen + - `filmscan` - Simulates the film scan and negative reversal process by referencing the "[Negadoctor](https://github.com/darktable-org/darktable/blob/master/src/iop/negadoctor.c)" module from [Darktable](https://www.darktable.org/), but adding in a few auto features for "batteries included" + - `filmgrain` - Adds grain based on the filmgrain method by [Zhang et al. (2023)](https://dl.acm.org/doi/10.1145/3592127) in either RGB or monochrome + +All scripts are designed to take in TIFF/PNG/JPG, and output TIFF/PNG/JPG. TIFFs are output in uncompressed 16-bit. + +`filmcolor` can additionally take in Sony ARW and various DNG camera RAW files. + +All scripts are self contained and portable. + +Details about each script can be found in their respective readmes. + +This project also contains test input images and outputs at various stages of development. \ No newline at end of file diff --git a/docs/2017_Newson_film_grain.pdf b/docs/2017_Newson_film_grain.pdf new file mode 100644 index 0000000..0481ac2 --- /dev/null +++ b/docs/2017_Newson_film_grain.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2af7e48df36f997dea0721dd96eac206bfbd540ceddeb1107f556623f4b67687 +size 1016181 diff --git a/docs/A Model for Simulating the Photographic Development Process on Digital Images.pdf b/docs/A Model for Simulating the Photographic Development Process on Digital Images.pdf new file mode 100644 index 0000000..b4525b9 --- /dev/null +++ b/docs/A Model for Simulating the Photographic Development Process on Digital Images.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f4fe0427e5c097e69cb2ad59c04da9f73dcb00aca2bb050b9110399d6a0e9cb +size 345504 diff --git a/docs/Simulation of film media.pdf b/docs/Simulation of film media.pdf new file mode 100644 index 0000000..7e2216d --- /dev/null +++ b/docs/Simulation of film media.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c049f30257c9800cc98985dd4a2d9f89880a69e4096fdc4930bf4193aec4bebd +size 661653 diff --git a/docs/e4050_portra_400.pdf b/docs/e4050_portra_400.pdf new file mode 100644 index 0000000..46deeaa --- /dev/null +++ b/docs/e4050_portra_400.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83ac6775d37832a4cb466892a3e1cf4c88917a6ee93384e59d6924b1cd97e3a +size 262115 diff --git a/docs/negadoctor.md b/docs/negadoctor.md new file mode 100644 index 0000000..84c0c0b --- /dev/null +++ b/docs/negadoctor.md @@ -0,0 +1,1093 @@ +/* + This file is part of darktable, + Copyright (C) 2020-2024 darktable developers. + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +*/ + +#include "bauhaus/bauhaus.h" +#include "common/darktable.h" +#include "common/opencl.h" +#include "control/control.h" +#include "develop/develop.h" +#include "develop/imageop.h" +#include "develop/imageop_math.h" +#include "develop/imageop_gui.h" +#include "develop/openmp_maths.h" +#include "dtgtk/button.h" +#include "dtgtk/resetlabel.h" +#include "gui/accelerators.h" +#include "gui/gtk.h" +#include "gui/presets.h" +#include "gui/color_picker_proxy.h" +#include "iop/iop_api.h" + +#include +#include +#include + +/** DOCUMENTATION + * + * This module allows to invert scanned negatives and simulate their print on paper, based on Kodak Cineon + * densitometry algorithm. It is better than the old invert module because it takes into account the Dmax of the film + * and allows white balance adjustments, as well as paper grade (gamma) simulation. It also allows density correction + * in log space, to account for the exposure settings of the scanner. Finally, it is applied after input colour profiling, + * which means the inversion happens after the scanner or the camera got color-corrected, while the old invert module + * invert the RAW, non-demosaiced, file before any colour correction. + * + * References : + * + * - https://www.kodak.com/uploadedfiles/motion/US_plugins_acrobat_en_motion_education_sensitometry_workbook.pdf + * - http://www.digital-intermediate.co.uk/film/pdf/Cineon.pdf + * - https://lists.gnu.org/archive/html/openexr-devel/2005-03/msg00009.html + **/ + + #define THRESHOLD 2.3283064365386963e-10f // -32 EV + + +DT_MODULE_INTROSPECTION(2, dt_iop_negadoctor_params_t) + + +typedef enum dt_iop_negadoctor_filmstock_t +{ + // What kind of emulsion are we working on ? + DT_FILMSTOCK_NB = 0, // $DESCRIPTION: "black and white film" + DT_FILMSTOCK_COLOR = 1 // $DESCRIPTION: "color film" +} dt_iop_negadoctor_filmstock_t; + + +typedef struct dt_iop_negadoctor_params_t +{ + dt_iop_negadoctor_filmstock_t film_stock; /* $DEFAULT: DT_FILMSTOCK_COLOR $DESCRIPTION: "film stock" */ + float Dmin[4]; /* color of film substrate + $MIN: 0.00001 $MAX: 1.5 $DEFAULT: 1.0 */ + float wb_high[4]; /* white balance RGB coeffs (illuminant) + $MIN: 0.25 $MAX: 2 $DEFAULT: 1.0 */ + float wb_low[4]; /* white balance RGB offsets (base light) + $MIN: 0.25 $MAX: 2 $DEFAULT: 1.0 */ + float D_max; /* max density of film + $MIN: 0.1 $MAX: 6 $DEFAULT: 2.046 */ + float offset; /* inversion offset + $MIN: -1.0 $MAX: 1.0 $DEFAULT: -0.05 $DESCRIPTION: "scan exposure bias" */ + float black; /* display black level + $MIN: -0.5 $MAX: 0.5 $DEFAULT: 0.0755 $DESCRIPTION: "paper black (density correction)" */ + float gamma; /* display gamma + $MIN: 1.0 $MAX: 8.0 $DEFAULT: 4.0 $DESCRIPTION: "paper grade (gamma)" */ + float soft_clip; /* highlights roll-off + $MIN: 0.0001 $MAX: 1.0 $DEFAULT: 0.75 $DESCRIPTION: "paper gloss (specular highlights)" */ + float exposure; /* extra exposure + $MIN: 0.5 $MAX: 2.0 $DEFAULT: 0.9245 $DESCRIPTION: "print exposure adjustment" */ +} dt_iop_negadoctor_params_t; + + +typedef struct dt_iop_negadoctor_data_t +{ + dt_aligned_pixel_t Dmin; // color of film substrate + dt_aligned_pixel_t wb_high; // white balance RGB coeffs / Dmax + dt_aligned_pixel_t offset; // inversion offset + float black; // display black level + float gamma; // display gamma + float soft_clip; // highlights roll-off + float soft_clip_comp; // 1 - softclip, complement to 1 + float exposure; // extra exposure +} dt_iop_negadoctor_data_t; + + +typedef struct dt_iop_negadoctor_gui_data_t +{ + GtkNotebook *notebook; + GtkWidget *film_stock; + GtkWidget *Dmin_R, *Dmin_G, *Dmin_B; + GtkWidget *wb_high_R, *wb_high_G, *wb_high_B; + GtkWidget *wb_low_R, *wb_low_G, *wb_low_B; + GtkWidget *D_max; + GtkWidget *offset; + GtkWidget *black, *gamma, *soft_clip, *exposure; + GtkWidget *Dmin_picker, *Dmin_sampler; + GtkWidget *WB_high_picker, *WB_high_sampler; + GtkWidget *WB_low_picker, *WB_low_sampler; +} dt_iop_negadoctor_gui_data_t; + + +typedef struct dt_iop_negadoctor_global_data_t +{ + int kernel_negadoctor; +} dt_iop_negadoctor_global_data_t; + + +const char *name() +{ + return _("negadoctor"); +} + +const char *aliases() +{ + return _("film|invert|negative|scan"); +} + +const char **description(dt_iop_module_t *self) +{ + return dt_iop_set_description(self, _("invert film negative scans and simulate printing on paper"), + _("corrective and creative"), + _("linear, RGB, display-referred"), + _("non-linear, RGB"), + _("non-linear, RGB, display-referred")); +} + +int flags() +{ + return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_ONE_INSTANCE; +} + + +int default_group() +{ + return IOP_GROUP_BASIC | IOP_GROUP_TECHNICAL; +} + + +dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, + dt_dev_pixelpipe_t *pipe, + dt_dev_pixelpipe_iop_t *piece) +{ + return IOP_CS_RGB; +} + +int legacy_params(dt_iop_module_t *self, + const void *const old_params, + const int old_version, + void **new_params, + int32_t *new_params_size, + int *new_version) +{ + typedef struct dt_iop_negadoctor_params_v2_t + { + dt_iop_negadoctor_filmstock_t film_stock; + float Dmin[4]; + float wb_high[4]; + float wb_low[4]; + float D_max; + float offset; + float black; + float gamma; + float soft_clip; + float exposure; + } dt_iop_negadoctor_params_v2_t; + + if(old_version == 1) + { + typedef struct dt_iop_negadoctor_params_v1_t + { + dt_iop_negadoctor_filmstock_t film_stock; + dt_aligned_pixel_t Dmin; // color of film substrate + dt_aligned_pixel_t wb_high; // white balance RGB coeffs (illuminant) + dt_aligned_pixel_t wb_low; // white balance RGB offsets (base light) + float D_max; // max density of film + float offset; // inversion offset + float black; // display black level + float gamma; // display gamma + float soft_clip; // highlights roll-off + float exposure; // extra exposure + } dt_iop_negadoctor_params_v1_t; + + const dt_iop_negadoctor_params_v1_t *o = (dt_iop_negadoctor_params_v1_t *)old_params; + dt_iop_negadoctor_params_v2_t *n = malloc(sizeof(dt_iop_negadoctor_params_v2_t)); + + // WARNING: when copying the arrays in a for loop, gcc wrongly assumed + // that n and o were aligned and used AVX instructions for me, + // which segfaulted. let's hope this doesn't get optimized too much. + n->film_stock = o->film_stock; + n->Dmin[0] = o->Dmin[0]; + n->Dmin[1] = o->Dmin[1]; + n->Dmin[2] = o->Dmin[2]; + n->Dmin[3] = o->Dmin[3]; + n->wb_high[0] = o->wb_high[0]; + n->wb_high[1] = o->wb_high[1]; + n->wb_high[2] = o->wb_high[2]; + n->wb_high[3] = o->wb_high[3]; + n->wb_low[0] = o->wb_low[0]; + n->wb_low[1] = o->wb_low[1]; + n->wb_low[2] = o->wb_low[2]; + n->wb_low[3] = o->wb_low[3]; + n->D_max = o->D_max; + n->offset = o->offset; + n->black = o->black; + n->gamma = o->gamma; + n->soft_clip = o->soft_clip; + n->exposure = o->exposure; + + *new_params = n; + *new_params_size = sizeof(dt_iop_negadoctor_params_v2_t); + *new_version = 2; + return 0; + } + return 1; +} + +void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe, + dt_dev_pixelpipe_iop_t *piece) +{ + const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)p1; + dt_iop_negadoctor_data_t *const d = piece->data; + + // keep WB_high even in B&W mode to apply sepia or warm tone look + // but premultiply it aheard with Dmax to spare one div per pixel + for(size_t c = 0; c < 4; c++) d->wb_high[c] = p->wb_high[c] / p->D_max; + + for(size_t c = 0; c < 4; c++) d->offset[c] = p->wb_high[c] * p->offset * p->wb_low[c]; + + // ensure we use a monochrome Dmin for B&W film + if(p->film_stock == DT_FILMSTOCK_COLOR) + for(size_t c = 0; c < 4; c++) d->Dmin[c] = p->Dmin[c]; + else if(p->film_stock == DT_FILMSTOCK_NB) + for(size_t c = 0; c < 4; c++) d->Dmin[c] = p->Dmin[0]; + + // arithmetic trick allowing to rewrite the pixel inversion as FMA + d->black = -p->exposure * (1.0f + p->black); + + // highlights soft clip + d->soft_clip = p->soft_clip; + d->soft_clip_comp = 1.0f - p->soft_clip; + + // copy + d->exposure = p->exposure; + d->gamma = p->gamma; +} + +static inline void _process_pixel(const dt_aligned_pixel_t pix_in, + dt_aligned_pixel_t pix_out, + const dt_aligned_pixel_t Dmin, + const dt_aligned_pixel_t wb_high, + const dt_aligned_pixel_t offset, + const dt_aligned_pixel_t black, + const dt_aligned_pixel_t exposure, + const dt_aligned_pixel_t gamma, + const dt_aligned_pixel_t soft_clip, + const dt_aligned_pixel_t soft_clip_comp) +{ + dt_aligned_pixel_t density; + // Convert transmission to density using Dmin as a fulcrum + dt_aligned_pixel_t clamped; + for_each_channel(c) + { + clamped[c] = MAX(pix_in[c],THRESHOLD); // threshold to -32 EV + density[c] = Dmin[c] / clamped[c]; + } + dt_aligned_pixel_t log_density; + dt_vector_log2(density, log_density); + #define LOG2_to_LOG10 0.3010299956f + for_each_channel(c) + log_density[c] *= -LOG2_to_LOG10; + // now log_density = -log10f( Dmin / MAX(pix_in, THRESHOLD) ) + dt_aligned_pixel_t corrected_de; + for_each_channel(c) + { + // Correct density in log space + corrected_de[c] = wb_high[c] * log_density[c] + offset[c]; + } + dt_aligned_pixel_t ten_to_x; + dt_vector_exp10(corrected_de, ten_to_x); + dt_aligned_pixel_t print_linear; + for_each_channel(c) + { + // Print density on paper : ((1 - 10^corrected_de + black) * exposure)^gamma rewritten for FMA + print_linear[c] = -(exposure[c] * ten_to_x[c] + black[c]); + print_linear[c] = MAX(print_linear[c], 0.0f); + } + dt_aligned_pixel_t print_gamma; + dt_vector_powf(print_linear, gamma, print_gamma); // note : this is always > 0 + dt_aligned_pixel_t e_to_gamma; + dt_aligned_pixel_t clipped_gamma; + for_each_channel(c) + clipped_gamma[c] = -(print_gamma[c] - soft_clip[c]) / soft_clip_comp[c]; + dt_vector_exp(clipped_gamma, e_to_gamma); + for_each_channel(c) + { + // Compress highlights. from https://lists.gnu.org/archive/html/openexr-devel/2005-03/msg00009.html + pix_out[c] = (print_gamma[c] > soft_clip[c]) + ? soft_clip[c] + (1.0f - e_to_gamma[c]) * soft_clip_comp[c] + : print_gamma[c]; + } +} + +void process(dt_iop_module_t *const self, dt_dev_pixelpipe_iop_t *const piece, + const void *const restrict ivoid, void *const restrict ovoid, + const dt_iop_roi_t *const restrict roi_in, const dt_iop_roi_t *const restrict roi_out) +{ + const dt_iop_negadoctor_data_t *const d = piece->data; + assert(piece->colors = 4); + + const float *const restrict in = (float *)ivoid; + float *const restrict out = (float *)ovoid; + + dt_aligned_pixel_t gamma; + dt_aligned_pixel_t black; + dt_aligned_pixel_t exposure; + dt_aligned_pixel_t soft_clip; + dt_aligned_pixel_t soft_clip_comp; + for_each_channel(c) + { + gamma[c] = d->gamma; + black[c] = d->black; + exposure[c] = d->exposure; + soft_clip[c] = d->soft_clip; + soft_clip_comp[c] = d->soft_clip_comp; + } + // Unpack vectors one by one with extra pragmas to be sure the compiler understands they can be vectorized + const float *const restrict Dmin = DT_IS_ALIGNED_PIXEL(d->Dmin); + const float *const restrict wb_high = DT_IS_ALIGNED_PIXEL(d->wb_high); + const float *const restrict offset = DT_IS_ALIGNED_PIXEL(d->offset); + + DT_OMP_FOR() + for(size_t k = 0; k < (size_t)roi_out->height * roi_out->width * 4; k += 4) + { + const float *const restrict pix_in = in + k; + float *const restrict pix_out = out + k; + _process_pixel(pix_in, pix_out, Dmin, wb_high, offset, black, exposure, gamma, soft_clip, soft_clip_comp); + } +} + + +#ifdef HAVE_OPENCL +int process_cl(dt_iop_module_t *const self, dt_dev_pixelpipe_iop_t *const piece, cl_mem dev_in, cl_mem dev_out, + const dt_iop_roi_t *const restrict roi_in, const dt_iop_roi_t *const restrict roi_out) +{ + const dt_iop_negadoctor_data_t *const d = piece->data; + const dt_iop_negadoctor_global_data_t *const gd = self->global_data; + + const int devid = piece->pipe->devid; + const int width = roi_in->width; + const int height = roi_in->height; + + return dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_negadoctor, width, height, + CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(d->Dmin), CLARG(d->wb_high), + CLARG(d->offset), CLARG(d->exposure), CLARG(d->black), CLARG(d->gamma), CLARG(d->soft_clip), CLARG(d->soft_clip_comp)); +} +#endif + + +void init(dt_iop_module_t *self) +{ + dt_iop_default_init(self); + + dt_iop_negadoctor_params_t *d = self->default_params; + + d->Dmin[0] = 1.00f; + d->Dmin[1] = 0.45f; + d->Dmin[2] = 0.25f; + d->Dmin[3] = 1.00f; // keep parameter validation with -d common happy +} + +void init_presets(dt_iop_module_so_t *self) +{ + dt_iop_negadoctor_params_t tmp = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_COLOR, + .Dmin = { 1.13f, 0.49f, 0.27f, 0.0f}, + .wb_high = { 1.0f, 1.0f, 1.0f, 0.0f }, + .wb_low = { 1.0f, 1.0f, 1.0f, 0.0f }, + .D_max = 1.6f, + .offset = -0.05f, + .gamma = 4.0f, + .soft_clip = 0.75f, + .exposure = 0.9245f, + .black = 0.0755f }; + + + dt_gui_presets_add_generic(_("color film"), self->op, + self->version(), &tmp, sizeof(tmp), 1, DEVELOP_BLEND_CS_RGB_DISPLAY); + + dt_iop_negadoctor_params_t tmq = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_NB, + .Dmin = { 1.0f, 1.0f, 1.0f, 0.0f}, + .wb_high = { 1.0f, 1.0f, 1.0f, 0.0f }, + .wb_low = { 1.0f, 1.0f, 1.0f, 0.0f }, + .D_max = 2.2f, + .offset = -0.05f, + .gamma = 5.0f, + .soft_clip = 0.75f, + .exposure = 1.f, + .black = 0.0755f }; + + + dt_gui_presets_add_generic(_("black and white film"), self->op, + self->version(), &tmq, sizeof(tmq), 1, DEVELOP_BLEND_CS_RGB_DISPLAY); +} + +void init_global(dt_iop_module_so_t *self) +{ + dt_iop_negadoctor_global_data_t *gd = malloc(sizeof(dt_iop_negadoctor_global_data_t)); + + self->data = gd; + const int program = 30; // negadoctor.cl, from programs.conf + gd->kernel_negadoctor = dt_opencl_create_kernel(program, "negadoctor"); +} + +void cleanup_global(dt_iop_module_so_t *self) +{ + dt_iop_negadoctor_global_data_t *gd = self->data; + dt_opencl_free_kernel(gd->kernel_negadoctor); + free(self->data); + self->data = NULL; +} + +void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) +{ + piece->data = g_malloc0(sizeof(dt_iop_negadoctor_data_t)); +} + +void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) +{ + g_free(piece->data); + piece->data = NULL; +} + + +/* Global GUI stuff */ + +static void setup_color_variables(dt_iop_negadoctor_gui_data_t *const g, const gint state) +{ + gtk_widget_set_visible(g->Dmin_G, state); + gtk_widget_set_visible(g->Dmin_B, state); +} + + +static void toggle_stock_controls(dt_iop_module_t *const self) +{ + dt_iop_negadoctor_gui_data_t *const g = self->gui_data; + const dt_iop_negadoctor_params_t *const p = self->params; + + if(p->film_stock == DT_FILMSTOCK_NB) + { + // Hide color controls + setup_color_variables(g, FALSE); + dt_bauhaus_widget_set_label(g->Dmin_R, NULL, N_("D min")); + } + else if(p->film_stock == DT_FILMSTOCK_COLOR) + { + // Show color controls + setup_color_variables(g, TRUE); + dt_bauhaus_widget_set_label(g->Dmin_R, NULL, N_("D min red component")); + } + else + { + // We shouldn't be there + dt_print(DT_DEBUG_ALWAYS, "negadoctor film stock: undefined behavior"); + } +} + + +static void Dmin_picker_update(dt_iop_module_t *self) +{ + dt_iop_negadoctor_gui_data_t *const g = self->gui_data; + const dt_iop_negadoctor_params_t *const p = self->params; + + GdkRGBA color; + color.alpha = 1.0f; + + if(p->film_stock == DT_FILMSTOCK_COLOR) + { + color.red = p->Dmin[0]; + color.green = p->Dmin[1]; + color.blue = p->Dmin[2]; + } + else if(p->film_stock == DT_FILMSTOCK_NB) + { + color.red = color.green = color.blue = p->Dmin[0]; + } + + gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->Dmin_picker), &color); +} + +static void Dmin_picker_callback(GtkColorButton *widget, dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + dt_iop_negadoctor_params_t *p = self->params; + + dt_iop_color_picker_reset(self, TRUE); + + GdkRGBA c; + gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c); + p->Dmin[0] = c.red; + p->Dmin[1] = c.green; + p->Dmin[2] = c.blue; + + ++darktable.gui->reset; + dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]); + dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[1]); + dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[2]); + --darktable.gui->reset; + + Dmin_picker_update(self); + dt_iop_color_picker_reset(self, TRUE); + dt_dev_add_history_item(darktable.develop, self, TRUE); +} + +static void WB_low_picker_update(dt_iop_module_t *self) +{ + dt_iop_negadoctor_gui_data_t *const g = self->gui_data; + const dt_iop_negadoctor_params_t *const p = self->params; + + GdkRGBA color; + color.alpha = 1.0f; + + dt_aligned_pixel_t WB_low_invert; + for(size_t c = 0; c < 3; ++c) WB_low_invert[c] = 2.0f - p->wb_low[c]; + const float WB_low_max = v_maxf(WB_low_invert); + for(size_t c = 0; c < 3; ++c) WB_low_invert[c] /= WB_low_max; + + color.red = WB_low_invert[0]; + color.green = WB_low_invert[1]; + color.blue = WB_low_invert[2]; + + gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->WB_low_picker), &color); +} + +static void WB_low_picker_callback(GtkColorButton *widget, dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + dt_iop_negadoctor_params_t *p = self->params; + + dt_iop_color_picker_reset(self, TRUE); + + GdkRGBA c; + gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c); + + dt_aligned_pixel_t RGB = { 2.0f - c.red, 2.0f - c.green, 2.0f - c.blue }; + + float RGB_min = v_minf(RGB); + for(size_t k = 0; k < 3; k++) p->wb_low[k] = RGB[k] / RGB_min; + p->wb_low[3] = 1.0f; + + ++darktable.gui->reset; + dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]); + dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]); + dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]); + --darktable.gui->reset; + + WB_low_picker_update(self); + dt_iop_color_picker_reset(self, TRUE); + dt_dev_add_history_item(darktable.develop, self, TRUE); +} + + +static void WB_high_picker_update(dt_iop_module_t *self) +{ + dt_iop_negadoctor_gui_data_t *const g = self->gui_data; + const dt_iop_negadoctor_params_t *const p = self->params; + + GdkRGBA color; + color.alpha = 1.0f; + + dt_aligned_pixel_t WB_high_invert; + for(size_t c = 0; c < 3; ++c) WB_high_invert[c] = 2.0f - p->wb_high[c]; + const float WB_high_max = v_maxf(WB_high_invert); + for(size_t c = 0; c < 3; ++c) WB_high_invert[c] /= WB_high_max; + + color.red = WB_high_invert[0]; + color.green = WB_high_invert[1]; + color.blue = WB_high_invert[2]; + + gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->WB_high_picker), &color); +} + +static void WB_high_picker_callback(GtkColorButton *widget, dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + dt_iop_negadoctor_params_t *p = self->params; + + dt_iop_color_picker_reset(self, TRUE); + + GdkRGBA c; + gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c); + + dt_aligned_pixel_t RGB = { 2.0f - c.red, 2.0f - c.green, 2.0f - c.blue }; + float RGB_min = v_minf(RGB); + for(size_t k = 0; k < 3; k++) p->wb_high[k] = RGB[k] / RGB_min; + p->wb_high[3] = 1.0f; + + ++darktable.gui->reset; + dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]); + dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]); + dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]); + --darktable.gui->reset; + + WB_high_picker_update(self); + dt_iop_color_picker_reset(self, TRUE); + dt_dev_add_history_item(darktable.develop, self, TRUE); +} + + +/* Color pickers auto-tuners */ + +// measure Dmin from the film edges first +static void apply_auto_Dmin(dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + dt_iop_negadoctor_params_t *p = self->params; + + for(int k = 0; k < 4; k++) p->Dmin[k] = self->picked_color[k]; + + ++darktable.gui->reset; + dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]); + dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[1]); + dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[2]); + --darktable.gui->reset; + + Dmin_picker_update(self); + dt_control_queue_redraw_widget(self->widget); + dt_dev_add_history_item(darktable.develop, self, TRUE); +} + +// from Dmin, find out the range of density values of the film and compute Dmax +static void apply_auto_Dmax(dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + dt_iop_negadoctor_params_t *p = self->params; + + dt_aligned_pixel_t RGB; + for(int c = 0; c < 3; c++) + { + RGB[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD)); + } + + // Take the max(RGB) for safety. Big values unclip whites + p->D_max = v_maxf(RGB); + + ++darktable.gui->reset; + dt_bauhaus_slider_set(g->D_max, p->D_max); + --darktable.gui->reset; + + dt_control_queue_redraw_widget(self->widget); + dt_dev_add_history_item(darktable.develop, self, TRUE); +} + +// from Dmax, compute the offset so the range of density is rescaled between [0; 1] +static void apply_auto_offset(dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + dt_iop_negadoctor_params_t *p = self->params; + + dt_aligned_pixel_t RGB; + for(int c = 0; c < 3; c++) + RGB[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD)) / p->D_max; + + // Take the min(RGB) for safety. Negative values unclip blacks + p->offset = v_minf(RGB); + + ++darktable.gui->reset; + dt_bauhaus_slider_set(g->offset, p->offset); + --darktable.gui->reset; + + dt_control_queue_redraw_widget(self->widget); + dt_dev_add_history_item(darktable.develop, self, TRUE); +} + +// from Dmax and offset, compute the white balance correction as multipliers of the offset +// such that offset × wb[c] make black monochrome +static void apply_auto_WB_low(dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + dt_iop_negadoctor_params_t *p = self->params; + + dt_aligned_pixel_t RGB_min; + for(int c = 0; c < 3; c++) + RGB_min[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color[c], THRESHOLD)) / p->D_max; + + const float RGB_v_min = v_minf(RGB_min); // warning: can be negative + for(int c = 0; c < 3; c++) p->wb_low[c] = RGB_v_min / RGB_min[c]; + p->wb_low[3] = 1.0f; + + ++darktable.gui->reset; + dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]); + dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]); + dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]); + --darktable.gui->reset; + + WB_low_picker_update(self); + dt_control_queue_redraw_widget(self->widget); + dt_dev_add_history_item(darktable.develop, self, TRUE); +} + +// from Dmax, offset and white balance multipliers, compute the white balance of the illuminant as multipliers of 1/Dmax +// such that WB[c] / Dmax make white monochrome +static void apply_auto_WB_high(dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + dt_iop_negadoctor_params_t *p = self->params; + + dt_aligned_pixel_t RGB_min; + for(int c = 0; c < 3; c++) + RGB_min[c] = fabsf(-1.0f / (p->offset * p->wb_low[c] - log10f(p->Dmin[c] / fmaxf(self->picked_color[c], THRESHOLD)) / p->D_max)); + + const float RGB_v_min = v_minf(RGB_min); // warning : must be positive + for(int c = 0; c < 3; c++) p->wb_high[c] = RGB_min[c] / RGB_v_min; + p->wb_high[3] = 1.0f; + + ++darktable.gui->reset; + dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]); + dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]); + dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]); + --darktable.gui->reset; + + WB_high_picker_update(self); + dt_control_queue_redraw_widget(self->widget); + dt_dev_add_history_item(darktable.develop, self, TRUE); +} + +// from Dmax, offset and both white balances, compute the print black adjustment +// such that the printed values range from 0 to + infinity +static void apply_auto_black(dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + dt_iop_negadoctor_params_t *p = self->params; + + dt_aligned_pixel_t RGB; + for(int c = 0; c < 3; c++) + { + RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD)); + RGB[c] *= p->wb_high[c] / p->D_max; + RGB[c] += p->wb_low[c] * p->offset * p->wb_high[c]; + RGB[c] = 0.1f - (1.0f - fast_exp10f(RGB[c])); // actually, remap between -3.32 EV and infinity for safety because gamma comes later + } + p->black = v_maxf(RGB); + + ++darktable.gui->reset; + dt_bauhaus_slider_set(g->black, p->black); + --darktable.gui->reset; + + dt_control_queue_redraw_widget(self->widget); + dt_dev_add_history_item(darktable.develop, self, TRUE); +} + +// from Dmax, offset, both white balances, and printblack, compute the print exposure adjustment as a scaling factor +// such that the printed values range from 0 to 1 +static void apply_auto_exposure(dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + dt_iop_negadoctor_params_t *p = self->params; + + dt_aligned_pixel_t RGB; + for(int c = 0; c < 3; c++) + { + RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD)); + RGB[c] *= p->wb_high[c] / p->D_max; + RGB[c] += p->wb_low[c] * p->offset; + RGB[c] = 0.96f / (1.0f - fast_exp10f(RGB[c]) + p->black); // actually, remap in [0; 0.96] for safety + } + p->exposure = v_minf(RGB); + + ++darktable.gui->reset; + dt_bauhaus_slider_set(g->exposure, log2f(p->exposure)); + --darktable.gui->reset; + + dt_control_queue_redraw_widget(self->widget); + dt_dev_add_history_item(darktable.develop, self, TRUE); +} + + +void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, + dt_dev_pixelpipe_t *pipe) +{ + if(darktable.gui->reset) return; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + + if (picker == g->Dmin_sampler) + apply_auto_Dmin(self); + else if(picker == g->WB_high_sampler) + apply_auto_WB_high(self); + else if(picker == g->offset) + apply_auto_offset(self); + else if(picker == g->D_max) + apply_auto_Dmax(self); + else if(picker == g->WB_low_sampler) + apply_auto_WB_low(self); + else if(picker == g->exposure) + apply_auto_exposure(self); + else if(picker == g->black) + apply_auto_black(self); + else + dt_print(DT_DEBUG_ALWAYS, "[negadoctor] unknown color picker"); +} + +void gui_init(dt_iop_module_t *self) +{ + dt_iop_negadoctor_gui_data_t *g = IOP_GUI_ALLOC(negadoctor); + + static dt_action_def_t notebook_def = { }; + g->notebook = dt_ui_notebook_new(¬ebook_def); + dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), ¬ebook_def); + + // Page FILM PROPERTIES + GtkWidget *page1 = self->widget = dt_ui_notebook_page(g->notebook, N_("film properties"), NULL); + + // Dmin + + gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(C_("section", "color of the film base")), FALSE, FALSE, 0); + + GtkWidget *row1 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)); + + g->Dmin_picker = gtk_color_button_new(); + gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->Dmin_picker), FALSE); + gtk_color_button_set_title(GTK_COLOR_BUTTON(g->Dmin_picker), _("select color of film material from a swatch")); + gtk_box_pack_start(GTK_BOX(row1), GTK_WIDGET(g->Dmin_picker), TRUE, TRUE, 0); + g_signal_connect(G_OBJECT(g->Dmin_picker), "color-set", G_CALLBACK(Dmin_picker_callback), self); + + g->Dmin_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row1); + gtk_widget_set_tooltip_text(g->Dmin_sampler , _("pick color of film material from image")); + dt_action_define_iop(self, N_("pickers"), N_("film material"), g->Dmin_sampler, &dt_action_def_toggle); + + gtk_box_pack_start(GTK_BOX(page1), GTK_WIDGET(row1), FALSE, FALSE, 0); + + g->Dmin_R = dt_bauhaus_slider_from_params(self, "Dmin[0]"); + dt_bauhaus_slider_set_digits(g->Dmin_R, 4); + dt_bauhaus_slider_set_format(g->Dmin_R, "%"); + dt_bauhaus_slider_set_factor(g->Dmin_R, 100); + dt_bauhaus_widget_set_label(g->Dmin_R, NULL, N_("D min red component")); + gtk_widget_set_tooltip_text(g->Dmin_R, _("adjust the color and shade of the film transparent base.\n" + "this value depends on the film material, \n" + "the chemical fog produced while developing the film,\n" + "and the scanner white balance.")); + + g->Dmin_G = dt_bauhaus_slider_from_params(self, "Dmin[1]"); + dt_bauhaus_slider_set_digits(g->Dmin_G, 4); + dt_bauhaus_slider_set_format(g->Dmin_G, "%"); + dt_bauhaus_slider_set_factor(g->Dmin_G, 100); + dt_bauhaus_widget_set_label(g->Dmin_G, NULL, N_("D min green component")); + gtk_widget_set_tooltip_text(g->Dmin_G, _("adjust the color and shade of the film transparent base.\n" + "this value depends on the film material, \n" + "the chemical fog produced while developing the film,\n" + "and the scanner white balance.")); + + g->Dmin_B = dt_bauhaus_slider_from_params(self, "Dmin[2]"); + dt_bauhaus_slider_set_digits(g->Dmin_B, 4); + dt_bauhaus_slider_set_format(g->Dmin_B, "%"); + dt_bauhaus_slider_set_factor(g->Dmin_B, 100); + dt_bauhaus_widget_set_label(g->Dmin_B, NULL, N_("D min blue component")); + gtk_widget_set_tooltip_text(g->Dmin_B, _("adjust the color and shade of the film transparent base.\n" + "this value depends on the film material, \n" + "the chemical fog produced while developing the film,\n" + "and the scanner white balance.")); + + // D max and scanner bias + + gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(C_("section", "dynamic range of the film")), FALSE, FALSE, 0); + + g->D_max = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "D_max")); + dt_bauhaus_slider_set_format(g->D_max, " dB"); + gtk_widget_set_tooltip_text(g->D_max, _("maximum density of the film, corresponding to white after inversion.\n" + "this value depends on the film specifications, the developing process,\n" + "the dynamic range of the scene and the scanner exposure settings.")); + + gtk_box_pack_start(GTK_BOX(page1), dt_ui_section_label_new(C_("section", "scanner exposure settings")), FALSE, FALSE, 0); + + g->offset = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "offset")); + dt_bauhaus_slider_set_format(g->offset, " dB"); + gtk_widget_set_tooltip_text(g->offset, _("correct the exposure of the scanner, for all RGB channels,\n" + "before the inversion, so blacks are neither clipped or too pale.")); + + // Page CORRECTIONS + GtkWidget *page2 = self->widget = dt_ui_notebook_page(g->notebook, N_("corrections"), NULL); + + // WB shadows + gtk_box_pack_start(GTK_BOX(page2), dt_ui_section_label_new(C_("section", "shadows color cast")), FALSE, FALSE, 0); + + GtkWidget *row3 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)); + + g->WB_low_picker = gtk_color_button_new(); + gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->WB_low_picker), FALSE); + gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_low_picker), _("select color of shadows from a swatch")); + gtk_box_pack_start(GTK_BOX(row3), GTK_WIDGET(g->WB_low_picker), TRUE, TRUE, 0); + g_signal_connect(G_OBJECT(g->WB_low_picker), "color-set", G_CALLBACK(WB_low_picker_callback), self); + + g->WB_low_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row3); + gtk_widget_set_tooltip_text(g->WB_low_sampler, _("pick shadows color from image")); + dt_action_define_iop(self, N_("pickers"), N_("shadows"), g->WB_low_sampler, &dt_action_def_toggle); + + gtk_box_pack_start(GTK_BOX(page2), GTK_WIDGET(row3), FALSE, FALSE, 0); + + g->wb_low_R = dt_bauhaus_slider_from_params(self, "wb_low[0]"); + dt_bauhaus_widget_set_label(g->wb_low_R, NULL, N_("shadows red offset")); + gtk_widget_set_tooltip_text(g->wb_low_R, _("correct the color cast in shadows so blacks are\n" + "truly achromatic. Setting this value before\n" + "the highlights illuminant white balance will help\n" + "recovering the global white balance in difficult cases.")); + + g->wb_low_G = dt_bauhaus_slider_from_params(self, "wb_low[1]"); + dt_bauhaus_widget_set_label(g->wb_low_G, NULL, N_("shadows green offset")); + gtk_widget_set_tooltip_text(g->wb_low_G, _("correct the color cast in shadows so blacks are\n" + "truly achromatic. Setting this value before\n" + "the highlights illuminant white balance will help\n" + "recovering the global white balance in difficult cases.")); + + g->wb_low_B = dt_bauhaus_slider_from_params(self, "wb_low[2]"); + dt_bauhaus_widget_set_label(g->wb_low_B, NULL, N_("shadows blue offset")); + gtk_widget_set_tooltip_text(g->wb_low_B, _("correct the color cast in shadows so blacks are\n" + "truly achromatic. Setting this value before\n" + "the highlights illuminant white balance will help\n" + "recovering the global white balance in difficult cases.")); + + // WB highlights + gtk_box_pack_start(GTK_BOX(page2), dt_ui_section_label_new(C_("section", "highlights white balance")), FALSE, FALSE, 0); + + GtkWidget *row2 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)); + + g->WB_high_picker = gtk_color_button_new(); + gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->WB_high_picker), FALSE); + gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_high_picker), _("select color of illuminant from a swatch")); + gtk_box_pack_start(GTK_BOX(row2), GTK_WIDGET(g->WB_high_picker), TRUE, TRUE, 0); + g_signal_connect(G_OBJECT(g->WB_high_picker), "color-set", G_CALLBACK(WB_high_picker_callback), self); + + g->WB_high_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row2); + gtk_widget_set_tooltip_text(g->WB_high_sampler , _("pick illuminant color from image")); + dt_action_define_iop(self, N_("pickers"), N_("illuminant"), g->WB_high_sampler, &dt_action_def_toggle); + + gtk_box_pack_start(GTK_BOX(page2), GTK_WIDGET(row2), FALSE, FALSE, 0); + + g->wb_high_R = dt_bauhaus_slider_from_params(self, "wb_high[0]"); + dt_bauhaus_widget_set_label(g->wb_high_R, NULL, N_("illuminant red gain")); + gtk_widget_set_tooltip_text(g->wb_high_R, _("correct the color of the illuminant so whites are\n" + "truly achromatic. Setting this value after\n" + "the shadows color cast will help\n" + "recovering the global white balance in difficult cases.")); + + g->wb_high_G = dt_bauhaus_slider_from_params(self, "wb_high[1]"); + dt_bauhaus_widget_set_label(g->wb_high_G, NULL, N_("illuminant green gain")); + gtk_widget_set_tooltip_text(g->wb_high_G, _("correct the color of the illuminant so whites are\n" + "truly achromatic. Setting this value after\n" + "the shadows color cast will help\n" + "recovering the global white balance in difficult cases.")); + + g->wb_high_B = dt_bauhaus_slider_from_params(self, "wb_high[2]"); + dt_bauhaus_widget_set_label(g->wb_high_B, NULL, N_("illuminant blue gain")); + gtk_widget_set_tooltip_text(g->wb_high_B, _("correct the color of the illuminant so whites are\n" + "truly achromatic. Setting this value after\n" + "the shadows color cast will help\n" + "recovering the global white balance in difficult cases.")); + + // Page PRINT PROPERTIES + GtkWidget *page3 = self->widget = dt_ui_notebook_page(g->notebook, N_("print properties"), NULL); + + // print corrections + gtk_box_pack_start(GTK_BOX(page3), dt_ui_section_label_new(C_("section", "virtual paper properties")), FALSE, FALSE, 0); + + g->black = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "black")); + dt_bauhaus_slider_set_digits(g->black, 4); + dt_bauhaus_slider_set_factor(g->black, 100); + dt_bauhaus_slider_set_format(g->black, "%"); + gtk_widget_set_tooltip_text(g->black, _("correct the density of black after the inversion,\n" + "to adjust the global contrast while avoiding clipping shadows.")); + + g->gamma = dt_bauhaus_slider_from_params(self, "gamma"); + dt_bauhaus_widget_set_label(g->gamma, NULL, N_("paper grade (gamma)")); + gtk_widget_set_tooltip_text(g->gamma, _("select the grade of the virtual paper, which is actually\n" + "equivalent to applying a gamma. it compensates the film D max\n" + "and recovers the contrast. use a high grade for high D max.")); + + g->soft_clip = dt_bauhaus_slider_from_params(self, "soft_clip"); + dt_bauhaus_slider_set_factor(g->soft_clip, 100); + dt_bauhaus_slider_set_digits(g->soft_clip, 4); + dt_bauhaus_slider_set_format(g->soft_clip, "%"); + gtk_widget_set_tooltip_text(g->soft_clip, _("gradually compress specular highlights past this value\n" + "to avoid clipping while pushing the exposure for mid-tones.\n" + "this somewhat reproduces the behavior of matte paper.")); + + gtk_box_pack_start(GTK_BOX(page3), dt_ui_section_label_new(C_("section", "virtual print emulation")), FALSE, FALSE, 0); + + g->exposure = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "exposure")); + dt_bauhaus_slider_set_hard_min(g->exposure, -1.0); + dt_bauhaus_slider_set_soft_min(g->exposure, -1.0); + dt_bauhaus_slider_set_hard_max(g->exposure, 1.0); + dt_bauhaus_slider_set_format(g->exposure, _(" EV")); + gtk_widget_set_tooltip_text(g->exposure, _("correct the printing exposure after inversion to adjust\n" + "the global contrast and avoid clipping highlights.")); + + // start building top level widget + self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_BAUHAUS_SPACE); + + // Film emulsion + g->film_stock = dt_bauhaus_combobox_from_params(self, "film_stock"); + gtk_widget_set_tooltip_text(g->film_stock, _("toggle on or off the color controls")); + + gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0); +} + + +void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) +{ + dt_iop_negadoctor_params_t *p = self->params; + dt_iop_negadoctor_gui_data_t *g = self->gui_data; + if(!w || w == g->film_stock) + { + toggle_stock_controls(self); + Dmin_picker_update(self); + } + else if(w == g->Dmin_R && p->film_stock == DT_FILMSTOCK_NB) + { + dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[0]); + dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[0]); + } + else if(w == g->Dmin_R || w == g->Dmin_G || w == g->Dmin_B) + { + Dmin_picker_update(self); + } + else if(w == g->exposure) + { + p->exposure = powf(2.0f, p->exposure); + } + + if(!w || w == g->wb_high_R || w == g->wb_high_G || w == g->wb_high_B) + { + WB_high_picker_update(self); + } + + if(!w || w == g->wb_low_R || w == g->wb_low_G || w == g->wb_low_B) + { + WB_low_picker_update(self); + } +} + + +void gui_update(dt_iop_module_t *const self) +{ + // let gui slider match current parameters: + dt_iop_negadoctor_gui_data_t *const g = self->gui_data; + const dt_iop_negadoctor_params_t *const p = self->params; + + dt_iop_color_picker_reset(self, TRUE); + + + dt_bauhaus_slider_set(g->exposure, log2f(p->exposure)); // warning: GUI is in EV + dt_bauhaus_slider_set_default(g->exposure, log2f(p->exposure)); // otherwise always showes as "changed" + + // Update custom stuff + gui_changed(self, NULL, NULL); +} + +void gui_reset(dt_iop_module_t *self) +{ + dt_iop_color_picker_reset(self, TRUE); +} +// clang-format off +// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py +// vim: shiftwidth=2 expandtab tabstop=2 cindent +// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; +// clang-format on diff --git a/docs/negadoctor_cl.md b/docs/negadoctor_cl.md new file mode 100644 index 0000000..01604b9 --- /dev/null +++ b/docs/negadoctor_cl.md @@ -0,0 +1,53 @@ +/* + This file is part of darktable, + Copyright (C) 2020 darktable developers. + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +*/ + +#include "common.h" + + +kernel void +negadoctor (read_only image2d_t in, write_only image2d_t out, int width, int height, + const float4 Dmin, const float4 wb_high, const float4 offset, + const float exposure, const float black, const float gamma, const float soft_clip, const float soft_clip_comp) +{ + const unsigned int x = get_global_id(0); + const unsigned int y = get_global_id(1); + + if(x >= width || y >= height) return; + + float4 i = read_imagef(in, sampleri, (int2)(x, y)); + float4 o; + + // Convert transmission to density using Dmin as a fulcrum + o = -native_log10(Dmin / fmax(i, (float4)2.3283064365386963e-10f)); // threshold to -32 EV + + // Correct density in log space + o = wb_high * o + offset; + + // Print density on paper : ((1 - 10^corrected_de + black) * exposure)^gamma rewritten for FMA + o = -((float4)exposure * native_exp10(o) + (float4)black); + o = dtcl_pow(fmax(o, (float4)0.0f), gamma); // note : this is always > 0 + + // Compress highlights and clip negatives. from https://lists.gnu.org/archive/html/openexr-devel/2005-03/msg00009.html + o = (o > (float4)soft_clip) ? soft_clip + ((float4)1.0f - native_exp(-(o - (float4)soft_clip) / (float4)soft_clip_comp)) * (float4)soft_clip_comp + : o; + + // Copy alpha + o.w = i.w; + + write_imagef(out, (int2)(x, y), o); +} diff --git a/docs/sigg97.pdf b/docs/sigg97.pdf new file mode 100644 index 0000000..017bbd3 --- /dev/null +++ b/docs/sigg97.pdf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40e437495b9e7390c2c96aaa7f10b306beda53a164696cd28551427c61486c94 +size 311893 diff --git a/filmcolor b/filmcolor new file mode 100755 index 0000000..7e67543 --- /dev/null +++ b/filmcolor @@ -0,0 +1,725 @@ +#!/usr/bin/env -S uv run --script +# /// script +# dependencies = [ +# "numpy", +# "scipy", +# "Pillow", +# "imageio", +# "rawpy", +# ] +# /// + +# -*- coding: utf-8 -*- +""" +Single-file Python script for Film Stock Color Emulation based on Datasheet CSV. + +Focuses on color transformation, applying effects derived from datasheet parameters. +Assumes input image is in linear RGB format. Excludes film grain simulation. + +Dependencies: numpy, imageio, scipy +Installation: pip install numpy imageio scipy Pillow +""" + +import argparse +import csv +import numpy as np +import imageio.v3 as iio +from scipy.interpolate import interp1d +from scipy.ndimage import gaussian_filter +import rawpy +import os +import sys +import json + +# --- Configuration --- +# Small epsilon to prevent log(0) or division by zero errors +EPSILON = 1e-10 + +from typing import Optional, List + + +class Info: + name: str + description: str + format_mm: int + version: str + + def __init__( + self, name: str, description: str, format_mm: int, version: str + ) -> None: + self.name = name + self.description = description + self.format_mm = format_mm + self.version = version + + def __repr__(self) -> str: + return ( + f"Info(name={self.name}, description={self.description}, " + f"format_mm={self.format_mm}, version={self.version})" + ) + + +class Balance: + r_shift: float + g_shift: float + b_shift: float + + def __init__(self, r_shift: float, g_shift: float, b_shift: float) -> None: + self.r_shift = r_shift + self.g_shift = g_shift + self.b_shift = b_shift + + def __repr__(self) -> str: + return ( + f"Balance(r_shift={self.r_shift:.3f}, g_shift={self.g_shift:.3f}, b_shift={self.b_shift:.3f})" + ) + + +class Gamma: + r_factor: float + g_factor: float + b_factor: float + + def __init__(self, r_factor: float, g_factor: float, b_factor: float) -> None: + self.r_factor = r_factor + self.g_factor = g_factor + self.b_factor = b_factor + + def __repr__(self) -> str: + return ( + f"Gamma(r_factor={self.r_factor:.3f}, g_factor={self.g_factor:.3f}, b_factor={self.b_factor:.3f})" + ) + + +class Processing: + gamma: Gamma + balance: Balance + + def __init__(self, gamma: Gamma, balance: Balance) -> None: + self.gamma = gamma + self.balance = balance + + def __repr__(self) -> str: + return ( + f"Processing(gamma=({self.gamma.r_factor:.3f}, {self.gamma.g_factor:.3f}, {self.gamma.b_factor:.3f}), " + f"balance=({self.balance.r_shift:.3f}, {self.balance.g_shift:.3f}, {self.balance.b_shift:.3f}))" + ) + + +class Couplers: + amount: float + diffusion_um: float + + def __init__(self, amount: float, diffusion_um: float) -> None: + self.amount = amount + self.diffusion_um = diffusion_um + + def __repr__(self) -> str: + return f"Couplers(amount={self.amount:.3f}, diffusion_um={self.diffusion_um:.1f})" + + +class HDCurvePoint: + d: Optional[float] + r: float + g: float + b: float + + def __init__(self, d: Optional[float], r: float, g: float, b: float) -> None: + self.d = d + self.r = r + self.g = g + self.b = b + + def __repr__(self) -> str: + return f"HDCurvePoint(d={self.d}, r={self.r:.3f}, g={self.g:.3f}, b={self.b:.3f})" + + +class SpectralSensitivityCurvePoint: + wavelength: float + y: float + m: float + c: float + + def __init__(self, wavelength: float, y: float, m: float, c: float) -> None: + self.wavelength = wavelength + self.y = y + self.m = m + self.c = c + + def __repr__(self) -> str: + return f"SpectralSensitivityCurvePoint(wavelength={self.wavelength:.1f}, y={self.y:.3f}, m={self.m:.3f}, c={self.c:.3f})" + + +class RGBValue: + r: float + g: float + b: float + + def __init__(self, r: float, g: float, b: float) -> None: + self.r = r + self.g = g + self.b = b + + def __repr__(self) -> str: + return f"RGBValue(r={self.r:.3f}, g={self.g:.3f}, b={self.b:.3f})" + + +class Curves: + hd: List[HDCurvePoint] + spectral_sensitivity: List[SpectralSensitivityCurvePoint] + + def __init__(self, hd: List[HDCurvePoint], spectral_sensitivity: List[SpectralSensitivityCurvePoint]) -> None: + self.hd = hd + self.spectral_sensitivity = spectral_sensitivity + + def __repr__(self) -> str: + return f"Curves(hd={',\n'.join(repr(point) for point in self.hd)}, spectral_sensitivity={',\n'.join(repr(point) for point in self.spectral_sensitivity)})" + + +class Halation: + strength: RGBValue + size_um: RGBValue + + def __init__(self, strength: RGBValue, size_um: RGBValue) -> None: + self.strength = strength + self.size_um = size_um + + def __repr__(self) -> str: + return f"Halation(strength={self.strength}, size_um={self.size_um})" + + +class Interlayer: + diffusion_um: float + + def __init__(self, diffusion_um: float) -> None: + self.diffusion_um = diffusion_um + + def __repr__(self) -> str: + return f"Interlayer(diffusion_um={self.diffusion_um:.1f})" + + +class Calibration: + iso: int + middle_gray_logE: float + + def __init__(self, iso: int, middle_gray_logE: float) -> None: + self.iso = iso + self.middle_gray_logE = middle_gray_logE + + def __repr__(self) -> str: + return ( + f"Calibration(iso={self.iso}\nmiddle_gray_logE={self.middle_gray_logE:.3f})" + ) + + +class Properties: + halation: Halation + couplers: Couplers + interlayer: Interlayer + curves: Curves + calibration: Calibration + + def __init__( + self, + halation: Halation, + couplers: Couplers, + interlayer: Interlayer, + curves: Curves, + calibration: Calibration, + ) -> None: + self.halation = halation + self.couplers = couplers + self.interlayer = interlayer + self.curves = curves + self.calibration = calibration + + def __repr__(self) -> str: + return ( + f"Properties(halation={self.halation}\ncouplers={self.couplers}\n" + f"interlayer={self.interlayer}\ncurves={self.curves}\n" + f"calibration={self.calibration})" + ) + + +class FilmDatasheet: + info: Info + processing: Processing + properties: Properties + + def __init__( + self, info: Info, processing: Processing, properties: Properties + ) -> None: + self.info = info + self.processing = processing + self.properties = properties + + def __repr__(self) -> str: + return ( + f"FilmDatasheet(info={self.info}\nprocessing={self.processing}\n" + f"properties={self.properties})" + ) + +import pprint + +def parse_datasheet_json(json_filepath) -> FilmDatasheet | None: + # Parse JSON into FilmDatasheet object + """Parses the film datasheet JSON file. + Args: + json_filepath (str): Path to the datasheet JSON file. + Returns: + FilmDatasheet: Parsed datasheet object. + """ + if not os.path.exists(json_filepath): + print(f"Error: Datasheet file not found at {json_filepath}", file=sys.stderr) + return None + try: + with open(json_filepath, "r") as jsonfile: + data = json.load(jsonfile) + # Parse the JSON data into the FilmDatasheet structure + info = Info( + name=data["info"]["name"], + description=data["info"]["description"], + format_mm=data["info"]["format_mm"], + version=data["info"]["version"], + ) + gamma = Gamma( + r_factor=data["processing"]["gamma"]["r_factor"], + g_factor=data["processing"]["gamma"]["g_factor"], + b_factor=data["processing"]["gamma"]["b_factor"], + ) + balance = Balance( + r_shift=data["processing"]["balance"]["r_shift"], + g_shift=data["processing"]["balance"]["g_shift"], + b_shift=data["processing"]["balance"]["b_shift"], + ) + processing = Processing(gamma=gamma, balance=balance) + halation = Halation( + strength=RGBValue( + r=data["properties"]["halation"]["strength"]["r"], + g=data["properties"]["halation"]["strength"]["g"], + b=data["properties"]["halation"]["strength"]["b"], + ), + size_um=RGBValue( + r=data["properties"]["halation"]["size_um"]["r"], + g=data["properties"]["halation"]["size_um"]["g"], + b=data["properties"]["halation"]["size_um"]["b"], + ), + ) + couplers = Couplers( + amount=data["properties"]["couplers"]["amount"], + diffusion_um=data["properties"]["couplers"]["diffusion_um"], + ) + interlayer = Interlayer( + diffusion_um=data["properties"]["interlayer"]["diffusion_um"] + ) + calibration = Calibration( + iso=data["properties"]["calibration"]["iso"], + middle_gray_logE=data["properties"]["calibration"]["middle_gray_logh"], + ) + curves = Curves( + hd=[ + HDCurvePoint(d=point["d"], r=point["r"], g=point["g"], b=point["b"]) + for point in data["properties"]["curves"]["hd"] + ], + spectral_sensitivity=[ + SpectralSensitivityCurvePoint( + wavelength=point["wavelength"], + y=point["y"], + m=point["m"], + c=point["c"], + ) + for point in data["properties"]["curves"]["spectral_sensitivity"] + ], + ) + print(f"Parsed {len(curves.hd)} H&D curve points.") + properties = Properties( + calibration=calibration, + halation=halation, + couplers=couplers, + interlayer=interlayer, + curves=curves, + ) + return FilmDatasheet( + info=info, processing=processing, properties=properties + ) + except Exception as e: + print(f"Error parsing datasheet JSON '{json_filepath}': {e}", file=sys.stderr) + return None + + +def um_to_pixels(sigma_um, image_width_px, film_format_mm): + """Converts sigma from micrometers to pixels.""" + if film_format_mm <= 0 or image_width_px <= 0: + return 0 + microns_per_pixel = (film_format_mm * 1000.0) / image_width_px + sigma_pixels = sigma_um / microns_per_pixel + return sigma_pixels + + +def apply_hd_curves( + log_exposure_rgb, + processing: Processing, + hd_curve: List[HDCurvePoint], + middle_gray_logE: float, +) -> np.ndarray: + """Applies H&D curves to log exposure values.""" + density_rgb = np.zeros_like(log_exposure_rgb) + gamma_factors = [ + processing.gamma.r_factor, + processing.gamma.g_factor, + processing.gamma.b_factor, + ] + balance_shifts = [ + processing.balance.r_shift, + processing.balance.g_shift, + processing.balance.b_shift, + ] + + min_logE = hd_curve[0].d + max_logE = hd_curve[-1].d + min_densities = [hd_curve[0].g, hd_curve[0].g, hd_curve[0].g] + max_densities = [hd_curve[-1].g, hd_curve[-1].g, hd_curve[-1].g] + + for i, channel in enumerate(["R", "G", "B"]): + # Apply gamma factor (affects contrast by scaling log exposure input) + # Handle potential division by zero if gamma factor is 0 + gamma_factor = gamma_factors[i] + if abs(gamma_factor) < EPSILON: + print( + f"Warning: Gamma factor for channel {channel} is near zero. Clamping to {EPSILON}.", + file=sys.stderr, + ) + gamma_factor = EPSILON if gamma_factor >= 0 else -EPSILON + + # Adjust log exposure relative to middle gray before applying gamma + log_exposure_adjusted = ( + middle_gray_logE + + (log_exposure_rgb[..., i] - middle_gray_logE) / gamma_factor + ) + + # Clamp adjusted exposure to the range defined in the H&D data before interpolation + log_exposure_clamped = np.clip(log_exposure_adjusted, min_logE, max_logE) + + # Create interpolation function for the current channel + if channel == "R": + interp_func = interp1d( + [d.d for d in hd_curve], + [d.r for d in hd_curve], + kind="linear", # Linear interpolation is common, could be 'cubic' + bounds_error=False, # Allows extrapolation, but we clamp manually below + fill_value=(min_densities[i], max_densities[i]), # type: ignore + ) + elif channel == "G": + interp_func = interp1d( + [d.d for d in hd_curve], + [d.g for d in hd_curve], + kind="linear", # Linear interpolation is common, could be 'cubic' + bounds_error=False, # Allows extrapolation, but we clamp manually below + fill_value=(min_densities[i], max_densities[i]), # type: ignore + ) + else: + interp_func = interp1d( + [d.d for d in hd_curve], + [d.b for d in hd_curve], + kind="linear", # Linear interpolation is common, could be 'cubic' + bounds_error=False, # Allows extrapolation, but we clamp manually below + fill_value=(min_densities[i], max_densities[i]), # type: ignore + ) + + # Apply interpolation + density = interp_func(log_exposure_clamped) + + # Apply density balance shift (additive density offset) + density += balance_shifts[i] + + density_rgb[..., i] = np.maximum(density, 0) # Ensure density is non-negative + + return density_rgb + + +def apply_saturation_rgb(image_linear, saturation_factor): + """Adjusts saturation directly in RGB space.""" + if saturation_factor == 1.0: + return image_linear + + # Luminance weights for sRGB primaries (Rec.709) + luminance = ( + 0.2126 * image_linear[..., 0] + + 0.7152 * image_linear[..., 1] + + 0.0722 * image_linear[..., 2] + ) + + # Expand luminance to 3 channels for broadcasting + luminance_rgb = np.expand_dims(luminance, axis=-1) + + # Apply saturation: Lerp between luminance (grayscale) and original color + saturated_image = luminance_rgb + saturation_factor * (image_linear - luminance_rgb) + + # Clip results to valid range (important after saturation boost) + return np.clip(saturated_image, 0.0, 1.0) + + +def apply_spatial_effects( + image, + film_format_mm, + couplerData: Couplers, + interlayerData: Interlayer, + halationData: Halation, + image_width_px, +): + """Applies diffusion blur and halation.""" + # Combine diffusion effects (assuming they add quadratically in terms of sigma) + total_diffusion_um = np.sqrt( + couplerData.diffusion_um**2 + interlayerData.diffusion_um**2 + ) + + if total_diffusion_um > EPSILON: + sigma_pixels_diffusion = um_to_pixels( + total_diffusion_um, image_width_px, film_format_mm + ) + if sigma_pixels_diffusion > EPSILON: + print( + f"Applying diffusion blur: sigma={sigma_pixels_diffusion:.2f} pixels ({total_diffusion_um:.1f} um)" + ) + # Apply blur to the linear image data + image = gaussian_filter( + image, + sigma=[sigma_pixels_diffusion, sigma_pixels_diffusion, 0], + mode="nearest", + ) # Blur R, G, B independently + image = np.clip(image, 0.0, 1.0) # Keep values in range + + # --- 2. Apply Halation --- + # This simulates light scattering back through the emulsion + halation_applied = False + blurred_image_halation = np.copy( + image + ) # Start with potentially diffusion-blurred image + + strengths = [ + halationData.strength.r, + halationData.strength.g, + halationData.strength.b, + ] + sizes_um = [ + halationData.size_um.r, + halationData.size_um.g, + halationData.size_um.b, + ] + + for i in range(3): + strength = strengths[i] + size_um = sizes_um[i] + if strength > EPSILON and size_um > EPSILON: + sigma_pixels_halation = um_to_pixels( + size_um, image_width_px, film_format_mm + ) + if sigma_pixels_halation > EPSILON: + halation_applied = True + print( + f"Applying halation blur (Channel {i}): sigma={sigma_pixels_halation:.2f} pixels ({size_um:.1f} um), strength={strength:.3f}" + ) + # Blur only the current channel for halation effect + channel_blurred = gaussian_filter( + image[..., i], sigma=sigma_pixels_halation, mode="nearest" + ) + # Add the blurred channel back, weighted by strength + blurred_image_halation[..., i] = ( + image[..., i] * (1.0 - strength) + channel_blurred * strength + ) + + if halation_applied: + # Clip final result after halation + image = np.clip(blurred_image_halation, 0.0, 1.0) + + return image + + +def main(): + parser = argparse.ArgumentParser( + description="Simulate film stock color characteristics using a datasheet JSON." + ) + parser.add_argument( + "input_image", + help="Path to the input RGB image (e.g., PNG, TIFF). Assumed linear RGB.", + ) + parser.add_argument("datasheet_json", help="Path to the film datasheet JSON file.") + parser.add_argument("output_image", help="Path to save the output emulated image.") + + args = parser.parse_args() + + # --- Load Datasheet --- + print(f"Loading datasheet: {args.datasheet_json}") + datasheet: FilmDatasheet | None = parse_datasheet_json(args.datasheet_json) + if datasheet is None: + sys.exit(1) + print( + f"Simulating: {datasheet.info.name} ({datasheet.info.format_mm}mm) (v{datasheet.info.version})\n\t{datasheet.info.description}" + ) + + import pprint + pprint.pp(datasheet) + + # --- Load Input Image --- + print(f"Loading input image: {args.input_image}") + try: + # For DNG files, force reading the raw image data, not the embedded thumbnail + if ( + args.input_image.lower().endswith(".dng") + or args.input_image.lower().endswith(".raw") + or args.input_image.lower().endswith(".arw") + ): + print("Detected Camera RAW file, reading raw image data...") + with rawpy.imread(args.input_image) as raw: + image_raw = raw.postprocess( + demosaic_algorithm=rawpy.DemosaicAlgorithm.AHD, # type: ignore + output_bps=16, # Use 16-bit output for better precision + use_camera_wb=True, # Use camera white balance + no_auto_bright=True, # Disable auto brightness adjustment + output_color=rawpy.ColorSpace.sRGB, # type: ignore + ) + # If the image has more than 3 channels, try to select the first 3 (RGB) + if image_raw.ndim == 3 and image_raw.shape[-1] > 3: + image_raw = image_raw[..., :3] + else: + image_raw = iio.imread(args.input_image) + except FileNotFoundError: + print( + f"Error: Input image file not found at {args.input_image}", file=sys.stderr + ) + sys.exit(1) + except Exception as e: + print(f"Error reading input image: {e}", file=sys.stderr) + sys.exit(1) + + # Check if image is likely sRGB (8-bit or 16-bit integer types are usually sRGB) + is_srgb = image_raw.dtype in (np.uint8, np.uint16) + if is_srgb: + print("Input image appears to be sRGB. Linearizing...") + # Convert to float in [0,1] + if image_raw.dtype == np.uint8: + image_float = image_raw.astype(np.float64) / 255.0 + elif image_raw.dtype == np.uint16: + image_float = image_raw.astype(np.float64) / 65535.0 + else: + image_float = image_raw.astype(np.float64) + + # sRGB to linear conversion + def srgb_to_linear(c): + c = np.clip(c, 0.0, 1.0) + return np.where(c <= 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4) + + image_raw = srgb_to_linear(image_float) + else: + print("Input image is assumed to be linear RGB.") + + # --- Prepare Image Data --- + # Convert to float64 for precision, handle different input types + if image_raw.dtype == np.uint8: + image_linear = image_raw.astype(np.float64) / 255.0 + elif image_raw.dtype == np.uint16: + image_linear = image_raw.astype(np.float64) / 65535.0 + elif image_raw.dtype == np.float32: + image_linear = image_raw.astype(np.float64) + elif image_raw.dtype == np.float64: + image_linear = image_raw + else: + print(f"Error: Unsupported image data type: {image_raw.dtype}", file=sys.stderr) + sys.exit(1) + + # Discard alpha channel if present + if image_linear.shape[-1] == 4: + print("Discarding alpha channel.") + image_linear = image_linear[..., :3] + elif image_linear.shape[-1] != 3: + print( + f"Error: Input image must be RGB (shape {image_linear.shape} not supported).", + file=sys.stderr, + ) + sys.exit(1) + + # Ensure input is non-negative + image_linear = np.maximum(image_linear, 0.0) + print(f"Input image dimensions: {image_linear.shape}") + image_width_px = image_linear.shape[1] + + # --- Pipeline Steps --- + + # 1. Convert Linear RGB to Log Exposure (LogE) + # Map linear 0.18 to the specified middle_gray_logE from the datasheet + print("Converting linear RGB to Log Exposure...") + middle_gray_logE = float(datasheet.properties.calibration.middle_gray_logE) + # Add epsilon inside log10 to handle pure black pixels + log_exposure_rgb = middle_gray_logE + np.log10(image_linear / 0.18 + EPSILON) + # Note: Values below 0.18 * 10**(hd_data['LogE'][0] - middle_gray_logE) + # or above 0.18 * 10**(hd_data['LogE'][-1] - middle_gray_logE) + # will map outside the H&D curve's defined LogE range and rely on clamping/extrapolation. + + # 2. Apply H&D Curves (Tonal Mapping + Balance Shifts + Gamma/Contrast) + print("Applying H&D curves...") + density_rgb = apply_hd_curves( + log_exposure_rgb, + datasheet.processing, + datasheet.properties.curves.hd, + middle_gray_logE, + ) + + # 3. Convert Density back to Linear Transmittance + # Higher density means lower transmittance + print("Converting density to linear transmittance...") + # Add density epsilon? Usually density floor (Dmin) handles this. + linear_transmittance = 10.0 ** (-density_rgb) + # Normalize transmittance? Optional, assumes Dmin corresponds roughly to max transmittance 1.0 + # Could normalize relative to Dmin from the curves if needed. + # linear_transmittance = linear_transmittance / (10.0**(-np.array([hd_data['Density_R'][0], hd_data['Density_G'][0], hd_data['Density_B'][0]]))) + linear_transmittance = np.clip(linear_transmittance, 0.0, 1.0) + + # 4. Apply Spatial Effects (Diffusion Blur, Halation) + print("Applying spatial effects (diffusion, halation)...") + # Apply these effects in the linear domain + linear_post_spatial = apply_spatial_effects( + linear_transmittance, + datasheet.info.format_mm, + datasheet.properties.couplers, + datasheet.properties.interlayer, + datasheet.properties.halation, + image_width_px, + ) + + # 5. Apply Saturation Adjustment (Approximating Coupler Effects) + print("Applying saturation adjustment...") + coupler_amount = datasheet.properties.couplers.amount + # Assuming coupler_amount directly scales saturation factor. + # Values > 1 increase saturation, < 1 decrease. + linear_post_saturation = apply_saturation_rgb(linear_post_spatial, coupler_amount) + + # --- Final Output Conversion --- + print("Converting to output format...") + # Clip final result and convert to uint8 + if args.output_image.lower().endswith(".tiff"): + output_image_uint8 = ( + np.clip(linear_post_saturation, 0.0, 1.0) * 65535.0 + ).astype(np.uint16) + else: + output_image_uint8 = (np.clip(linear_post_saturation, 0.0, 1.0) * 255.0).astype( + np.uint8 + ) + + # --- Save Output Image --- + print(f"Saving output image: {args.output_image}") + try: + if args.output_image.lower().endswith((".tiff", ".tif")): + # Use imageio for standard formats + iio.imwrite(args.output_image, output_image_uint8) + elif args.output_image.lower().endswith(".png"): + iio.imwrite(args.output_image, output_image_uint8, format="PNG") + else: + iio.imwrite(args.output_image, output_image_uint8, quality=95) + print("Done.") + except Exception as e: + print(f"Error writing output image: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/filmgrain b/filmgrain new file mode 100755 index 0000000..db5b038 --- /dev/null +++ b/filmgrain @@ -0,0 +1,432 @@ +#!/usr/bin/env -S uv run --script +# /// script +# dependencies = [ +# "numpy", +# "scipy", +# "Pillow", +# "imageio", +# "warp-lang", +# "rawpy", +# ] +# /// + + +import warp as wp +import numpy as np +import math +import argparse +import imageio.v3 as iio +from scipy.integrate import quad +from scipy.signal.windows import gaussian # For creating Gaussian kernel +import rawpy + +wp.init() + +# --- Helper functions based on the paper --- + +def save_debug_image(wp_array, filename): + """Saves a Warp 2D array as a grayscale image, scaling values.""" + try: + numpy_array = wp_array.numpy() + min_val, max_val = np.min(numpy_array), np.max(numpy_array) + if max_val > min_val: + norm_array = (numpy_array - min_val) / (max_val - min_val) + else: + norm_array = np.full_like(numpy_array, 0.5) + + img_uint8 = (norm_array * 255.0).clip(0, 255).astype(np.uint8) + iio.imwrite(filename, img_uint8) + print(f"Debug image saved to {filename}") + except Exception as e: + print(f"Error saving debug image {filename}: {e}") + + +@wp.func +def w_func(x: float): + if x >= 2.0: + return 0.0 + elif x < 0.0: + return 1.0 + else: + arg = x / 2.0 + if arg > 1.0: + arg = 1.0 + elif arg < -1.0: + arg = -1.0 + sqrt_arg = 4.0 - x * x + if sqrt_arg < 0.0: + sqrt_val = 0.0 + else: + sqrt_val = wp.sqrt(sqrt_arg) # This should be sqrt(1 - (x/2)^2) for unit disk overlap area formula + + # Corrected w(x) based on overlap area of two unit disks divided by pi + # overlap_area = 2 * acos(x/2) - x * sqrt(1 - (x/2)^2) + # w(x) = overlap_area / pi + # Ensure acos argument is clamped + acos_arg = x / 2.0 + if acos_arg > 1.0: acos_arg = 1.0 + if acos_arg < -1.0: acos_arg = -1.0 + + sqrt_term_arg = 1.0 - acos_arg * acos_arg + if sqrt_term_arg < 0.0: sqrt_term_arg = 0.0 + + overlap_area_over_pi = ( + 2.0 * wp.acos(acos_arg) - x * wp.sqrt(sqrt_term_arg) + ) / math.pi + return overlap_area_over_pi + + +@wp.func +def CB_const_radius_unit(u_pixel: float, x: float): + safe_u = wp.min(u_pixel, 0.99999) + if safe_u < 0.0: + safe_u = 0.0 + + one_minus_u = 1.0 - safe_u + if one_minus_u <= 1e-9: # Avoid pow(small_negative_base) or pow(0, neg_exponent from wx) + return 0.0 + + wx = w_func(x) + # Ensure 2.0 - wx does not lead to issues if wx is large, though wx should be <= 1 + exponent = 2.0 - wx # type: ignore + + term1 = wp.pow(one_minus_u, exponent) + term2 = one_minus_u * one_minus_u + return term1 - term2 + + +def integrand_variance(x, u_pixel): + if x < 0: return 0.0 + if x >= 2.0: return 0.0 + + safe_u = np.clip(u_pixel, 0.0, 0.99999) + one_minus_u = 1.0 - safe_u + if one_minus_u <= 1e-9: return 0.0 + + acos_arg = x / 2.0 + if acos_arg > 1.0: acos_arg = 1.0 + if acos_arg < -1.0: acos_arg = -1.0 + + sqrt_term_arg = 1.0 - acos_arg * acos_arg + if sqrt_term_arg < 0.0: sqrt_term_arg = 0.0 + + wx = (2.0 * np.arccos(acos_arg) - x * np.sqrt(sqrt_term_arg)) / np.pi + + cb = np.power(one_minus_u, 2.0 - wx) - np.power(one_minus_u, 2.0) + return cb * x + + +def precompute_variance_lut(num_u_samples=256): + """ + Precomputes the integral I(u) = ∫[0 to 2] CB(u, x, 1) * x dx for different u values. + Creates a LUT with num_u_samples + 1 entries (for u=0 to u=1 inclusive). + """ + print(f"Precomputing variance LUT with {num_u_samples+1} entries...") + # Samples u from 0 to 1 inclusive for the LUT + u_values_for_lut = np.linspace(0.0, 1.0, num_u_samples + 1, endpoint=True) + lut = np.zeros(num_u_samples + 1, dtype=np.float32) + + for i, u in enumerate(u_values_for_lut): + result, error = quad( + integrand_variance, 0, 2, args=(u,), epsabs=1e-6, limit=100 + ) + if result < 0: result = 0.0 + lut[i] = result + if i % ((num_u_samples + 1) // 10) == 0 : + print(f" LUT progress: {i}/{num_u_samples+1}") + print("Variance LUT computed.") + return lut + + +@wp.kernel +def generate_noise_kernel( + u_image: wp.array2d(dtype=float), + variance_lut: wp.array(dtype=float), + noise_out: wp.array2d(dtype=float), + mu_r: float, + sigma_filter: float, + seed: int, +): + ix, iy = wp.tid() + height = u_image.shape[0] + width = u_image.shape[1] + if ix >= height or iy >= width: return + + lut_size = variance_lut.shape[0] + u_val = u_image[ix, iy] + + lut_pos = u_val * float(lut_size - 1) + lut_index0 = int(lut_pos) + lut_index0 = wp.min(wp.max(lut_index0, 0), lut_size - 2) # Ensure lut_index0 and lut_index0+1 are valid + lut_index1 = lut_index0 + 1 + t = lut_pos - float(lut_index0) + if t < 0.0: t = 0.0 # Clamp t to avoid issues with precision + if t > 1.0: t = 1.0 + + integral_val = wp.lerp(variance_lut[lut_index0], variance_lut[lut_index1], t) + + var_bp = 0.0 + if sigma_filter > 1e-6 and mu_r > 1e-6: # mu_r check also important + var_bp = wp.max(0.0, (mu_r * mu_r) / (2.0 * sigma_filter * sigma_filter) * integral_val) + + std_dev = wp.sqrt(var_bp) + state = wp.rand_init(seed, ix * width + iy + seed) # Add seed to sequence as well + noise_sample = wp.randn(state) * std_dev + noise_out[ix, iy] = noise_sample + +@wp.kernel +def convolve_2d_kernel( + input_array: wp.array2d(dtype=float), + kernel: wp.array(dtype=float), + kernel_radius: int, + output_array: wp.array2d(dtype=float), +): + ix, iy = wp.tid() + height = input_array.shape[0] + width = input_array.shape[1] + if ix >= height or iy >= width: return + + kernel_dim = 2 * kernel_radius + 1 + accum = float(0.0) + + for ky_offset in range(kernel_dim): + for kx_offset in range(kernel_dim): + k_idx = ky_offset * kernel_dim + kx_offset + weight = kernel[k_idx] + + # Image coordinates to sample from + read_row = ix + (ky_offset - kernel_radius) # Corrected: ix is row, iy is col usually + read_col = iy + (kx_offset - kernel_radius) + + clamped_row = wp.max(0, wp.min(read_row, height - 1)) + clamped_col = wp.max(0, wp.min(read_col, width - 1)) + + sample_val = input_array[clamped_row, clamped_col] + accum += weight * sample_val + output_array[ix, iy] = accum + +@wp.kernel +def add_rgb_noise_and_clip_kernel( + r_in: wp.array2d(dtype=float), + g_in: wp.array2d(dtype=float), + b_in: wp.array2d(dtype=float), + noise_r: wp.array2d(dtype=float), + noise_g: wp.array2d(dtype=float), + noise_b: wp.array2d(dtype=float), + r_out: wp.array2d(dtype=float), + g_out: wp.array2d(dtype=float), + b_out: wp.array2d(dtype=float)): + """Adds channel-specific filtered noise to each channel and clips.""" + ix, iy = wp.tid() # type: ignore + + height = r_in.shape[0] + width = r_in.shape[1] + if ix >= height or iy >= width: return + + + r_out[ix, iy] = wp.clamp(r_in[ix, iy] + noise_r[ix, iy], 0.0, 1.0) # type: ignore + g_out[ix, iy] = wp.clamp(g_in[ix, iy] + noise_g[ix, iy], 0.0, 1.0) # type: ignore + b_out[ix, iy] = wp.clamp(b_in[ix, iy] + noise_b[ix, iy], 0.0, 1.0) # type: ignore + + +def create_gaussian_kernel_2d(sigma, radius): + kernel_size = 2 * radius + 1 + g = gaussian(kernel_size, sigma, sym=True) # Ensure symmetry for odd kernel_size + kernel_2d = np.outer(g, g) + sum_sq = np.sum(kernel_2d**2) + if sum_sq > 1e-9: # Avoid division by zero if kernel is all zeros + kernel_2d /= np.sqrt(sum_sq) + return kernel_2d.flatten().astype(np.float32) + + +def render_film_grain(image_path, mu_r, sigma_filter, output_path, seed=42, mono=False): + try: + if image_path.lower().endswith('.arw') or image_path.lower().endswith('.dng'): + # Use rawpy for TIFF images to handle metadata correctly + with rawpy.imread(image_path) as raw: + img_np = raw.postprocess( + use_camera_wb=True, + no_auto_bright=True, + output_bps=16, + half_size=False, + gamma=(1.0, 1.0), # No gamma correction + ) + elif image_path.lower().endswith('.tiff') or image_path.lower().endswith('.tif') or image_path.lower().endswith('.png') or image_path.lower().endswith('.jpg') or image_path.lower().endswith('.jpeg'): + img_np = iio.imread(image_path) + else: + raise ValueError("Unsupported image format. Please use TIFF, PNG, JPG, or RAW (ARW, DNG) formats.") + except FileNotFoundError: + print(f"Error: Input image not found at {image_path}") + return + except Exception as e: + print(f"Error loading image: {e}") + return + + if img_np.ndim == 2: + img_np = img_np[..., np.newaxis] + if img_np.shape[2] == 4: + img_np = img_np[..., :3] + + + if img_np.dtype == np.uint8: + img_np_float = img_np.astype(np.float32) / 255.0 + elif img_np.dtype == np.uint16: + img_np_float = img_np.astype(np.float32) / 65535.0 + else: + img_np_float = img_np.astype(np.float32) + + height, width, channels = img_np_float.shape + + print(f"Input image: {width}x{height}x{channels}") + print(f"Parameters: μr = {mu_r}, σ_filter = {sigma_filter}") + + # Use 256 u_samples for LUT, resulting in 257 entries (0 to 256 for u=0 to u=1) + variance_lut_np = precompute_variance_lut(num_u_samples=256) + variance_lut_wp = wp.array(variance_lut_np, dtype=float, device="cuda") + + kernel_radius = max(1, int(np.ceil(3 * sigma_filter))) + kernel_np = create_gaussian_kernel_2d(sigma_filter, kernel_radius) + kernel_wp = wp.array(kernel_np, dtype=float, device="cuda") + print(f"Using Gaussian filter h with sigma={sigma_filter}, radius={kernel_radius}") + + # --- Prepare original channel data on GPU --- + r_original_wp = wp.array(img_np_float[:, :, 0], dtype=float, device="cuda") + if channels == 3: + g_original_wp = wp.array(img_np_float[:, :, 1], dtype=float, device="cuda") + b_original_wp = wp.array(img_np_float[:, :, 2], dtype=float, device="cuda") + else: # Grayscale input + g_original_wp = r_original_wp + b_original_wp = r_original_wp + + # --- Allocate noise arrays on GPU --- + noise_r_unfiltered_wp = wp.empty_like(r_original_wp) + noise_g_unfiltered_wp = wp.empty_like(g_original_wp) + noise_b_unfiltered_wp = wp.empty_like(b_original_wp) + + noise_r_filtered_wp = wp.empty_like(r_original_wp) + noise_g_filtered_wp = wp.empty_like(g_original_wp) + noise_b_filtered_wp = wp.empty_like(b_original_wp) + + if mono: + if channels == 1: + img_gray_np = img_np_float[:, :, 0] + else: + # Standard RGB to Luminance weights + img_gray_np = (0.299 * img_np_float[:, :, 0] + + 0.587 * img_np_float[:, :, 1] + + 0.114 * img_np_float[:, :, 2]) + print("Generating monochromatic noise...") + u_gray_wp = wp.array(img_gray_np, dtype=float, device="cuda") + noise_image_wp = wp.empty_like(u_gray_wp) + wp.launch(kernel=generate_noise_kernel, + dim=(height, width), + inputs=[u_gray_wp, variance_lut_wp, noise_image_wp, mu_r, sigma_filter, seed], + device="cuda") + noise_filtered_wp = wp.empty_like(u_gray_wp) + wp.launch(kernel=convolve_2d_kernel, + dim=(height, width), + inputs=[noise_image_wp, kernel_wp, kernel_radius, noise_filtered_wp], + device="cuda") + noise_r_filtered_wp.assign(noise_filtered_wp) + noise_g_filtered_wp.assign(noise_filtered_wp) + noise_b_filtered_wp.assign(noise_filtered_wp) + else: + # --- Process R Channel --- + print("Processing R channel...") + wp.launch(kernel=generate_noise_kernel, dim=(height, width), + inputs=[r_original_wp, variance_lut_wp, noise_r_unfiltered_wp, mu_r, sigma_filter, seed], device="cuda") + wp.launch(kernel=convolve_2d_kernel, dim=(height, width), + inputs=[noise_r_unfiltered_wp, kernel_wp, kernel_radius, noise_r_filtered_wp], device="cuda") + + if channels == 3: + # --- Process G Channel --- + print("Processing G channel...") + wp.launch(kernel=generate_noise_kernel, dim=(height, width), + inputs=[g_original_wp, variance_lut_wp, noise_g_unfiltered_wp, mu_r, sigma_filter, seed + 1], device="cuda") # Offset seed + wp.launch(kernel=convolve_2d_kernel, dim=(height, width), + inputs=[noise_g_unfiltered_wp, kernel_wp, kernel_radius, noise_g_filtered_wp], device="cuda") + + # --- Process B Channel --- + print("Processing B channel...") + wp.launch(kernel=generate_noise_kernel, dim=(height, width), + inputs=[b_original_wp, variance_lut_wp, noise_b_unfiltered_wp, mu_r, sigma_filter, seed + 2], device="cuda") # Offset seed + wp.launch(kernel=convolve_2d_kernel, dim=(height, width), + inputs=[noise_b_unfiltered_wp, kernel_wp, kernel_radius, noise_b_filtered_wp], device="cuda") + else: # Grayscale: copy R channel's filtered noise to G and B components + noise_g_filtered_wp.assign(noise_r_filtered_wp) # Use assign for Warp arrays + noise_b_filtered_wp.assign(noise_r_filtered_wp) + + + # --- Add noise and clip --- + print("Adding noise to channels and clipping...") + r_output_wp = wp.empty_like(r_original_wp) + g_output_wp = wp.empty_like(g_original_wp) + b_output_wp = wp.empty_like(b_original_wp) + + wp.launch(kernel=add_rgb_noise_and_clip_kernel, + dim=(height, width), + inputs=[r_original_wp, g_original_wp, b_original_wp, + noise_r_filtered_wp, noise_g_filtered_wp, noise_b_filtered_wp, + r_output_wp, g_output_wp, b_output_wp], + device="cuda") + + # --- Copy back to host --- + output_img_np = np.zeros((height,width,3), dtype=np.float32) # Always create 3-channel output buffer + output_img_np[:, :, 0] = r_output_wp.numpy() + output_img_np[:, :, 1] = g_output_wp.numpy() + output_img_np[:, :, 2] = b_output_wp.numpy() + + try: + if output_path.lower().endswith('.tiff') or output_path.lower().endswith('.tif'): + output_img_uint16 = (output_img_np * 65535.0).clip(0, 65535).astype(np.uint16) + iio.imwrite(output_path, output_img_uint16) + print(f"Output image saved to {output_path}") + elif output_path.lower().endswith('.png'): + output_img_uint8 = (output_img_np * 255.0).clip(0, 255).astype(np.uint8) + iio.imwrite(output_path, output_img_uint8) + print(f"Output image saved to {output_path}") + elif output_path.lower().endswith('.jpg') or output_path.lower().endswith('.jpeg'): + output_img_uint8 = (output_img_np * 255.0).clip(0, 255).astype(np.uint8) + iio.imwrite(output_path, output_img_uint8, quality=95) + print(f"Output image saved to {output_path}") + except Exception as e: + print(f"Error saving image: {e}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Apply realistic film grain (Zhang et al. 2023 method)." + ) + parser.add_argument("input_image", help="Path to the input image (TIFF, PNG, JPG, or RAW (ARW/DNG) format)") + parser.add_argument("output_image", help="Path to save the output image (TIFF (16-bit), PNG, JPG format)") + parser.add_argument( + "--mu_r", type=float, default=0.1, help="Mean grain radius (relative to pixel size)" + ) + parser.add_argument( + "--sigma", + type=float, + default=0.8, + help="Standard deviation of the Gaussian Filter for noise blurring (sigma_filter).", + ) + parser.add_argument( + "--seed", type=int, default=42, help="Random seed for noise generation" + ) + parser.add_argument( + "--mono", action="store_true", help="Apply monochrome film grain across channels based on luminance", default=False + ) + + args = parser.parse_args() + + if args.mu_r <= 0: + print("Warning: mu_r should be positive. Using default 0.1") + args.mu_r = 0.1 + if args.sigma <= 0: + print("Warning: sigma_filter should be positive. Using default 0.8") + args.sigma = 0.8 + if args.sigma < 3 * args.mu_r: + print( + f"Warning: sigma_filter ({args.sigma}) is less than 3*mu_r ({3 * args.mu_r:.2f}). Approximations in the model might be less accurate." + ) + + render_film_grain( + args.input_image, args.mu_r, args.sigma, args.output_image, args.seed, args.mono + ) \ No newline at end of file diff --git a/filmscan b/filmscan new file mode 100755 index 0000000..ea35391 --- /dev/null +++ b/filmscan @@ -0,0 +1,469 @@ +#!/usr/bin/env -S uv run --script +# /// script +# dependencies = [ +# "numpy", +# "scipy", +# "Pillow", +# "imageio", +# "colour-science", +# ] +# /// + +import numpy as np +import imageio.v3 as iio +import colour +from scipy.ndimage import uniform_filter, maximum_filter, minimum_filter +import argparse +import os +from pathlib import Path + +# --- Constants from negadoctor.c --- +THRESHOLD = 2.3283064365386963e-10 +# LOG2_TO_LOG10 = 0.3010299956 # This is log10(2) +LOG10_2 = np.log10(2.0) # More precise + +# --- Default parameters (from dt_iop_negadoctor_params_t defaults) --- +# These are for parameters NOT being auto-detected +DEFAULT_WB_HIGH = np.array([1.0, 1.0, 1.0], dtype=np.float32) +DEFAULT_WB_LOW = np.array([1.0, 1.0, 1.0], dtype=np.float32) +DEFAULT_GAMMA = 4.0 +DEFAULT_SOFT_CLIP = 0.75 +# Film stock is implicitly color by using 3-channel Dmin etc. + +# --- Utility Functions --- + +def find_patch_average(image: np.ndarray, center_y: int, center_x: int, patch_size: int) -> np.ndarray: + """Averages pixel values in a square patch around a center point.""" + half_patch = patch_size // 2 + y_start = np.clip(center_y - half_patch, 0, image.shape[0] - 1) + y_end = np.clip(center_y + half_patch + 1, 0, image.shape[0]) # +1 for slice + x_start = np.clip(center_x - half_patch, 0, image.shape[1] - 1) + x_end = np.clip(center_x + half_patch + 1, 0, image.shape[1]) + + if y_start >= y_end or x_start >= x_end: # Should not happen with proper clipping + return image[center_y, center_x] + + patch = image[y_start:y_end, x_start:x_end] + return np.mean(patch, axis=(0, 1)) + +def get_representative_patch_value(image: np.ndarray, mode: str = 'brightest', + patch_size_ratio: float = 1/64, min_patch_size: int = 8, + max_patch_size: int = 64) -> np.ndarray: + """ + Finds the brightest or darkest small patch in an image. + 'Brightest' on a negative is the film base. + 'Darkest' on a negative is a highlight in the scene. + The mode refers to the luma/intensity. + """ + if image.ndim != 3 or image.shape[2] != 3: + raise ValueError("Image must be an RGB image (H, W, 3)") + + patch_size = int(min(image.shape[0], image.shape[1]) * patch_size_ratio) + patch_size = np.clip(patch_size, min_patch_size, max_patch_size) + patch_size = max(1, patch_size // 2 * 2 + 1) # Ensure odd for easier centering if needed + + # Use a uniform filter to get local averages, speeds up finding good candidates + # Consider image intensity for finding min/max spot + if image.shape[0] < patch_size or image.shape[1] < patch_size: + # Image too small for filtering, find single pixel and sample around it + intensity_map = np.mean(image, axis=2) + if mode == 'brightest': + center_y, center_x = np.unravel_index(np.argmax(intensity_map), intensity_map.shape) + else: # darkest + center_y, center_x = np.unravel_index(np.argmin(intensity_map), intensity_map.shape) + return find_patch_average(image, center_y, center_x, patch_size) + + + # For larger images, we can use filters + # Calculate a proxy for patch brightness/darkness (e.g. sum of RGB in patch) + # A simple way is to find min/max of filtered image + # For more robustness, one might sample multiple candidate patches + # For simplicity here, find global min/max pixel and sample patch around it. + + intensity_map = np.mean(image, axis=2) # Luminance proxy + + if mode == 'brightest': + # Find the brightest pixel + flat_idx = np.argmax(intensity_map) + else: # darkest + # Find the darkest pixel + flat_idx = np.argmin(intensity_map) + + center_y, center_x = np.unravel_index(flat_idx, intensity_map.shape) + + # Refine patch location: average a small area around the found extreme pixel + # to reduce noise sensitivity. + # The patch size for averaging: + avg_patch_size = max(3, patch_size // 4) # A smaller patch for local averaging + + return find_patch_average(image, center_y, center_x, avg_patch_size) + + +# --- Automated Parameter Calculation Functions --- +# These functions will use the original input image (img_aces_negative) for sampling, +# as implied by the C GUI code `self->picked_color...` + +def auto_calculate_dmin(img_aces_negative: np.ndarray, **kwargs) -> np.ndarray: + """Dmin is the color of the film base (brightest part of the negative).""" + # In the C code, Dmin values are typically > 0, often around [1.0, 0.45, 0.25] for color. + # These are divisors, so they should not be zero. + # The input image is 0-1. A very bright film base might be e.g. 0.8, 0.7, 0.6 + # The C code Dmin seems to be in a different scale or used differently. + # Let's assume picked_color is directly used as Dmin. + # "Dmin[0] = 1.00f; Dmin[1] = 0.45f; Dmin[2] = 0.25f;" default + # However, `apply_auto_Dmin` sets `p->Dmin[k] = self->picked_color[k]`. + # If `picked_color` is from a 0-1 range image, Dmin will also be 0-1. + # This implies the input image is used directly. + dmin = get_representative_patch_value(img_aces_negative, mode='brightest', **kwargs) + return np.maximum(dmin, THRESHOLD) # Ensure Dmin is not too small + +def auto_calculate_dmax(img_aces_negative: np.ndarray, dmin_param: np.ndarray, **kwargs) -> float: + """ + D_max = v_maxf(log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD))) + picked_color_min is the darkest patch on the negative (scene highlight). + """ + darkest_negative_patch = get_representative_patch_value(img_aces_negative, mode='darkest', **kwargs) + darkest_negative_patch = np.maximum(darkest_negative_patch, THRESHOLD) + + # Ensure dmin_param and darkest_negative_patch are broadcastable if dmin_param is scalar + dmin_param_rgb = np.array(dmin_param, dtype=np.float32) + if dmin_param_rgb.ndim == 0: + dmin_param_rgb = np.full(3, dmin_param_rgb, dtype=np.float32) + + log_arg = dmin_param_rgb / darkest_negative_patch + rgb_dmax_contrib = np.log10(log_arg) + d_max = np.max(rgb_dmax_contrib) + return max(d_max, 0.1) # D_max must be positive + +def auto_calculate_offset(img_aces_negative: np.ndarray, dmin_param: np.ndarray, d_max_param: float, **kwargs) -> float: + """ + p->offset = v_minf(log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD)) / p->D_max) + picked_color_max is the brightest patch on the negative (scene shadow if Dmin is elsewhere, or Dmin itself). + For this context, it should be the part of the image that will become the deepest black after inversion, + which is *not* Dmin. So it's the brightest part of the *exposed* negative area. + This is tricky. If Dmin is picked from unexposed film edge, then picked_color_max is the brightest *image* area. + If Dmin is picked from brightest *image* area (no unexposed edge), then this becomes circular. + Let's assume Dmin was found correctly from the film base (unexposed or lightest part). + Then `picked_color_max` here refers to the brightest *actual image content* on the negative, + which will become the *darkest shadows* in the positive. + So we still use 'brightest' mode for `get_representative_patch_value` on the negative. + """ + brightest_negative_patch = get_representative_patch_value(img_aces_negative, mode='brightest', **kwargs) + brightest_negative_patch = np.maximum(brightest_negative_patch, THRESHOLD) + + dmin_param_rgb = np.array(dmin_param, dtype=np.float32) + if dmin_param_rgb.ndim == 0: + dmin_param_rgb = np.full(3, dmin_param_rgb, dtype=np.float32) + + log_arg = dmin_param_rgb / brightest_negative_patch + rgb_offset_contrib = np.log10(log_arg) / d_max_param + offset = np.min(rgb_offset_contrib) + return offset # Can be negative + +def auto_calculate_paper_black(img_aces_negative: np.ndarray, dmin_param: np.ndarray, d_max_param: float, + offset_param: float, wb_high_param: np.ndarray, wb_low_param: np.ndarray, + **kwargs) -> float: + """ + p->black = v_maxf(RGB) + RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD)); + RGB[c] *= p->wb_high[c] / p->D_max; + RGB[c] += p->wb_low[c] * p->offset * p->wb_high[c]; # Error in my C-analysis: This wb_high is p->wb_high, not d->wb_high + # d->offset already has p->wb_high. + # Corrected: RGB[c] += p->wb_low[c] * p->offset * (p->wb_high[c] / p->D_max); -- NO + # C code: d->offset[c] = p->wb_high[c] * p->offset * p->wb_low[c]; + # corrected_de[c] = (p->wb_high[c]/p->D_max) * log_density[c] + d->offset[c] + # This means the auto_black formula needs to use the same terms for consistency. + Let's re-evaluate the C `apply_auto_black`: + RGB_log_density_term[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD)) + RGB_density_corrected_term[c] = (p->wb_high[c] / p->D_max) * RGB_log_density_term[c] + \ + (p->wb_high[c] * p->offset * p->wb_low[c]) + RGB[c] = 0.1f - (1.0f - fast_exp10f(RGB_density_corrected_term[c])); + p->black = v_maxf(RGB); + `picked_color_max` is the brightest patch on the negative. + """ + brightest_negative_patch = get_representative_patch_value(img_aces_negative, mode='brightest', **kwargs) + brightest_negative_patch = np.maximum(brightest_negative_patch, THRESHOLD) + + dmin_param_rgb = np.array(dmin_param, dtype=np.float32) + if dmin_param_rgb.ndim == 0: + dmin_param_rgb = np.full(3, dmin_param_rgb, dtype=np.float32) + + log_density_term = -np.log10(dmin_param_rgb / brightest_negative_patch) + + # This is `corrected_de` for the brightest_negative_patch + density_corrected_term = (wb_high_param / d_max_param) * log_density_term + \ + (wb_high_param * offset_param * wb_low_param) # This matches d->offset structure + + val_for_black_calc = 0.1 - (1.0 - np.power(10.0, density_corrected_term)) + paper_black = np.max(val_for_black_calc) + return paper_black + +def auto_calculate_print_exposure(img_aces_negative: np.ndarray, dmin_param: np.ndarray, d_max_param: float, + offset_param: float, paper_black_param: float, + wb_high_param: np.ndarray, wb_low_param: np.ndarray, + **kwargs) -> float: + """ + p->exposure = v_minf(RGB) + RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD)); + RGB[c] *= p->wb_high[c] / p->D_max; + RGB[c] += p->wb_low[c] * p->offset * p->wb_high_param[c]; // Similar correction as paper_black + // This should be p->wb_low[c] * d->offset[c] using the effective offset + // which is p->wb_low[c] * (p->offset * p->wb_high[c]) + Let's re-evaluate C `apply_auto_exposure`: + RGB_log_density_term[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD)) + RGB_density_corrected_term[c] = (p->wb_high[c] / p->D_max) * RGB_log_density_term[c] + \ + (p->wb_high[c] * p->offset * p->wb_low[c]) + RGB[c] = 0.96f / ( (1.0f - fast_exp10f(RGB_density_corrected_term[c])) + p->black ); + p->exposure = v_minf(RGB); + `picked_color_min` is the darkest patch on the negative. + """ + darkest_negative_patch = get_representative_patch_value(img_aces_negative, mode='darkest', **kwargs) + darkest_negative_patch = np.maximum(darkest_negative_patch, THRESHOLD) + + dmin_param_rgb = np.array(dmin_param, dtype=np.float32) + if dmin_param_rgb.ndim == 0: + dmin_param_rgb = np.full(3, dmin_param_rgb, dtype=np.float32) + + log_density_term = -np.log10(dmin_param_rgb / darkest_negative_patch) + + density_corrected_term = (wb_high_param / d_max_param) * log_density_term + \ + (wb_high_param * offset_param * wb_low_param) + + denominator = (1.0 - np.power(10.0, density_corrected_term)) + paper_black_param + # Avoid division by zero or very small numbers if denominator is problematic + denominator = np.where(np.abs(denominator) < THRESHOLD, np.sign(denominator + THRESHOLD) * THRESHOLD, denominator) + + val_for_exposure_calc = 0.96 / denominator + print_exposure = np.min(val_for_exposure_calc) + return max(print_exposure, 0.01) # Ensure exposure is positive + +# --- Core Negadoctor Process --- +def negadoctor_process(img_aces_negative: np.ndarray, + dmin_param: np.ndarray, + wb_high_param: np.ndarray, + wb_low_param: np.ndarray, + d_max_param: float, + offset_param: float, # scan exposure bias + paper_black_param: float, # paper black (density correction) + gamma_param: float, # paper grade (gamma) + soft_clip_param: float, # paper gloss (specular highlights) + print_exposure_param: float # print exposure adjustment + ) -> np.ndarray: + """ + Applies the negadoctor calculations based on `_process_pixel` and `commit_params`. + Input image and Dmin are expected to be in a compatible range (e.g., 0-1). + """ + # Ensure params are numpy arrays for broadcasting + dmin_param = np.array(dmin_param, dtype=np.float32).reshape(1, 1, 3) + wb_high_param = np.array(wb_high_param, dtype=np.float32).reshape(1, 1, 3) + wb_low_param = np.array(wb_low_param, dtype=np.float32).reshape(1, 1, 3) + + # From commit_params: + # d->wb_high[c] = p->wb_high[c] / p->D_max; + effective_wb_high = wb_high_param / d_max_param + + # d->offset[c] = p->wb_high[c] * p->offset * p->wb_low[c]; + # Note: p->offset is scalar offset_param + effective_offset = wb_high_param * offset_param * wb_low_param + + # d->black = -p->exposure * (1.0f + p->black); + # Note: p->exposure is scalar print_exposure_param, p->black is scalar paper_black_param + effective_paper_black = -print_exposure_param * (1.0 + paper_black_param) + + # d->soft_clip_comp = 1.0f - p->soft_clip; + soft_clip_comp = 1.0 - soft_clip_param + + # --- _process_pixel logic --- + # 1. Convert transmission to density using Dmin as a fulcrum + # density[c] = Dmin[c] / clamped[c]; + # log_density[c] = log2(density[c]) * -LOG2_to_LOG10 = -log10(density[c]) + clamped_input = np.maximum(img_aces_negative, THRESHOLD) + density = dmin_param / clamped_input + log_density = -np.log10(density) # This is log10(clamped_input / dmin_param) + + # 2. Correct density in log space + # corrected_de[c] = effective_wb_high[c] * log_density[c] + effective_offset[c]; + corrected_density = effective_wb_high * log_density + effective_offset + + # 3. Print density on paper + # print_linear[c] = -(exposure[c] * ten_to_x[c] + black[c]); + # exposure[c] is print_exposure_param, black[c] is effective_paper_black + # ten_to_x is 10^corrected_density + ten_to_corrected_density = np.power(10.0, corrected_density) + print_linear = -(print_exposure_param * ten_to_corrected_density + effective_paper_black) + print_linear = np.maximum(print_linear, 0.0) + + # 4. Apply paper grade (gamma) + # print_gamma = print_linear ^ gamma_param + print_gamma = np.power(print_linear, gamma_param) + + # 5. Compress highlights (soft clip) + # pix_out[c] = (print_gamma[c] > soft_clip[c]) + # ? soft_clip[c] + (1.0f - e_to_gamma[c]) * soft_clip_comp[c] + # : print_gamma[c]; + # where e_to_gamma[c] = exp(-(print_gamma[c] - soft_clip[c]) / soft_clip_comp[c]) + + # Avoid issues with soft_clip_comp being zero if soft_clip_param is 1.0 + if np.isclose(soft_clip_comp, 0.0): + output_pixels = np.where(print_gamma > soft_clip_param, soft_clip_param, print_gamma) + else: + exponent = -(print_gamma - soft_clip_param) / soft_clip_comp + # Clip exponent to avoid overflow in np.exp for very large negative print_gamma values + exponent = np.clip(exponent, -700, 700) # exp(-709) is ~0, exp(709) is ~inf + e_to_gamma = np.exp(exponent) + + compressed_highlights = soft_clip_param + (1.0 - e_to_gamma) * soft_clip_comp + output_pixels = np.where(print_gamma > soft_clip_param, compressed_highlights, print_gamma) + + return np.clip(output_pixels, 0.0, 1.0) # Final clip to 0-1 range + + +# --- Main Execution --- +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Python implementation of Darktable's Negadoctor module.") + parser.add_argument("input_file", help="Path to the input negative image (TIFF).") + parser.add_argument("output_file", help="Path to save the processed positive image.") + parser.add_argument("--patch_size_ratio", type=float, default=1/128, help="Ratio of image min dim for patch size in auto-detection.") # smaller default + parser.add_argument("--min_patch_size", type=int, default=8, help="Minimum patch size in pixels.") + parser.add_argument("--max_patch_size", type=int, default=32, help="Maximum patch size in pixels.") # smaller default + parser.add_argument("--aces_transform", default="ACEScg", help="ACES working space (e.g., ACEScg, ACEScc, ACEScct).") + parser.add_argument("--output_colorspace", default="sRGB", help="Colorspace for output image (e.g., sRGB, Display P3).") + + + args = parser.parse_args() + + print(f"Loading image: {args.input_file}") + try: + img_raw = iio.imread(args.input_file) + except Exception as e: + print(f"Error loading image: {e}") + exit(1) + + print(f"Original image dtype: {img_raw.dtype}, shape: {img_raw.shape}, min: {img_raw.min()}, max: {img_raw.max()}") + + if img_raw.dtype == np.uint16: + img_float = img_raw.astype(np.float32) / 65535.0 + elif img_raw.dtype == np.uint8: + img_float = img_raw.astype(np.float32) / 255.0 + elif img_raw.dtype == np.float32 or img_raw.dtype == np.float64: + if img_raw.max() > 1.01 : # Check if it's not already 0-1 + print("Warning: Input float image max > 1.0. Assuming it's not normalized. Clamping and scaling may occur.") + img_float = np.clip(img_raw, 0, None).astype(np.float32) # Needs proper scaling if not 0-1 + if img_float.max() > 1.0: # if it's still high, e.g. integer range float + if np.percentile(img_float, 99.9) < 256: # likely 8-bit range + img_float /= 255.0 + elif np.percentile(img_float, 99.9) < 65536: # likely 16-bit range + img_float /= 65535.0 + else: # unknown large float range + print("Warning: Unknown float range. Trying to normalize by max value.") + img_float /= img_float.max() + else: + img_float = img_raw.astype(np.float32) + else: + raise ValueError(f"Unsupported image dtype: {img_raw.dtype}") + + img_float = np.clip(img_float, 0.0, 1.0) + if img_float.ndim == 2: # Grayscale + img_float = np.stack([img_float]*3, axis=-1) # make 3-channel + + # Assuming input TIFF is sRGB encoded (common for scans unless specified) + # Convert to linear sRGB first, then to ACEScg + print("Converting to ACEScg...") + # img_linear_srgb = colour.gamma_correct(img_float, 1/2.2, 'ITU-R BT.709') # Approximate sRGB EOTF decoding + img_linear_srgb = colour.models.eotf_sRGB(img_float) # More accurate sRGB EOTF decoding + img_acescg = colour.RGB_to_RGB(img_linear_srgb, + colour.models.RGB_COLOURSPACE_sRGB, + colour.models.RGB_COLOURSPACE_ACESCG) + img_acescg = np.clip(img_acescg, 0.0, None) # ACEScg can have values > 1.0 for very bright sources + + print(f"Image in ACEScg: shape: {img_acescg.shape}, min: {img_acescg.min():.4f}, max: {img_acescg.max():.4f}, mean: {img_acescg.mean():.4f}") + + # Automated parameter detection + patch_kwargs = { + "patch_size_ratio": args.patch_size_ratio, + "min_patch_size": args.min_patch_size, + "max_patch_size": args.max_patch_size + } + + print("Auto-detecting parameters...") + param_dmin = auto_calculate_dmin(img_acescg, **patch_kwargs) + print(f" Dmin: {param_dmin}") + + param_d_max = auto_calculate_dmax(img_acescg, param_dmin, **patch_kwargs) + print(f" D_max: {param_d_max:.4f}") + + param_offset = auto_calculate_offset(img_acescg, param_dmin, param_d_max, **patch_kwargs) + print(f" Offset (Scan Bias): {param_offset:.4f}") + + param_paper_black = auto_calculate_paper_black(img_acescg, param_dmin, param_d_max, param_offset, + DEFAULT_WB_HIGH, DEFAULT_WB_LOW, **patch_kwargs) + print(f" Paper Black: {param_paper_black:.4f}") + + param_print_exposure = auto_calculate_print_exposure(img_acescg, param_dmin, param_d_max, param_offset, + param_paper_black, DEFAULT_WB_HIGH, DEFAULT_WB_LOW, + **patch_kwargs) + print(f" Print Exposure: {param_print_exposure:.4f}") + + # Perform Negadoctor processing + print("Applying Negadoctor process...") + img_processed_acescg = negadoctor_process( + img_acescg, + dmin_param=param_dmin, + wb_high_param=DEFAULT_WB_HIGH, + wb_low_param=DEFAULT_WB_LOW, + d_max_param=param_d_max, + offset_param=param_offset, + paper_black_param=param_paper_black, + gamma_param=DEFAULT_GAMMA, + soft_clip_param=DEFAULT_SOFT_CLIP, + print_exposure_param=param_print_exposure + ) + print(f"Processed (ACEScg): min: {img_processed_acescg.min():.4f}, max: {img_processed_acescg.max():.4f}, mean: {img_processed_acescg.mean():.4f}") + + + # Convert back to output colorspace (e.g., sRGB) + print(f"Converting from ACEScg to {args.output_colorspace}...") + if args.output_colorspace.upper() == 'SRGB': + output_cs = colour.models.RGB_COLOURSPACE_sRGB + img_out_linear = colour.RGB_to_RGB(img_processed_acescg, + colour.models.RGB_COLOURSPACE_ACESCG, + output_cs) + img_out_linear = np.clip(img_out_linear, 0.0, 1.0) # Clip before gamma correction + # img_out_gamma = colour.models.oetf_sRGB(img_out_linear) # Accurate sRGB OETF + img_out_gamma = colour.models.eotf_inverse_sRGB(img_out_linear) # Accurate sRGB OETF + + elif args.output_colorspace.upper() == 'DISPLAY P3': + output_cs = colour.models.RGB_COLOURSPACE_DISPLAY_P3 + img_out_linear = colour.RGB_to_RGB(img_processed_acescg, + colour.models.RGB_COLOURSPACE_ACESCG, + output_cs) + img_out_linear = np.clip(img_out_linear, 0.0, 1.0) + img_out_gamma = colour.models.eotf_inverse_sRGB(img_out_linear) # Display P3 uses sRGB transfer function + else: + print(f"Warning: Unsupported output colorspace {args.output_colorspace}. Defaulting to sRGB.") + output_cs = colour.models.RGB_COLOURSPACE_sRGB + img_out_linear = colour.RGB_to_RGB(img_processed_acescg, + colour.models.RGB_COLOURSPACE_ACESCG, + output_cs) + img_out_linear = np.clip(img_out_linear, 0.0, 1.0) + img_out_gamma = colour.models.eotf_inverse_sRGB(img_out_linear) + + + img_out_final = np.clip(img_out_gamma, 0.0, 1.0) + print(f"Final output image: min: {img_out_final.min():.4f}, max: {img_out_final.max():.4f}, mean: {img_out_final.mean():.4f}") + + # Save the image + output_path = Path(args.output_file) + if output_path.suffix.lower() in ['.tif', '.tiff']: + img_to_save = (img_out_final * 65535.0).astype(np.uint16) + print(f"Saving 16-bit TIFF: {args.output_file}") + else: + img_to_save = (img_out_final * 255.0).astype(np.uint8) + print(f"Saving 8-bit image (e.g. PNG/JPG): {args.output_file}") + + try: + iio.imwrite(args.output_file, img_to_save) + print("Processing complete.") + except Exception as e: + print(f"Error saving image: {e}") + exit(1) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..a51fb8b --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from filmsim!") + + +if __name__ == "__main__": + main() diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d10842e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "filmsim" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "colour-science>=0.4.6", + "imageio>=2.37.0", + "jupyter>=1.1.1", + "jupyterlab>=4.4.3", + "numpy>=2.2.6", + "pillow>=11.2.1", + "rawpy>=0.25.0", + "scipy>=1.15.3", + "warp-lang>=1.7.2", +] diff --git a/sim_data/portra_400.json b/sim_data/portra_400.json new file mode 100644 index 0000000..1059e71 --- /dev/null +++ b/sim_data/portra_400.json @@ -0,0 +1,140 @@ +{ + "info": { + "name": "Portra 400", + "description": "KODAK PROFESSIONAL PORTRA 400 is the world's finest grain high-speed color negative film. At true ISO 400 speed, this film delivers spectacular skin tones plus exceptional color saturation over a wide range of lighting conditions. PORTRA 400 Film is the ideal choice for portrait and fashion photography, as well as for nature, travel and outdoor photography, where the action is fast or the lighting can't be controlled.", + "format_mm": 35, + "version": "1.0.0" + }, + "processing": { + "gamma": { + "r_factor": 1.0, + "g_factor": 1.0, + "b_factor": 1.0 + }, + "balance": { + "r_shift": 0.0, + "g_shift": 0.0, + "b_shift": 0.0 + } + }, + "properties": { + "calibration": { + "iso": 400, + "middle_gray_logh": -1.44 + }, + "halation": { + "strength": { + "r": 0.015, + "g": 0.007, + "b": 0.002 + }, + "size_um": { + "r": 200.0, + "g": 100.0, + "b": 50.0 + } + }, + "couplers": { + "amount": 1.0, + "diffusion_um": 5.0 + }, + "interlayer": { + "diffusion_um": 2.1 + }, + "curves": { + "hd": [ + { "d": -3.4, "r": 0.213, "g": 0.6402, "b": 0.8619 }, + { "d": -3.3, "r": 0.2156, "g": 0.6441, "b": 0.8616 }, + { "d": -3.2, "r": 0.2182, "g": 0.6482, "b": 0.867 }, + { "d": -3.1, "r": 0.2219, "g": 0.6524, "b": 0.8749 }, + { "d": -3, "r": 0.2263, "g": 0.656, "b": 0.8852 }, + { "d": -2.9, "r": 0.2307, "g": 0.6589, "b": 0.9079 }, + { "d": -2.8, "r": 0.2455, "g": 0.677, "b": 0.9413 }, + { "d": -2.7, "r": 0.2653, "g": 0.702, "b": 0.9823 }, + { "d": -2.6, "r": 0.3005, "g": 0.735, "b": 1.0363 }, + { "d": -2.5, "r": 0.3373, "g": 0.7768, "b": 1.0943 }, + { "d": -2.4, "r": 0.3848, "g": 0.8275, "b": 1.1578 }, + { "d": -2.3, "r": 0.4354, "g": 0.879, "b": 1.2213 }, + { "d": -2.2, "r": 0.4885, "g": 0.9338, "b": 1.2848 }, + { "d": -2.1, "r": 0.5424, "g": 0.9885, "b": 1.3482 }, + { "d": -2, "r": 0.597, "g": 1.0433, "b": 1.4117 }, + { "d": -1.9, "r": 0.6516, "g": 1.098, "b": 1.4752 }, + { "d": -1.8, "r": 0.7062, "g": 1.1527, "b": 1.5387 }, + { "d": -1.7, "r": 0.7608, "g": 1.2075, "b": 1.6021 }, + { "d": -1.6, "r": 0.8154, "g": 1.2622, "b": 1.6656 }, + { "d": -1.5, "r": 0.87, "g": 1.317, "b": 1.7291 }, + { "d": -1.4, "r": 0.9246, "g": 1.3717, "b": 1.7926 }, + { "d": -1.3, "r": 0.9792, "g": 1.4264, "b": 1.856 }, + { "d": -1.2, "r": 1.0338, "g": 1.4812, "b": 1.9195 }, + { "d": -1.1, "r": 1.0883, "g": 1.5359, "b": 1.983 }, + { "d": -1, "r": 1.1429, "g": 1.5907, "b": 2.0465 }, + { "d": -0.9, "r": 1.1975, "g": 1.6454, "b": 2.1099 }, + { "d": -0.8, "r": 1.2521, "g": 1.7002, "b": 2.1734 }, + { "d": -0.7, "r": 1.3067, "g": 1.7549, "b": 2.2369 }, + { "d": -0.6, "r": 1.3613, "g": 1.8096, "b": 2.3004 }, + { "d": -0.5, "r": 1.4159, "g": 1.8644, "b": 2.3638 }, + { "d": -0.4, "r": 1.4705, "g": 1.9191, "b": 2.4273 }, + { "d": -0.3, "r": 1.5251, "g": 1.9739, "b": 2.4908 }, + { "d": -0.2, "r": 1.5797, "g": 2.0286, "b": 2.5543 }, + { "d": -0.1, "r": 1.6343, "g": 2.0834, "b": 2.6177 }, + { "d": 0, "r": 1.6889, "g": 2.1381, "b": 2.6812 }, + { "d": 0.1, "r": 1.7435, "g": 2.1928, "b": 2.7447 }, + { "d": 0.2, "r": 1.7981, "g": 2.2476, "b": 2.8082 }, + { "d": 0.3, "r": 1.8527, "g": 2.3023, "b": 2.8716 }, + { "d": 0.4, "r": 1.9073, "g": 2.3571, "b": 2.9351 }, + { "d": 0.5, "r": 1.9619, "g": 2.4118, "b": 2.9986 } + ], + "spectral_sensitivity" : [ + { "wavelength": 379.664, "y": 1.715, "m": 0.00, "c": 0.00 }, + { "wavelength": 385.87, "y": 2.019, "m": 0.00, "c": 0.00 }, + { "wavelength": 392.077, "y": 2.294, "m": 1.311, "c": 0.00 }, + { "wavelength": 398.283, "y": 2.51, "m": 1.468, "c": 0.00 }, + { "wavelength": 404.489, "y": 2.589, "m": 1.566, "c": 0.00 }, + { "wavelength": 410.695, "y": 2.579, "m": 1.527, "c": 0.00 }, + { "wavelength": 416.901, "y": 2.53, "m": 1.468, "c": 0.00 }, + { "wavelength": 423.108, "y": 2.549, "m": 1.409, "c": 0.00 }, + { "wavelength": 429.314, "y": 2.549, "m": 1.359, "c": 0.00 }, + { "wavelength": 435.52, "y": 2.539, "m": 1.33, "c": 0.00 }, + { "wavelength": 441.726, "y": 2.529, "m": 1.31, "c": 0.00 }, + { "wavelength": 447.933, "y": 2.51, "m": 1.3, "c": 0.00 }, + { "wavelength": 454.139, "y": 2.5, "m": 1.31, "c": 0.00 }, + { "wavelength": 460.345, "y": 2.51, "m": 1.32, "c": 0.00 }, + { "wavelength": 466.551, "y": 2.569, "m": 1.33, "c": 0.00 }, + { "wavelength": 472.757, "y": 2.539, "m": 1.408, "c": 0.00 }, + { "wavelength": 478.964, "y": 2.358, "m": 1.585, "c": 0.00 }, + { "wavelength": 485.17, "y": 2.038, "m": 1.723, "c": 0.00 }, + { "wavelength": 491.376, "y": 1.596, "m": 1.88, "c": 0.399 }, + { "wavelength": 497.582, "y": 1.288, "m": 1.988, "c": 0.47 }, + { "wavelength": 503.788, "y": 1.095, "m": 2.037, "c": 0.549 }, + { "wavelength": 509.995, "y": 0.81, "m": 2.086, "c": 0.644 }, + { "wavelength": 516.201, "y": 0.486, "m": 2.135, "c": 0.706 }, + { "wavelength": 522.407, "y": 0.00, "m": 2.194, "c": 0.765 }, + { "wavelength": 528.613, "y": 0.00, "m": 2.253, "c": 0.804 }, + { "wavelength": 534.82, "y": 0.00, "m": 2.361, "c": 0.804 }, + { "wavelength": 541.026, "y": 0.00, "m": 2.43, "c": 0.775 }, + { "wavelength": 547.232, "y": 0.00, "m": 2.488, "c": 0.774 }, + { "wavelength": 553.438, "y": 0.00, "m": 2.518, "c": 0.833 }, + { "wavelength": 559.644, "y": 0.00, "m": 2.479, "c": 0.981 }, + { "wavelength": 565.851, "y": 0.00, "m": 2.4, "c": 1.138 }, + { "wavelength": 572.057, "y": 0.00, "m": 2.311, "c": 1.315 }, + { "wavelength": 578.263, "y": 0.00, "m": 2.213, "c": 1.599 }, + { "wavelength": 584.469, "y": 0.00, "m": 1.854, "c": 1.796 }, + { "wavelength": 590.675, "y": 0.00, "m": 1.504, "c": 1.982 }, + { "wavelength": 596.882, "y": 0.00, "m": 1.113, "c": 2.09 }, + { "wavelength": 603.088, "y": 0.00, "m": 0.00, "c": 2.159 }, + { "wavelength": 609.294, "y": 0.00, "m": 0.00, "c": 2.238 }, + { "wavelength": 615.5, "y": 0.00, "m": 0.00, "c": 2.297 }, + { "wavelength": 621.707, "y": 0.00, "m": 0.00, "c": 2.355 }, + { "wavelength": 627.913, "y": 0.00, "m": 0.00, "c": 2.385 }, + { "wavelength": 634.119, "y": 0.00, "m": 0.00, "c": 2.385 }, + { "wavelength": 640.325, "y": 0.00, "m": 0.00, "c": 2.414 }, + { "wavelength": 646.531, "y": 0.00, "m": 0.00, "c": 2.522 }, + { "wavelength": 652.738, "y": 0.00, "m": 0.00, "c": 2.601 }, + { "wavelength": 658.944, "y": 0.00, "m": 0.00, "c": 2.571 }, + { "wavelength": 671.356, "y": 0.00, "m": 0.00, "c": 1.805 }, + { "wavelength": 677.562, "y": 0.00, "m": 0.00, "c": 1.132 }, + { "wavelength": 683.769, "y": 0.00, "m": 0.00, "c": 0.744 } + ] + } + } +} \ No newline at end of file diff --git a/test_images/Baseline JPEG/01.JPG b/test_images/Baseline JPEG/01.JPG new file mode 100755 index 0000000..7622f45 --- /dev/null +++ b/test_images/Baseline JPEG/01.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cec8608ee97fac87a093e76417cd4a0bbd125a9d835e8b69d25152e0a13e22c0 +size 30785024 diff --git a/test_images/Baseline JPEG/02.JPG b/test_images/Baseline JPEG/02.JPG new file mode 100755 index 0000000..a080a79 --- /dev/null +++ b/test_images/Baseline JPEG/02.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac77891a4132bc90d874c87b4ed70ce7bad11e2902b991f57765fe5fd700337d +size 31634944 diff --git a/test_images/Baseline JPEG/03.JPG b/test_images/Baseline JPEG/03.JPG new file mode 100755 index 0000000..a50ea56 --- /dev/null +++ b/test_images/Baseline JPEG/03.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91b8fd5bec75f8298122944758f1a7424ce80c3e95a9711f2ddced4e0d17176c +size 19321344 diff --git a/test_images/Baseline JPEG/04.JPG b/test_images/Baseline JPEG/04.JPG new file mode 100755 index 0000000..3a7019e --- /dev/null +++ b/test_images/Baseline JPEG/04.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66d258361be9edad075f02ffb59615d8ebbc448ec7178bc56382373f987ed82e +size 30750720 diff --git a/test_images/Baseline JPEG/05.JPG b/test_images/Baseline JPEG/05.JPG new file mode 100755 index 0000000..7f25b7c --- /dev/null +++ b/test_images/Baseline JPEG/05.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be5e4689c9f72a8377c2d03a4f8233f094f7a2543dfcaf0c2e09e1bb8c40e4bc +size 30249472 diff --git a/test_images/Baseline JPEG/06.JPG b/test_images/Baseline JPEG/06.JPG new file mode 100755 index 0000000..f8ba7d0 --- /dev/null +++ b/test_images/Baseline JPEG/06.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:882b8481fb98a887388ba85174d3d4f66666894cb1fcbb7d5389c32f11f0033d +size 30541312 diff --git a/test_images/Baseline JPEG/07.JPG b/test_images/Baseline JPEG/07.JPG new file mode 100755 index 0000000..7cd0208 --- /dev/null +++ b/test_images/Baseline JPEG/07.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb997c2e9b7c97e71b984433b42a16c2f67e42723fb547d36839c71acdf7544f +size 27344896 diff --git a/test_images/Baseline JPEG/08.JPG b/test_images/Baseline JPEG/08.JPG new file mode 100755 index 0000000..15f2ab0 --- /dev/null +++ b/test_images/Baseline JPEG/08.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fbf51083c1952a50d8cc2260a882302c144f2f1ab07c243b444b4366ec4d9cd +size 22437888 diff --git a/test_images/Baseline JPEG/09.JPG b/test_images/Baseline JPEG/09.JPG new file mode 100755 index 0000000..10a5dd2 --- /dev/null +++ b/test_images/Baseline JPEG/09.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9dbefe4da21f9dd588f0af65e56c2328c12290fa83c2a1c0cca9e86cbecb51a +size 21235712 diff --git a/test_images/Baseline JPEG/10.JPG b/test_images/Baseline JPEG/10.JPG new file mode 100755 index 0000000..4e75979 --- /dev/null +++ b/test_images/Baseline JPEG/10.JPG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:934f263e75fc867d90f13c44f212ac7a48fc97875e5f1480aa27a45e597cb225 +size 28227072 diff --git a/test_images/RAW/01.DNG b/test_images/RAW/01.DNG new file mode 100755 index 0000000..be4136b --- /dev/null +++ b/test_images/RAW/01.DNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c256e211dc8b63c4b3c5a6c7d5dd628300c20572c8d92999c8a8e0f7c462f34 +size 92491776 diff --git a/test_images/RAW/02.DNG b/test_images/RAW/02.DNG new file mode 100755 index 0000000..c0dd840 --- /dev/null +++ b/test_images/RAW/02.DNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:285778d2d6f0fde2f1b6f53a5b5f7502d251d2740710161d9c5cd7092484d5c6 +size 89466368 diff --git a/test_images/RAW/03.DNG b/test_images/RAW/03.DNG new file mode 100755 index 0000000..152539d --- /dev/null +++ b/test_images/RAW/03.DNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2a98309ba2549a6fa0b7411f01a2c1bf3a81b8edb57a4b1577f95e6b4715519 +size 70726144 diff --git a/test_images/RAW/04.DNG b/test_images/RAW/04.DNG new file mode 100755 index 0000000..60a3a0d --- /dev/null +++ b/test_images/RAW/04.DNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b53efe13402689a7d7cdf22981e087c442fd6d04af7fcbd80b6c7a381697118 +size 91091968 diff --git a/test_images/RAW/05.DNG b/test_images/RAW/05.DNG new file mode 100755 index 0000000..96589a4 --- /dev/null +++ b/test_images/RAW/05.DNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15296bb3ea24b7e1047b8a32d03d6cd872a9399e456bf4ab2389e6d36aad54ad +size 88541184 diff --git a/test_images/RAW/06.DNG b/test_images/RAW/06.DNG new file mode 100755 index 0000000..62e6f34 --- /dev/null +++ b/test_images/RAW/06.DNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1af1858cd1eabaf2574a0eb74c5fc7d5f21a83744c93b3e0e752e168238265e5 +size 82905088 diff --git a/test_images/RAW/07.DNG b/test_images/RAW/07.DNG new file mode 100755 index 0000000..06098bb --- /dev/null +++ b/test_images/RAW/07.DNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16f08bc33a6fc1ad6811d74eca3200f6d80e4af4938f2f0bbd8b3c7586b5d053 +size 80912896 diff --git a/test_images/RAW/08.DNG b/test_images/RAW/08.DNG new file mode 100755 index 0000000..d2d240d --- /dev/null +++ b/test_images/RAW/08.DNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afc918f0d57b844b5ded3a5cb46b96b90af5fede2b7cfbab1e2f4c670d71c02f +size 77693952 diff --git a/test_images/RAW/09.DNG b/test_images/RAW/09.DNG new file mode 100755 index 0000000..82bd628 --- /dev/null +++ b/test_images/RAW/09.DNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aca3a6014962a8f195fadccbdb3ed27ae79b4cb5e1cecf5dbf548a1d4c4a1a2b +size 75727872 diff --git a/test_images/RAW/10.DNG b/test_images/RAW/10.DNG new file mode 100755 index 0000000..7257365 --- /dev/null +++ b/test_images/RAW/10.DNG @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f533a540e7357456b654ef30d22388ebf660da09aa253a60b2548c956a6dab4 +size 83150336 diff --git a/test_images/v1.0output/filmcolor/01.DNG.tiff.jpg b/test_images/v1.0output/filmcolor/01.DNG.tiff.jpg new file mode 100644 index 0000000..bc012c8 --- /dev/null +++ b/test_images/v1.0output/filmcolor/01.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52c3d834e946bf388169eed5b736847e45d472c5eb9b903dda4de750f40f5588 +size 13171502 diff --git a/test_images/v1.0output/filmcolor/02.DNG.tiff.jpg b/test_images/v1.0output/filmcolor/02.DNG.tiff.jpg new file mode 100644 index 0000000..683ec8e --- /dev/null +++ b/test_images/v1.0output/filmcolor/02.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:648062acfa8cf73594a6a6986beb26af4953fd8824474f1e82e48316d4513016 +size 13545303 diff --git a/test_images/v1.0output/filmcolor/03.DNG.tiff.jpg b/test_images/v1.0output/filmcolor/03.DNG.tiff.jpg new file mode 100644 index 0000000..b452a0b --- /dev/null +++ b/test_images/v1.0output/filmcolor/03.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81e426f2b9cddf17c8c1e33c5e5d553efec901733b3a5cabe2a1790ddea02d5c +size 7063577 diff --git a/test_images/v1.0output/filmcolor/04.DNG.tiff.jpg b/test_images/v1.0output/filmcolor/04.DNG.tiff.jpg new file mode 100644 index 0000000..9b49ae3 --- /dev/null +++ b/test_images/v1.0output/filmcolor/04.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e66df6f700f86d0b568babedde51b323c414473e9b7b7ef8b149be768e9541d +size 9888480 diff --git a/test_images/v1.0output/filmcolor/05.DNG.tiff.jpg b/test_images/v1.0output/filmcolor/05.DNG.tiff.jpg new file mode 100644 index 0000000..7628356 --- /dev/null +++ b/test_images/v1.0output/filmcolor/05.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c89ef1d305a74397756c8628d31119dc16ab0ec19d0031c01c8fe2f6ccafb0b +size 20447880 diff --git a/test_images/v1.0output/filmcolor/06.DNG.tiff.jpg b/test_images/v1.0output/filmcolor/06.DNG.tiff.jpg new file mode 100644 index 0000000..e5654d2 --- /dev/null +++ b/test_images/v1.0output/filmcolor/06.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e232de23d7b0c39ddc8143998648fc93ac36fb1f66a914fbc2366a476727b35 +size 11315350 diff --git a/test_images/v1.0output/filmcolor/07.DNG.tiff.jpg b/test_images/v1.0output/filmcolor/07.DNG.tiff.jpg new file mode 100644 index 0000000..bf4635a --- /dev/null +++ b/test_images/v1.0output/filmcolor/07.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c73e06cc3ceea0a8bd50084453c538294bd13b3cfedb8225d53888e770a1018 +size 9411562 diff --git a/test_images/v1.0output/filmcolor/08.DNG.tiff.jpg b/test_images/v1.0output/filmcolor/08.DNG.tiff.jpg new file mode 100644 index 0000000..38f0e05 --- /dev/null +++ b/test_images/v1.0output/filmcolor/08.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3df6c4dae778b01ee02ed696d41b7b4d9c11a0c26fb9d3a781509698af72aa6e +size 7442528 diff --git a/test_images/v1.0output/filmcolor/09.DNG.tiff.jpg b/test_images/v1.0output/filmcolor/09.DNG.tiff.jpg new file mode 100644 index 0000000..df298a1 --- /dev/null +++ b/test_images/v1.0output/filmcolor/09.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abf0febc0127141875a889140d329cb2dff050cefef757b0b0d94a1feba88eb4 +size 8280904 diff --git a/test_images/v1.0output/filmcolor/10.DNG.tiff.jpg b/test_images/v1.0output/filmcolor/10.DNG.tiff.jpg new file mode 100644 index 0000000..9d9d2f6 --- /dev/null +++ b/test_images/v1.0output/filmcolor/10.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c1144fbd774b833110b2e1773bff5e93c38f65bea55987d66cd355fc10d1434 +size 16154480 diff --git a/test_images/v1.0output/filmgrainmono/01.DNG.tiff.jpg b/test_images/v1.0output/filmgrainmono/01.DNG.tiff.jpg new file mode 100644 index 0000000..5ade1be --- /dev/null +++ b/test_images/v1.0output/filmgrainmono/01.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4647cd53ebb391dcaa0d5f2190283bbde2e5dce76b41101469fcaee9c92161ea +size 28564696 diff --git a/test_images/v1.0output/filmgrainmono/02.DNG.tiff.jpg b/test_images/v1.0output/filmgrainmono/02.DNG.tiff.jpg new file mode 100644 index 0000000..b1e8f62 --- /dev/null +++ b/test_images/v1.0output/filmgrainmono/02.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8dbe6dbe8355f9f32eae442490c99884b3f6d183b98999ff3f9835e2c354388c +size 26898629 diff --git a/test_images/v1.0output/filmgrainmono/03.DNG.tiff.jpg b/test_images/v1.0output/filmgrainmono/03.DNG.tiff.jpg new file mode 100644 index 0000000..1ce324d --- /dev/null +++ b/test_images/v1.0output/filmgrainmono/03.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d109bb413f27045674843d7b9a0d29136e3994d01904271ab7ef620d567ccfe +size 19281984 diff --git a/test_images/v1.0output/filmgrainmono/04.DNG.tiff.jpg b/test_images/v1.0output/filmgrainmono/04.DNG.tiff.jpg new file mode 100644 index 0000000..705a3c4 --- /dev/null +++ b/test_images/v1.0output/filmgrainmono/04.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef976a7d875e857a0b88b9b668252bf2ecd100c35278e35d1aec90c2c30a9cee +size 25881436 diff --git a/test_images/v1.0output/filmgrainmono/05.DNG.tiff.jpg b/test_images/v1.0output/filmgrainmono/05.DNG.tiff.jpg new file mode 100644 index 0000000..b699ee6 --- /dev/null +++ b/test_images/v1.0output/filmgrainmono/05.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03d6dc71a0d7f69a7f82585a43235b3b7d1639f4a1f146ae29d39431720cb55b +size 35235549 diff --git a/test_images/v1.0output/filmgrainmono/06.DNG.tiff.jpg b/test_images/v1.0output/filmgrainmono/06.DNG.tiff.jpg new file mode 100644 index 0000000..f503627 --- /dev/null +++ b/test_images/v1.0output/filmgrainmono/06.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c6a964c86f2915003cc9f6d3f257594994bac271c38a2550a963f4d7784f189 +size 27651471 diff --git a/test_images/v1.0output/filmgrainmono/07.DNG.tiff.jpg b/test_images/v1.0output/filmgrainmono/07.DNG.tiff.jpg new file mode 100644 index 0000000..70e0f03 --- /dev/null +++ b/test_images/v1.0output/filmgrainmono/07.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f48d078fcec684f0a35a527715ec348f9ac09c5b6c0e3c7b94a8565c9468678 +size 26362483 diff --git a/test_images/v1.0output/filmgrainmono/08.DNG.tiff.jpg b/test_images/v1.0output/filmgrainmono/08.DNG.tiff.jpg new file mode 100644 index 0000000..4bc1a97 --- /dev/null +++ b/test_images/v1.0output/filmgrainmono/08.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edb5cd0320db7e2c363a51b1b97deea4ba965bfac7d3ea0013c1ae0a049d3380 +size 23396602 diff --git a/test_images/v1.0output/filmgrainmono/09.DNG.tiff.jpg b/test_images/v1.0output/filmgrainmono/09.DNG.tiff.jpg new file mode 100644 index 0000000..6ca385b --- /dev/null +++ b/test_images/v1.0output/filmgrainmono/09.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:455aa4978d99cacc02c76a926b59c058a259d5001317ddfdb410b274a608de32 +size 21424971 diff --git a/test_images/v1.0output/filmgrainmono/10.DNG.tiff.jpg b/test_images/v1.0output/filmgrainmono/10.DNG.tiff.jpg new file mode 100644 index 0000000..865de03 --- /dev/null +++ b/test_images/v1.0output/filmgrainmono/10.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8691ff4dcd53415b163ccffd345dd69aa459def7d5e3043a000d9966dcddcbbe +size 20582380 diff --git a/test_images/v1.0output/filmgrainrgb/01.DNG.tiff.jpg b/test_images/v1.0output/filmgrainrgb/01.DNG.tiff.jpg new file mode 100644 index 0000000..bf441fe --- /dev/null +++ b/test_images/v1.0output/filmgrainrgb/01.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58e7e87c47a16adb4b03f7489cf0ec7f80eb73ebcfd546c1d4ac4eb05e3c0722 +size 33877155 diff --git a/test_images/v1.0output/filmgrainrgb/02.DNG.tiff.jpg b/test_images/v1.0output/filmgrainrgb/02.DNG.tiff.jpg new file mode 100644 index 0000000..898c002 --- /dev/null +++ b/test_images/v1.0output/filmgrainrgb/02.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc0e15b4c6fe571ae37d6976d46bdd389dd993b17f8c7430474c7773e11e5329 +size 32538314 diff --git a/test_images/v1.0output/filmgrainrgb/03.DNG.tiff.jpg b/test_images/v1.0output/filmgrainrgb/03.DNG.tiff.jpg new file mode 100644 index 0000000..8adbc0f --- /dev/null +++ b/test_images/v1.0output/filmgrainrgb/03.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:203a86226c3c9453e10b9e17fe55b165fc4d6b0959ffa5a6164b5cde69a288e7 +size 25214104 diff --git a/test_images/v1.0output/filmgrainrgb/04.DNG.tiff.jpg b/test_images/v1.0output/filmgrainrgb/04.DNG.tiff.jpg new file mode 100644 index 0000000..4ccaa3c --- /dev/null +++ b/test_images/v1.0output/filmgrainrgb/04.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f54a158b3e079e6776267a43c9a5a4e725b83e02fbb3c8d668f2961ab4b0a49 +size 32136107 diff --git a/test_images/v1.0output/filmgrainrgb/05.DNG.tiff.jpg b/test_images/v1.0output/filmgrainrgb/05.DNG.tiff.jpg new file mode 100644 index 0000000..133a944 --- /dev/null +++ b/test_images/v1.0output/filmgrainrgb/05.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:313718ceb364adac213ca090b9d00ff1bd76d4b3e17a9749a379b5373f9987f3 +size 37508839 diff --git a/test_images/v1.0output/filmgrainrgb/06.DNG.tiff.jpg b/test_images/v1.0output/filmgrainrgb/06.DNG.tiff.jpg new file mode 100644 index 0000000..babf098 --- /dev/null +++ b/test_images/v1.0output/filmgrainrgb/06.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df569b7894f76fb69656f349428c606f5b9a55849f65a954ddbb51534976280d +size 33693335 diff --git a/test_images/v1.0output/filmgrainrgb/07.DNG.tiff.jpg b/test_images/v1.0output/filmgrainrgb/07.DNG.tiff.jpg new file mode 100644 index 0000000..326f291 --- /dev/null +++ b/test_images/v1.0output/filmgrainrgb/07.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7ab66b174e8f63f72fb607efb5e2b81f6f18d38fb2373238cdd9db039bea393 +size 32440375 diff --git a/test_images/v1.0output/filmgrainrgb/08.DNG.tiff.jpg b/test_images/v1.0output/filmgrainrgb/08.DNG.tiff.jpg new file mode 100644 index 0000000..a9d6b37 --- /dev/null +++ b/test_images/v1.0output/filmgrainrgb/08.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0925240ea0294cf301f005222692f3941ab0184632dcd7465952ab8cb5acbc9e +size 30662501 diff --git a/test_images/v1.0output/filmgrainrgb/09.DNG.tiff.jpg b/test_images/v1.0output/filmgrainrgb/09.DNG.tiff.jpg new file mode 100644 index 0000000..5ad6e98 --- /dev/null +++ b/test_images/v1.0output/filmgrainrgb/09.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ac23280bb235507ec05738d2394e25379ad553260d13403585f9ac567b5fe68 +size 27824651 diff --git a/test_images/v1.0output/filmgrainrgb/10.DNG.tiff.jpg b/test_images/v1.0output/filmgrainrgb/10.DNG.tiff.jpg new file mode 100644 index 0000000..7f53f00 --- /dev/null +++ b/test_images/v1.0output/filmgrainrgb/10.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8af810360687e9541cc58f0b7a242af70428709772e69f11841162ea38c2ca0e +size 21153898 diff --git a/test_images/v1.0output/filmscan/01.DNG.tiff.jpg b/test_images/v1.0output/filmscan/01.DNG.tiff.jpg new file mode 100644 index 0000000..d1e1024 --- /dev/null +++ b/test_images/v1.0output/filmscan/01.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:509213e4c470c657036db2821d2b5a5ecbc07f8372c5e7d646a8a65d830cbd9b +size 20291627 diff --git a/test_images/v1.0output/filmscan/02.DNG.tiff.jpg b/test_images/v1.0output/filmscan/02.DNG.tiff.jpg new file mode 100644 index 0000000..101ce23 --- /dev/null +++ b/test_images/v1.0output/filmscan/02.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36c0fb083760816ff897d52459b088b8788579f0d1399ec6f51986a884f41a8d +size 19676297 diff --git a/test_images/v1.0output/filmscan/03.DNG.tiff.jpg b/test_images/v1.0output/filmscan/03.DNG.tiff.jpg new file mode 100644 index 0000000..f30f2b8 --- /dev/null +++ b/test_images/v1.0output/filmscan/03.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:add9c3350e18a6639730cd613b334dcef6192431c36711da8c2948471ba34b5d +size 10761742 diff --git a/test_images/v1.0output/filmscan/04.DNG.tiff.jpg b/test_images/v1.0output/filmscan/04.DNG.tiff.jpg new file mode 100644 index 0000000..2c0491c --- /dev/null +++ b/test_images/v1.0output/filmscan/04.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c95a29c2e24aa683b2b96453e1bbe1c1d29b84546fa917b1182033eb734dc0fa +size 16709214 diff --git a/test_images/v1.0output/filmscan/05.DNG.tiff.jpg b/test_images/v1.0output/filmscan/05.DNG.tiff.jpg new file mode 100644 index 0000000..326b011 --- /dev/null +++ b/test_images/v1.0output/filmscan/05.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:381e4514a7466d01c0c4845cfb84ed334d8f2dded0734467b7dc3b1297b718f6 +size 31123190 diff --git a/test_images/v1.0output/filmscan/06.DNG.tiff.jpg b/test_images/v1.0output/filmscan/06.DNG.tiff.jpg new file mode 100644 index 0000000..588ffc7 --- /dev/null +++ b/test_images/v1.0output/filmscan/06.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:390f80cb639f4a97e07cdf9948bb122c3bac2579bdf902e3af46922cb2faa8c2 +size 17608100 diff --git a/test_images/v1.0output/filmscan/07.DNG.tiff.jpg b/test_images/v1.0output/filmscan/07.DNG.tiff.jpg new file mode 100644 index 0000000..09a2575 --- /dev/null +++ b/test_images/v1.0output/filmscan/07.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e585e53c03db1b3d93359a5bd66f9814ec6c6628577172db95685d9b38dff6eb +size 15351678 diff --git a/test_images/v1.0output/filmscan/08.DNG.tiff.jpg b/test_images/v1.0output/filmscan/08.DNG.tiff.jpg new file mode 100644 index 0000000..2b171b6 --- /dev/null +++ b/test_images/v1.0output/filmscan/08.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:769f51c0634398810836af555742a635d5d26092f7ce2a78596e96c36ad76c4d +size 11905009 diff --git a/test_images/v1.0output/filmscan/09.DNG.tiff.jpg b/test_images/v1.0output/filmscan/09.DNG.tiff.jpg new file mode 100644 index 0000000..d291867 --- /dev/null +++ b/test_images/v1.0output/filmscan/09.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c63b9c3da5f99651bf53f37fe7844c9c637c2d82816772a6d115a70daa593cbd +size 11458182 diff --git a/test_images/v1.0output/filmscan/10.DNG.tiff.jpg b/test_images/v1.0output/filmscan/10.DNG.tiff.jpg new file mode 100644 index 0000000..7bb61eb --- /dev/null +++ b/test_images/v1.0output/filmscan/10.DNG.tiff.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fce0c9bfbda7d3575d43fc2649ae730912f1ee69f69476f1a5c48d64558b868a +size 18286909 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..fdde432 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1490 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321 }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657 }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658 }, + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583 }, + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168 }, + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709 }, + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613 }, + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583 }, + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475 }, + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698 }, + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817 }, + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104 }, +] + +[[package]] +name = "arrow" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "types-python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419 }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, +] + +[[package]] +name = "async-lru" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, +] + +[[package]] +name = "bleach" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406 }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "colour-science" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/29/4ea0082b8ad8c5e18b9ac7cf7ad5f21b70e10976ed53b877773ead3c268d/colour_science-0.4.6.tar.gz", hash = "sha256:be98c2c9b2a5caf0c443431f402599ca9e1cc7d944bb804156803bcc97af4cf0", size = 2228183 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/3e/7a39e00d11a58ab6aa75985433ad4b8001eabf965c20280ed22ed1512887/colour_science-0.4.6-py3-none-any.whl", hash = "sha256:4cd90e6d500c16f3c24225da57031e1944de52fec6b484f5bb3d4ea7d0cfee08", size = 2480689 }, +] + +[[package]] +name = "comm" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180 }, +] + +[[package]] +name = "debugpy" +version = "1.8.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676 }, + { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514 }, + { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756 }, + { url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119 }, + { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230 }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, +] + +[[package]] +name = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/50/4b769ce1ac4071a1ef6d86b1a3fb56cdc3a37615e8c5519e1af96cdac366/fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4", size = 373939 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924 }, +] + +[[package]] +name = "filmsim" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "colour-science" }, + { name = "imageio" }, + { name = "jupyter" }, + { name = "jupyterlab" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "rawpy" }, + { name = "scipy" }, + { name = "warp-lang" }, +] + +[package.metadata] +requires-dist = [ + { name = "colour-science", specifier = ">=0.4.6" }, + { name = "imageio", specifier = ">=2.37.0" }, + { name = "jupyter", specifier = ">=1.1.1" }, + { name = "jupyterlab", specifier = ">=4.4.3" }, + { name = "numpy", specifier = ">=2.2.6" }, + { name = "pillow", specifier = ">=11.2.1" }, + { name = "rawpy", specifier = ">=0.25.0" }, + { name = "scipy", specifier = ">=1.15.3" }, + { name = "warp-lang", specifier = ">=1.7.2" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imageio" +version = "2.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/47/57e897fb7094afb2d26e8b2e4af9a45c7cf1a405acdeeca001fdf2c98501/imageio-2.37.0.tar.gz", hash = "sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996", size = 389963 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl", hash = "sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed", size = 315796 }, +] + +[[package]] +name = "ipykernel" +version = "6.29.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173 }, +] + +[[package]] +name = "ipython" +version = "9.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/09/4c7e06b96fbd203e06567b60fb41b06db606b6a82db6db7b2c85bb72a15c/ipython-9.3.0.tar.gz", hash = "sha256:79eb896f9f23f50ad16c3bc205f686f6e030ad246cc309c6279a242b14afe9d8", size = 4426460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/99/9ed3d52d00f1846679e3aa12e2326ac7044b5e7f90dc822b60115fa533ca/ipython-9.3.0-py3-none-any.whl", hash = "sha256:1a0b6dd9221a1f5dddf725b57ac0cb6fddc7b5f470576231ae9162b9b3455a04", size = 605320 }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, +] + +[[package]] +name = "ipywidgets" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/48/d3dbac45c2814cb73812f98dd6b38bbcc957a4e7bb31d6ea9c03bf94ed87/ipywidgets-8.1.7.tar.gz", hash = "sha256:15f1ac050b9ccbefd45dccfbb2ef6bed0029d8278682d569d71b8dd96bee0376", size = 116721 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/6a/9166369a2f092bd286d24e6307de555d63616e8ddb373ebad2b5635ca4cd/ipywidgets-8.1.7-py3-none-any.whl", hash = "sha256:764f2602d25471c213919b8a1997df04bef869251db4ca8efba1b76b1bd9f7bb", size = 139806 }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321 }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "json5" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079 }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595 }, +] + +[[package]] +name = "jsonschema" +version = "4.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709 }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437 }, +] + +[[package]] +name = "jupyter" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "jupyter-console" }, + { name = "jupyterlab" }, + { name = "nbconvert" }, + { name = "notebook" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657 }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105 }, +] + +[[package]] +name = "jupyter-console" +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "pyzmq" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510 }, +] + +[[package]] +name = "jupyter-core" +version = "5.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880 }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430 }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/b4/3200b0b09c12bc3b72d943d923323c398eff382d1dcc7c0dbc8b74630e40/jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001", size = 48741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/e0/7bd7cff65594fd9936e2f9385701e44574fc7d721331ff676ce440b14100/jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da", size = 69146 }, +] + +[[package]] +name = "jupyter-server" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/c8/ba2bbcd758c47f1124c4ca14061e8ce60d9c6fd537faee9534a95f83521a/jupyter_server-2.16.0.tar.gz", hash = "sha256:65d4b44fdf2dcbbdfe0aa1ace4a842d4aaf746a2b7b168134d5aaed35621b7f6", size = 728177 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/1f/5ebbced977171d09a7b0c08a285ff9a20aafb9c51bde07e52349ff1ddd71/jupyter_server-2.16.0-py3-none-any.whl", hash = "sha256:3d8db5be3bc64403b1c65b400a1d7f4647a5ce743f3b20dbdefe8ddb7b55af9e", size = 386904 }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa", size = 13656 }, +] + +[[package]] +name = "jupyterlab" +version = "4.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/2d/d1678dcf2db66cb4a38a80d9e5fcf48c349f3ac12f2d38882993353ae768/jupyterlab-4.4.3.tar.gz", hash = "sha256:a94c32fd7f8b93e82a49dc70a6ec45a5c18281ca2a7228d12765e4e210e5bca2", size = 23032376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/4d/7dd5c2ffbb960930452a031dc8410746183c924580f2ab4e68ceb5b3043f/jupyterlab-4.4.3-py3-none-any.whl", hash = "sha256:164302f6d4b6c44773dfc38d585665a4db401a16e5296c37df5cba63904fbdea", size = 12295480 }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884 }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.27.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/c9/a883ce65eb27905ce77ace410d83587c82ea64dc85a48d1f7ed52bcfa68d/jupyterlab_server-2.27.3.tar.gz", hash = "sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4", size = 76173 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/09/2032e7d15c544a0e3cd831c51d77a8ca57f7555b2e1b2922142eddb02a84/jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4", size = 59700 }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/7d/160595ca88ee87ac6ba95d82177d29ec60aaa63821d3077babb22ce031a5/jupyterlab_widgets-3.0.15.tar.gz", hash = "sha256:2920888a0c2922351a9202817957a68c07d99673504d6cd37345299e971bb08b", size = 213149 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/6a/ca128561b22b60bd5a0c4ea26649e68c8556b82bc70a0c396eebc977fe86/jupyterlab_widgets-3.0.15-py3-none-any.whl", hash = "sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c", size = 216571 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, +] + +[[package]] +name = "mistune" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/79/bda47f7dd7c3c55770478d6d02c9960c430b0cf1773b72366ff89126ea31/mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0", size = 94347 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410 }, +] + +[[package]] +name = "nbclient" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434 }, +] + +[[package]] +name = "nbconvert" +version = "7.16.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525 }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454 }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, +] + +[[package]] +name = "notebook" +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, + { name = "jupyterlab" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/21/4f83b15e483da4f4f63928edd0cb08b6e7d33f8a15c23b116a90c44c6235/notebook-7.4.3.tar.gz", hash = "sha256:a1567481cd3853f2610ee0ecf5dfa12bb508e878ee8f92152c134ef7f0568a76", size = 13881668 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/1b/16c809d799e3ddd7a97c8b43734f79624b74ddef9707e7d92275a13777bc/notebook-7.4.3-py3-none-any.whl", hash = "sha256:9cdeee954e04101cadb195d90e2ab62b7c9286c1d4f858bf3bb54e40df16c0c3", size = 14286402 }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307 }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663 }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098 }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166 }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674 }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005 }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707 }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008 }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420 }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655 }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329 }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388 }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950 }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759 }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284 }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826 }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329 }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049 }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408 }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863 }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938 }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774 }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895 }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, +] + +[[package]] +name = "prometheus-client" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/cf/40dde0a2be27cc1eb41e333d1a674a74ce8b8b0457269cc640fd42b07cf7/prometheus_client-0.22.1.tar.gz", hash = "sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28", size = 69746 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094", size = 58694 }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-json-logger" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163 }, +] + +[[package]] +name = "pywin32" +version = "310" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384 }, + { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039 }, + { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152 }, +] + +[[package]] +name = "pywinpty" +version = "2.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/7c/917f9c4681bb8d34bfbe0b79d36bbcd902651aeab48790df3d30ba0202fb/pywinpty-2.0.15.tar.gz", hash = "sha256:312cf39153a8736c617d45ce8b6ad6cd2107de121df91c455b10ce6bba7a39b2", size = 29017 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/16/2ab7b3b7f55f3c6929e5f629e1a68362981e4e5fed592a2ed1cb4b4914a5/pywinpty-2.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:ab5920877dd632c124b4ed17bc6dd6ef3b9f86cd492b963ffdb1a67b85b0f408", size = 1405020 }, + { url = "https://files.pythonhosted.org/packages/7c/16/edef3515dd2030db2795dbfbe392232c7a0f3dc41b98e92b38b42ba497c7/pywinpty-2.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:a4560ad8c01e537708d2790dbe7da7d986791de805d89dd0d3697ca59e9e4901", size = 1404151 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "pyzmq" +version = "26.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/11/b9213d25230ac18a71b39b3723494e57adebe36e066397b961657b3b41c1/pyzmq-26.4.0.tar.gz", hash = "sha256:4bd13f85f80962f91a651a7356fe0472791a5f7a92f227822b5acf44795c626d", size = 278293 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/20/fb2c92542488db70f833b92893769a569458311a76474bda89dc4264bd18/pyzmq-26.4.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:c43fac689880f5174d6fc864857d1247fe5cfa22b09ed058a344ca92bf5301e3", size = 1339484 }, + { url = "https://files.pythonhosted.org/packages/58/29/2f06b9cabda3a6ea2c10f43e67ded3e47fc25c54822e2506dfb8325155d4/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:902aca7eba477657c5fb81c808318460328758e8367ecdd1964b6330c73cae43", size = 666106 }, + { url = "https://files.pythonhosted.org/packages/77/e4/dcf62bd29e5e190bd21bfccaa4f3386e01bf40d948c239239c2f1e726729/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5e48a830bfd152fe17fbdeaf99ac5271aa4122521bf0d275b6b24e52ef35eb6", size = 902056 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/b36b3d7aea236087d20189bec1a87eeb2b66009731d7055e5c65f845cdba/pyzmq-26.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31be2b6de98c824c06f5574331f805707c667dc8f60cb18580b7de078479891e", size = 860148 }, + { url = "https://files.pythonhosted.org/packages/18/a6/f048826bc87528c208e90604c3bf573801e54bd91e390cbd2dfa860e82dc/pyzmq-26.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6332452034be001bbf3206ac59c0d2a7713de5f25bb38b06519fc6967b7cf771", size = 855983 }, + { url = "https://files.pythonhosted.org/packages/0a/27/454d34ab6a1d9772a36add22f17f6b85baf7c16e14325fa29e7202ca8ee8/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:da8c0f5dd352136853e6a09b1b986ee5278dfddfebd30515e16eae425c872b30", size = 1197274 }, + { url = "https://files.pythonhosted.org/packages/f4/3d/7abfeab6b83ad38aa34cbd57c6fc29752c391e3954fd12848bd8d2ec0df6/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:f4ccc1a0a2c9806dda2a2dd118a3b7b681e448f3bb354056cad44a65169f6d86", size = 1507120 }, + { url = "https://files.pythonhosted.org/packages/13/ff/bc8d21dbb9bc8705126e875438a1969c4f77e03fc8565d6901c7933a3d01/pyzmq-26.4.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c0b5fceadbab461578daf8d1dcc918ebe7ddd2952f748cf30c7cf2de5d51101", size = 1406738 }, + { url = "https://files.pythonhosted.org/packages/f5/5d/d4cd85b24de71d84d81229e3bbb13392b2698432cf8fdcea5afda253d587/pyzmq-26.4.0-cp313-cp313-win32.whl", hash = "sha256:28e2b0ff5ba4b3dd11062d905682bad33385cfa3cc03e81abd7f0822263e6637", size = 577826 }, + { url = "https://files.pythonhosted.org/packages/c6/6c/f289c1789d7bb6e5a3b3bef7b2a55089b8561d17132be7d960d3ff33b14e/pyzmq-26.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:23ecc9d241004c10e8b4f49d12ac064cd7000e1643343944a10df98e57bc544b", size = 640406 }, + { url = "https://files.pythonhosted.org/packages/b3/99/676b8851cb955eb5236a0c1e9ec679ea5ede092bf8bf2c8a68d7e965cac3/pyzmq-26.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:1edb0385c7f025045d6e0f759d4d3afe43c17a3d898914ec6582e6f464203c08", size = 556216 }, + { url = "https://files.pythonhosted.org/packages/65/c2/1fac340de9d7df71efc59d9c50fc7a635a77b103392d1842898dd023afcb/pyzmq-26.4.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:93a29e882b2ba1db86ba5dd5e88e18e0ac6b627026c5cfbec9983422011b82d4", size = 1333769 }, + { url = "https://files.pythonhosted.org/packages/5c/c7/6c03637e8d742c3b00bec4f5e4cd9d1c01b2f3694c6f140742e93ca637ed/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45684f276f57110bb89e4300c00f1233ca631f08f5f42528a5c408a79efc4a", size = 658826 }, + { url = "https://files.pythonhosted.org/packages/a5/97/a8dca65913c0f78e0545af2bb5078aebfc142ca7d91cdaffa1fbc73e5dbd/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72073e75260cb301aad4258ad6150fa7f57c719b3f498cb91e31df16784d89b", size = 891650 }, + { url = "https://files.pythonhosted.org/packages/7d/7e/f63af1031eb060bf02d033732b910fe48548dcfdbe9c785e9f74a6cc6ae4/pyzmq-26.4.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be37e24b13026cfedd233bcbbccd8c0bcd2fdd186216094d095f60076201538d", size = 849776 }, + { url = "https://files.pythonhosted.org/packages/f6/fa/1a009ce582802a895c0d5fe9413f029c940a0a8ee828657a3bb0acffd88b/pyzmq-26.4.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:237b283044934d26f1eeff4075f751b05d2f3ed42a257fc44386d00df6a270cf", size = 842516 }, + { url = "https://files.pythonhosted.org/packages/6e/bc/f88b0bad0f7a7f500547d71e99f10336f2314e525d4ebf576a1ea4a1d903/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b30f862f6768b17040929a68432c8a8be77780317f45a353cb17e423127d250c", size = 1189183 }, + { url = "https://files.pythonhosted.org/packages/d9/8c/db446a3dd9cf894406dec2e61eeffaa3c07c3abb783deaebb9812c4af6a5/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:c80fcd3504232f13617c6ab501124d373e4895424e65de8b72042333316f64a8", size = 1495501 }, + { url = "https://files.pythonhosted.org/packages/05/4c/bf3cad0d64c3214ac881299c4562b815f05d503bccc513e3fd4fdc6f67e4/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:26a2a7451606b87f67cdeca2c2789d86f605da08b4bd616b1a9981605ca3a364", size = 1395540 }, +] + +[[package]] +name = "rawpy" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/03/9191587911b3526b8ae5ed182e641d7a812060a8eae78c984562bdb5d61f/rawpy-0.25.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:d95163f5ad28faa5fd176f3fe9154f1bc14097ed7e07f2092dddfab68c3bfd8d", size = 1110752 }, + { url = "https://files.pythonhosted.org/packages/50/a7/cf06440823f994c5da963dbe0b3adb2a7f3d28709ad3fe6dac58a124317b/rawpy-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5ac7d9ac0a84b8e957bb9df47184f615719aab1dbd9ae46efe6caf7f3eb0cbc2", size = 1010758 }, + { url = "https://files.pythonhosted.org/packages/9c/a8/f2263e6135a9971e1636223ff231a8595f2d8c3efb9b326911c36d6597cc/rawpy-0.25.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3cd4e5f23ccd60ddf0a2ce807d0a898b5143799829ce68aa33289ac2319c1e", size = 1919409 }, + { url = "https://files.pythonhosted.org/packages/c3/10/c6131a6f4871c40037d1919d2b2f63878f6c2e9f9ceeb2a66d0d2a059a0d/rawpy-0.25.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aab8834a03fbd1645d46431b714c09617b257840b632fef59aba4b94010ff8b1", size = 1940250 }, + { url = "https://files.pythonhosted.org/packages/cf/d3/5904eff78c5d5abd27e3b1ee525b2951ec207d31e77c9dabf11debe139d3/rawpy-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:68cc8e550982621e692e033060f9505d6fa371eae539e148879d7b8006b11c43", size = 845118 }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490 }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242 }, +] + +[[package]] +name = "rpds-py" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/a6/60184b7fc00dd3ca80ac635dd5b8577d444c57e8e8742cecabfacb829921/rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3", size = 27304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/da/323848a2b62abe6a0fec16ebe199dc6889c5d0a332458da8985b2980dffe/rpds_py-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559", size = 364498 }, + { url = "https://files.pythonhosted.org/packages/1f/b4/4d3820f731c80fd0cd823b3e95b9963fec681ae45ba35b5281a42382c67d/rpds_py-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1", size = 350083 }, + { url = "https://files.pythonhosted.org/packages/d5/b1/3a8ee1c9d480e8493619a437dec685d005f706b69253286f50f498cbdbcf/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c", size = 389023 }, + { url = "https://files.pythonhosted.org/packages/3b/31/17293edcfc934dc62c3bf74a0cb449ecd549531f956b72287203e6880b87/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb", size = 403283 }, + { url = "https://files.pythonhosted.org/packages/d1/ca/e0f0bc1a75a8925024f343258c8ecbd8828f8997ea2ac71e02f67b6f5299/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40", size = 524634 }, + { url = "https://files.pythonhosted.org/packages/3e/03/5d0be919037178fff33a6672ffc0afa04ea1cfcb61afd4119d1b5280ff0f/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79", size = 416233 }, + { url = "https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325", size = 390375 }, + { url = "https://files.pythonhosted.org/packages/7a/ac/a87f339f0e066b9535074a9f403b9313fd3892d4a164d5d5f5875ac9f29f/rpds_py-0.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295", size = 424537 }, + { url = "https://files.pythonhosted.org/packages/1f/8f/8d5c1567eaf8c8afe98a838dd24de5013ce6e8f53a01bd47fe8bb06b5533/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b", size = 566425 }, + { url = "https://files.pythonhosted.org/packages/95/33/03016a6be5663b389c8ab0bbbcca68d9e96af14faeff0a04affcb587e776/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98", size = 595197 }, + { url = "https://files.pythonhosted.org/packages/33/8d/da9f4d3e208c82fda311bff0cf0a19579afceb77cf456e46c559a1c075ba/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd", size = 561244 }, + { url = "https://files.pythonhosted.org/packages/e2/b3/39d5dcf7c5f742ecd6dbc88f6f84ae54184b92f5f387a4053be2107b17f1/rpds_py-0.25.1-cp313-cp313-win32.whl", hash = "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31", size = 222254 }, + { url = "https://files.pythonhosted.org/packages/5f/19/2d6772c8eeb8302c5f834e6d0dfd83935a884e7c5ce16340c7eaf89ce925/rpds_py-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500", size = 234741 }, + { url = "https://files.pythonhosted.org/packages/5b/5a/145ada26cfaf86018d0eb304fe55eafdd4f0b6b84530246bb4a7c4fb5c4b/rpds_py-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5", size = 224830 }, + { url = "https://files.pythonhosted.org/packages/4b/ca/d435844829c384fd2c22754ff65889c5c556a675d2ed9eb0e148435c6690/rpds_py-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129", size = 359668 }, + { url = "https://files.pythonhosted.org/packages/1f/01/b056f21db3a09f89410d493d2f6614d87bb162499f98b649d1dbd2a81988/rpds_py-0.25.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d", size = 345649 }, + { url = "https://files.pythonhosted.org/packages/e0/0f/e0d00dc991e3d40e03ca36383b44995126c36b3eafa0ccbbd19664709c88/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72", size = 384776 }, + { url = "https://files.pythonhosted.org/packages/9f/a2/59374837f105f2ca79bde3c3cd1065b2f8c01678900924949f6392eab66d/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34", size = 395131 }, + { url = "https://files.pythonhosted.org/packages/9c/dc/48e8d84887627a0fe0bac53f0b4631e90976fd5d35fff8be66b8e4f3916b/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9", size = 520942 }, + { url = "https://files.pythonhosted.org/packages/7c/f5/ee056966aeae401913d37befeeab57a4a43a4f00099e0a20297f17b8f00c/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5", size = 411330 }, + { url = "https://files.pythonhosted.org/packages/ab/74/b2cffb46a097cefe5d17f94ede7a174184b9d158a0aeb195f39f2c0361e8/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194", size = 387339 }, + { url = "https://files.pythonhosted.org/packages/7f/9a/0ff0b375dcb5161c2b7054e7d0b7575f1680127505945f5cabaac890bc07/rpds_py-0.25.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6", size = 418077 }, + { url = "https://files.pythonhosted.org/packages/0d/a1/fda629bf20d6b698ae84c7c840cfb0e9e4200f664fc96e1f456f00e4ad6e/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78", size = 562441 }, + { url = "https://files.pythonhosted.org/packages/20/15/ce4b5257f654132f326f4acd87268e1006cc071e2c59794c5bdf4bebbb51/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72", size = 590750 }, + { url = "https://files.pythonhosted.org/packages/fb/ab/e04bf58a8d375aeedb5268edcc835c6a660ebf79d4384d8e0889439448b0/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66", size = 558891 }, + { url = "https://files.pythonhosted.org/packages/90/82/cb8c6028a6ef6cd2b7991e2e4ced01c854b6236ecf51e81b64b569c43d73/rpds_py-0.25.1-cp313-cp313t-win32.whl", hash = "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523", size = 218718 }, + { url = "https://files.pythonhosted.org/packages/b6/97/5a4b59697111c89477d20ba8a44df9ca16b41e737fa569d5ae8bff99e650/rpds_py-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763", size = 232218 }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256 }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540 }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115 }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884 }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018 }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716 }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342 }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869 }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851 }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011 }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407 }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030 }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709 }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045 }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062 }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132 }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503 }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097 }, +] + +[[package]] +name = "send2trash" +version = "1.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3a/aec9b02217bb79b87bbc1a21bc6abc51e3d5dcf65c30487ac96c0908c722/Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf", size = 17394 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9", size = 18072 }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154 }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610 }, +] + +[[package]] +name = "tornado" +version = "6.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948 }, + { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112 }, + { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672 }, + { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019 }, + { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252 }, + { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930 }, + { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351 }, + { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328 }, + { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396 }, + { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840 }, + { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596 }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20250516" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/88/d65ed807393285204ab6e2801e5d11fbbea811adcaa979a2ed3b67a5ef41/types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5", size = 13943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/3f/b0e8db149896005adc938a1e7f371d6d7e9eca4053a29b108978ed15e0c2/types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93", size = 14356 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140 }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] + +[[package]] +name = "warp-lang" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/eb/bef2bf8404186115be15cd015c11ab356228410514c9878ad1a8b405b9a7/warp_lang-1.7.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:3825d7ddeb1c733812ff00cce12febcd7fb80b20c413b17e3517dc67b9d5b5ee", size = 45180599 }, + { url = "https://files.pythonhosted.org/packages/5d/81/49be6af5701f21582b8b41ffb9c159bcf63606b83a7134f3906e670fa990/warp_lang-1.7.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:44ea71528d667b4a6b851767d2ccceee3c81a49956798122004aefd95e614a6f", size = 126571159 }, + { url = "https://files.pythonhosted.org/packages/2e/56/3c004907c5322157b3e5dc6534f3d12ff55fbba0f2458bf8af6d767d0485/warp_lang-1.7.2-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:e966450620d6c7a091417d9368528ed1ddf8e819304c84d12e0c440b2e47ef79", size = 127496325 }, + { url = "https://files.pythonhosted.org/packages/8f/2f/455ffdfd0e0aefcf7da0b90ec567329ca01c01e2c5cfafee207799efa627/warp_lang-1.7.2-py3-none-win_amd64.whl", hash = "sha256:a68dd8a4493670aa02ec12c4eb724d4ee9f54e47fa332333fb863a696fe4b40b", size = 108800535 }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "webcolors" +version = "24.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/29/061ec845fb58521848f3739e466efd8250b4b7b98c1b6c5bf4d40b419b7e/webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6", size = 45064 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e8/c0e05e4684d13459f93d312077a9a2efbe04d59c393bc2b8802248c908d4/webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9", size = 14934 }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774 }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/53/2e0253c5efd69c9656b1843892052a31c36d37ad42812b5da45c62191f7e/widgetsnbextension-4.0.14.tar.gz", hash = "sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af", size = 1097428 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/51/5447876806d1088a0f8f71e16542bf350918128d0a69437df26047c8e46f/widgetsnbextension-4.0.14-py3-none-any.whl", hash = "sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575", size = 2196503 }, +]