abachrome-float 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.envrc +3 -0
- data/.rubocop.yml +10 -0
- data/CHANGELOG.md +21 -0
- data/CLA.md +45 -0
- data/CODE-OF-CONDUCT.md +9 -0
- data/LICENSE +19 -0
- data/README.md +315 -0
- data/Rakefile +15 -0
- data/SECURITY.md +94 -0
- data/abachrome-float.gemspec +36 -0
- data/demos/ncurses/plasma.rb +124 -0
- data/devenv.lock +171 -0
- data/devenv.nix +52 -0
- data/devenv.yaml +8 -0
- data/lib/abachrome/color.rb +197 -0
- data/lib/abachrome/color_mixins/blend.rb +100 -0
- data/lib/abachrome/color_mixins/lighten.rb +90 -0
- data/lib/abachrome/color_mixins/spectral_mix.rb +70 -0
- data/lib/abachrome/color_mixins/to_colorspace.rb +107 -0
- data/lib/abachrome/color_mixins/to_grayscale.rb +87 -0
- data/lib/abachrome/color_mixins/to_lrgb.rb +121 -0
- data/lib/abachrome/color_mixins/to_oklab.rb +117 -0
- data/lib/abachrome/color_mixins/to_oklch.rb +110 -0
- data/lib/abachrome/color_mixins/to_srgb.rb +142 -0
- data/lib/abachrome/color_models/cmyk.rb +159 -0
- data/lib/abachrome/color_models/hsv.rb +49 -0
- data/lib/abachrome/color_models/lms.rb +38 -0
- data/lib/abachrome/color_models/oklab.rb +37 -0
- data/lib/abachrome/color_models/oklch.rb +91 -0
- data/lib/abachrome/color_models/rgb.rb +58 -0
- data/lib/abachrome/color_models/xyz.rb +31 -0
- data/lib/abachrome/color_models/yiq.rb +37 -0
- data/lib/abachrome/color_space.rb +199 -0
- data/lib/abachrome/converter.rb +117 -0
- data/lib/abachrome/converters/base.rb +128 -0
- data/lib/abachrome/converters/cmyk_to_srgb.rb +42 -0
- data/lib/abachrome/converters/lms_to_lrgb.rb +40 -0
- data/lib/abachrome/converters/lms_to_srgb.rb +27 -0
- data/lib/abachrome/converters/lms_to_xyz.rb +34 -0
- data/lib/abachrome/converters/lrgb_to_lms.rb +3 -0
- data/lib/abachrome/converters/lrgb_to_oklab.rb +57 -0
- data/lib/abachrome/converters/lrgb_to_srgb.rb +59 -0
- data/lib/abachrome/converters/lrgb_to_xyz.rb +33 -0
- data/lib/abachrome/converters/oklab_to_lms.rb +44 -0
- data/lib/abachrome/converters/oklab_to_lrgb.rb +71 -0
- data/lib/abachrome/converters/oklab_to_oklch.rb +56 -0
- data/lib/abachrome/converters/oklab_to_srgb.rb +46 -0
- data/lib/abachrome/converters/oklch_to_lrgb.rb +79 -0
- data/lib/abachrome/converters/oklch_to_oklab.rb +52 -0
- data/lib/abachrome/converters/oklch_to_srgb.rb +46 -0
- data/lib/abachrome/converters/oklch_to_xyz.rb +70 -0
- data/lib/abachrome/converters/srgb_to_cmyk.rb +64 -0
- data/lib/abachrome/converters/srgb_to_lrgb.rb +55 -0
- data/lib/abachrome/converters/srgb_to_oklab.rb +45 -0
- data/lib/abachrome/converters/srgb_to_oklch.rb +47 -0
- data/lib/abachrome/converters/srgb_to_yiq.rb +49 -0
- data/lib/abachrome/converters/xyz_to_lms.rb +34 -0
- data/lib/abachrome/converters/xyz_to_oklab.rb +42 -0
- data/lib/abachrome/converters/yiq_to_srgb.rb +47 -0
- data/lib/abachrome/gamut/base.rb +74 -0
- data/lib/abachrome/gamut/p3.rb +27 -0
- data/lib/abachrome/gamut/rec2020.rb +25 -0
- data/lib/abachrome/gamut/srgb.rb +49 -0
- data/lib/abachrome/illuminants/base.rb +35 -0
- data/lib/abachrome/illuminants/d50.rb +33 -0
- data/lib/abachrome/illuminants/d55.rb +29 -0
- data/lib/abachrome/illuminants/d65.rb +37 -0
- data/lib/abachrome/illuminants/d75.rb +29 -0
- data/lib/abachrome/named/css.rb +157 -0
- data/lib/abachrome/named/tailwind.rb +301 -0
- data/lib/abachrome/outputs/css.rb +119 -0
- data/lib/abachrome/palette.rb +244 -0
- data/lib/abachrome/palette_mixins/interpolate.rb +53 -0
- data/lib/abachrome/palette_mixins/resample.rb +61 -0
- data/lib/abachrome/palette_mixins/stretch_luminance.rb +72 -0
- data/lib/abachrome/parsers/css.rb +452 -0
- data/lib/abachrome/parsers/hex.rb +52 -0
- data/lib/abachrome/parsers/tailwind.rb +45 -0
- data/lib/abachrome/spectral.rb +276 -0
- data/lib/abachrome/to_abcd.rb +23 -0
- data/lib/abachrome/version.rb +7 -0
- data/lib/abachrome.rb +242 -0
- data/logo.png +0 -0
- data/logo.webp +0 -0
- data/security/assesments/2025-10-12-SECURITY_ASSESSMENT.md +53 -0
- data/security/vex.json +21 -0
- metadata +146 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome::Spectral - Kubelka-Munk spectral color mixing
|
|
4
|
+
#
|
|
5
|
+
# This module implements the Kubelka-Munk theory for realistic paint-like color mixing.
|
|
6
|
+
# Based on spectral.js by Ronald van Wijnen (https://github.com/rvanwijnen/spectral.js)
|
|
7
|
+
#
|
|
8
|
+
# The Kubelka-Munk model simulates how real pigments absorb and scatter light by:
|
|
9
|
+
# 1. Converting RGB colors to spectral reflectance curves (38 wavelength samples)
|
|
10
|
+
# 2. Computing absorption/scattering coefficients (KS values)
|
|
11
|
+
# 3. Mixing colors by weighted averaging of KS values
|
|
12
|
+
# 4. Converting back to RGB via XYZ color space
|
|
13
|
+
#
|
|
14
|
+
# This produces more realistic color mixing than simple RGB interpolation,
|
|
15
|
+
# avoiding issues like muddy browns when mixing complementary colors.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
module Abachrome
|
|
19
|
+
module Spectral
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# Number of wavelength samples in spectral curves
|
|
23
|
+
SIZE = 38
|
|
24
|
+
|
|
25
|
+
# Gamma value for sRGB companding
|
|
26
|
+
GAMMA = 2.4
|
|
27
|
+
|
|
28
|
+
# Base spectral reflectance curves for White, Cyan, Magenta, Yellow, Red, Green, Blue
|
|
29
|
+
# These are the fundamental building blocks for converting RGB to spectral data
|
|
30
|
+
BASE_SPECTRA = {
|
|
31
|
+
W: [
|
|
32
|
+
1.00116072718764, 1.00116065159728, 1.00116031922747, 1.00115867270789, 1.00115259844552, 1.00113252528998, 1.00108500663327, 1.00099687889453, 1.00086525152274,
|
|
33
|
+
1.0006962900094, 1.00050496114888, 1.00030808187992, 1.00011966602013, 0.999952765968407, 0.999821836899297, 0.999738609557593, 0.999709551639612, 0.999731930210627,
|
|
34
|
+
0.999799436346195, 0.999900330316671, 1.00002040652611, 1.00014478793658, 1.00025997903412, 1.00035579697089, 1.00042753780269, 1.00047623344888, 1.00050720967508,
|
|
35
|
+
1.00052519156373, 1.00053509606896, 1.00054022097482, 1.00054272816784, 1.00054389569087, 1.00054448212151, 1.00054476959992, 1.00054489887762, 1.00054496254689,
|
|
36
|
+
1.00054498927058, 1.000544996993
|
|
37
|
+
].freeze,
|
|
38
|
+
C: [
|
|
39
|
+
0.970585001322962, 0.970592498143425, 0.970625348729891, 0.970786806119017, 0.971368673228248, 0.973163230621252, 0.976740223158765, 0.981587605491377, 0.986280265652949,
|
|
40
|
+
0.989949147689134, 0.99249270153842, 0.994145680405256, 0.995183975033212, 0.995756750110818, 0.99591281828671, 0.995606157834528, 0.994597600961854, 0.99221571549237,
|
|
41
|
+
0.986236452783249, 0.967943337264541, 0.891285004244943, 0.536202477862053, 0.154108119001878, 0.0574575093228929, 0.0315349873107007, 0.0222633920086335, 0.0182022841492439,
|
|
42
|
+
0.016299055973264, 0.0153656239334613, 0.0149111568733976, 0.0146954339898235, 0.0145964146717719, 0.0145470156699655, 0.0145228771899495, 0.0145120341118965,
|
|
43
|
+
0.0145066940939832, 0.0145044507314479, 0.0145038009464639
|
|
44
|
+
].freeze,
|
|
45
|
+
M: [
|
|
46
|
+
0.990673557319988, 0.990671524961979, 0.990662582353421, 0.990618107644795, 0.99045148087871, 0.989871081400204, 0.98828660875964, 0.984290692797504, 0.973934905625306,
|
|
47
|
+
0.941817838460145, 0.817390326195156, 0.432472805065729, 0.13845397825887, 0.0537347216940033, 0.0292174996673231, 0.021313651750859, 0.0201349530181136, 0.0241323096280662,
|
|
48
|
+
0.0372236145223627, 0.0760506552706601, 0.205375471942399, 0.541268903460439, 0.815841685086486, 0.912817704123976, 0.946339830166962, 0.959927696331991, 0.966260595230312,
|
|
49
|
+
0.969325970058424, 0.970854536721399, 0.971605066528128, 0.971962769757392, 0.972127272274509, 0.972209417745812, 0.972249577678424, 0.972267621998742, 0.97227650946215,
|
|
50
|
+
0.972280243306874, 0.97228132482656
|
|
51
|
+
].freeze,
|
|
52
|
+
Y: [
|
|
53
|
+
0.0210523371789306, 0.0210564627517414, 0.0210746178695038, 0.0211649058448753, 0.0215027957272504, 0.0226738799041561, 0.0258235649693629, 0.0334879385639851,
|
|
54
|
+
0.0519069663740307, 0.100749014833473, 0.239129899706847, 0.534804312272748, 0.79780757864303, 0.911449894067384, 0.953797963004507, 0.971241615465429, 0.979303123807588,
|
|
55
|
+
0.983380119507575, 0.985461246567755, 0.986435046976605, 0.986738250670141, 0.986617882445032, 0.986277776758643, 0.985860592444056, 0.98547492767621, 0.985176934765558,
|
|
56
|
+
0.984971574014181, 0.984846303415712, 0.984775351811199, 0.984738066625265, 0.984719648311765, 0.984711023391939, 0.984706683300676, 0.984704554393091, 0.98470359630937,
|
|
57
|
+
0.984703124077552, 0.98470292561509, 0.984702868122795
|
|
58
|
+
].freeze,
|
|
59
|
+
R: [
|
|
60
|
+
0.0315605737777207, 0.0315520718330149, 0.0315148215513658, 0.0313318044982702, 0.0306729857725527, 0.0286480476989607, 0.0246450407045709, 0.0192960753663651,
|
|
61
|
+
0.0142066612220556, 0.0102942608878609, 0.0076191460521811, 0.005898041083542, 0.0048233247781713, 0.0042298748350633, 0.0040599171299341, 0.0043533695594676,
|
|
62
|
+
0.0053434425970201, 0.0076917201010463, 0.0135969795736536, 0.0316975442661115, 0.107861196355249, 0.463812603168704, 0.847055405272011, 0.943185409393918, 0.968862150696558,
|
|
63
|
+
0.978030667473603, 0.982043643854306, 0.983923623718707, 0.984845484154382, 0.985294275814596, 0.985507295219825, 0.985605071539837, 0.985653849933578, 0.985677685033883,
|
|
64
|
+
0.985688391806122, 0.985693664690031, 0.985695879848205, 0.985696521463762
|
|
65
|
+
].freeze,
|
|
66
|
+
G: [
|
|
67
|
+
0.0095560747554212, 0.0095581580120851, 0.0095673245444588, 0.0096129126297349, 0.0097837090401843, 0.010378622705871, 0.0120026452378567, 0.0160977721473922,
|
|
68
|
+
0.026706190223168, 0.0595555440185881, 0.186039826532826, 0.570579820116159, 0.861467768400292, 0.945879089767658, 0.970465486474305, 0.97841363028445, 0.979589031411224,
|
|
69
|
+
0.975533536908632, 0.962288755397813, 0.92312157451312, 0.793434018943111, 0.459270135902429, 0.185574103666303, 0.0881774959955372, 0.05436302287667, 0.0406288447060719,
|
|
70
|
+
0.034221520431697, 0.0311185790956966, 0.0295708898336134, 0.0288108739348928, 0.0284486271324597, 0.0282820301724731, 0.0281988376490237, 0.0281581655342037,
|
|
71
|
+
0.0281398910216386, 0.0281308901665811, 0.0281271086805816, 0.0281260133612096
|
|
72
|
+
].freeze,
|
|
73
|
+
B: [
|
|
74
|
+
0.979404752502014, 0.97940070684313, 0.979382903470261, 0.979294364945594, 0.97896301460857, 0.977814466694043, 0.974724321133836, 0.967198482343973, 0.949079657530575,
|
|
75
|
+
0.900850128940977, 0.76315044546224, 0.465922171649319, 0.201263280451005, 0.0877524413419623, 0.0457176793291679, 0.0284706050521843, 0.020527176756985, 0.0165302792310211,
|
|
76
|
+
0.0145135107212858, 0.0136003508637687, 0.0133604258769571, 0.013548894314568, 0.0139594356366992, 0.014443425575357, 0.0148854440621406, 0.0152254296999746,
|
|
77
|
+
0.0154592848180209, 0.0156018026485961, 0.0156824871281936, 0.0157248764360615, 0.0157458108784121, 0.0157556123350225, 0.0157605443964911, 0.0157629637515278,
|
|
78
|
+
0.0157640525629106, 0.015764589232951, 0.0157648147772649, 0.0157648801149616
|
|
79
|
+
].freeze
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
82
|
+
# CIE 1931 Color Matching Functions weighted by D65 Standard Illuminant
|
|
83
|
+
# Used to convert spectral reflectance to XYZ tristimulus values
|
|
84
|
+
CMF = [
|
|
85
|
+
[
|
|
86
|
+
0.0000646919989576, 0.0002194098998132, 0.0011205743509343, 0.0037666134117111, 0.011880553603799, 0.0232864424191771, 0.0345594181969747, 0.0372237901162006,
|
|
87
|
+
0.0324183761091486, 0.021233205609381, 0.0104909907685421, 0.0032958375797931, 0.0005070351633801, 0.0009486742057141, 0.0062737180998318, 0.0168646241897775,
|
|
88
|
+
0.028689649025981, 0.0426748124691731, 0.0562547481311377, 0.0694703972677158, 0.0830531516998291, 0.0861260963002257, 0.0904661376847769, 0.0850038650591277,
|
|
89
|
+
0.0709066691074488, 0.0506288916373645, 0.035473961885264, 0.0214682102597065, 0.0125164567619117, 0.0068045816390165, 0.0034645657946526, 0.0014976097506959,
|
|
90
|
+
0.000769700480928, 0.0004073680581315, 0.0001690104031614, 0.0000952245150365, 0.0000490309872958, 0.0000199961492222
|
|
91
|
+
].freeze,
|
|
92
|
+
[
|
|
93
|
+
0.000001844289444, 0.0000062053235865, 0.0000310096046799, 0.0001047483849269, 0.0003536405299538, 0.0009514714056444, 0.0022822631748318, 0.004207329043473,
|
|
94
|
+
0.0066887983719014, 0.0098883960193565, 0.0152494514496311, 0.0214183109449723, 0.0334229301575068, 0.0513100134918512, 0.070402083939949, 0.0878387072603517,
|
|
95
|
+
0.0942490536184085, 0.0979566702718931, 0.0941521856862608, 0.0867810237486753, 0.0788565338632013, 0.0635267026203555, 0.05374141675682, 0.042646064357412,
|
|
96
|
+
0.0316173492792708, 0.020885205921391, 0.0138601101360152, 0.0081026402038399, 0.004630102258803, 0.0024913800051319, 0.0012593033677378, 0.000541646522168,
|
|
97
|
+
0.0002779528920067, 0.0001471080673854, 0.0000610327472927, 0.0000343873229523, 0.0000177059860053, 0.000007220974913
|
|
98
|
+
].freeze,
|
|
99
|
+
[
|
|
100
|
+
0.000305017147638, 0.0010368066663574, 0.0053131363323992, 0.0179543925899536, 0.0570775815345485, 0.113651618936287, 0.17335872618355, 0.196206575558657,
|
|
101
|
+
0.186082370706296, 0.139950475383207, 0.0891745294268649, 0.0478962113517075, 0.0281456253957952, 0.0161376622950514, 0.0077591019215214, 0.0042961483736618,
|
|
102
|
+
0.0020055092122156, 0.0008614711098802, 0.0003690387177652, 0.0001914287288574, 0.0001495555858975, 0.0000923109285104, 0.0000681349182337, 0.0000288263655696,
|
|
103
|
+
0.0000157671820553, 0.0000039406041027, 0.000001584012587, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
|
|
104
|
+
].freeze
|
|
105
|
+
].freeze
|
|
106
|
+
|
|
107
|
+
# sRGB to XYZ transformation matrix (D65 illuminant)
|
|
108
|
+
RGB_TO_XYZ = [
|
|
109
|
+
[0.41239079926595934, 0.357584339383878, 0.1804807884018343],
|
|
110
|
+
[0.21263900587151027, 0.715168678767756, 0.07219231536073371],
|
|
111
|
+
[0.01933081871559182, 0.11919477979462598, 0.9505321522496607]
|
|
112
|
+
].freeze
|
|
113
|
+
|
|
114
|
+
# XYZ to sRGB transformation matrix (D65 illuminant)
|
|
115
|
+
XYZ_TO_RGB = [
|
|
116
|
+
[3.2409699419045226, -1.537383177570094, -0.4986107602930034],
|
|
117
|
+
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
|
|
118
|
+
[0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
|
|
119
|
+
].freeze
|
|
120
|
+
|
|
121
|
+
# Inverse companding: sRGB to linear RGB
|
|
122
|
+
def uncompand(x)
|
|
123
|
+
x = x.to_f
|
|
124
|
+
x > 0.04045 ? ((x + 0.055) / 1.055)**GAMMA : x / 12.92
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Companding: linear RGB to sRGB
|
|
128
|
+
def compand(x)
|
|
129
|
+
x = x.to_f
|
|
130
|
+
x > 0.0031308 ? (1.055 * (x**(1.0 / GAMMA))) - 0.055 : x * 12.92
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Convert sRGB [0-1] to linear RGB
|
|
134
|
+
def srgb_to_lrgb(srgb)
|
|
135
|
+
srgb.map { |x| uncompand(x) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Convert linear RGB to sRGB [0-1]
|
|
139
|
+
def lrgb_to_srgb(lrgb)
|
|
140
|
+
lrgb.map { |x| compand(x) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Matrix-vector multiplication
|
|
144
|
+
def mul_mat_vec(matrix, vector)
|
|
145
|
+
matrix.map do |row|
|
|
146
|
+
row.zip(vector).map { |a, b| a * b }.sum
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Convert linear RGB to spectral reflectance curve
|
|
151
|
+
# Uses the seven primary spectral curves (W, C, M, Y, R, G, B)
|
|
152
|
+
def lrgb_to_reflectance(lrgb)
|
|
153
|
+
r, g, b = lrgb
|
|
154
|
+
|
|
155
|
+
# Extract white component
|
|
156
|
+
w = [r, g, b].min
|
|
157
|
+
r -= w
|
|
158
|
+
g -= w
|
|
159
|
+
b -= w
|
|
160
|
+
|
|
161
|
+
# Extract CMY components
|
|
162
|
+
c = [g, b].min
|
|
163
|
+
m = [r, b].min
|
|
164
|
+
y = [r, g].min
|
|
165
|
+
|
|
166
|
+
# Extract pure RGB components
|
|
167
|
+
r_pure = [0, [r - b, r - g].min].max
|
|
168
|
+
g_pure = [0, [g - b, g - r].min].max
|
|
169
|
+
b_pure = [0, [b - g, b - r].min].max
|
|
170
|
+
|
|
171
|
+
# Combine spectral curves
|
|
172
|
+
Array.new(SIZE) do |i|
|
|
173
|
+
[
|
|
174
|
+
Float::EPSILON,
|
|
175
|
+
(w * BASE_SPECTRA[:W][i]) +
|
|
176
|
+
(c * BASE_SPECTRA[:C][i]) +
|
|
177
|
+
(m * BASE_SPECTRA[:M][i]) +
|
|
178
|
+
(y * BASE_SPECTRA[:Y][i]) +
|
|
179
|
+
(r_pure * BASE_SPECTRA[:R][i]) +
|
|
180
|
+
(g_pure * BASE_SPECTRA[:G][i]) +
|
|
181
|
+
(b_pure * BASE_SPECTRA[:B][i])
|
|
182
|
+
].max
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Convert spectral reflectance to XYZ using CIE color matching functions
|
|
187
|
+
def reflectance_to_xyz(reflectance)
|
|
188
|
+
mul_mat_vec(CMF, reflectance)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Convert XYZ to linear RGB
|
|
192
|
+
def xyz_to_lrgb(xyz)
|
|
193
|
+
mul_mat_vec(XYZ_TO_RGB, xyz)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Convert linear RGB to XYZ
|
|
197
|
+
def lrgb_to_xyz(lrgb)
|
|
198
|
+
mul_mat_vec(RGB_TO_XYZ, lrgb)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Kubelka-Munk absorption/scattering parameter
|
|
202
|
+
# Converts reflectance R to absorption/scattering coefficient KS
|
|
203
|
+
def ks_from_reflectance(r)
|
|
204
|
+
((1.0 - r)**2) / (2.0 * r)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Inverse Kubelka-Munk function
|
|
208
|
+
# Converts KS back to reflectance
|
|
209
|
+
def reflectance_from_ks(ks)
|
|
210
|
+
1.0 + ks - Math.sqrt((ks**2) + (2.0 * ks))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Calculate luminance from XYZ (Y component)
|
|
214
|
+
def luminance_from_xyz(xyz)
|
|
215
|
+
[Float::EPSILON, xyz[1]].max
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Mix colors using Kubelka-Munk theory
|
|
219
|
+
#
|
|
220
|
+
# @param colors [Array<Hash>] Array of hashes with :color (Abachrome::Color) and :weight (Numeric)
|
|
221
|
+
# @param tinting_strengths [Hash] Optional hash mapping colors to tinting strengths (default: 1.0)
|
|
222
|
+
# @return [Abachrome::Color] The mixed color
|
|
223
|
+
#
|
|
224
|
+
# @example
|
|
225
|
+
# red = Abachrome.from_rgb(1, 0, 0)
|
|
226
|
+
# blue = Abachrome.from_rgb(0, 0, 1)
|
|
227
|
+
# mixed = Abachrome::Spectral.mix([{color: red, weight: 1}, {color: blue, weight: 1}])
|
|
228
|
+
def mix(colors, tinting_strengths: {})
|
|
229
|
+
# Convert colors to linear RGB and then to spectral reflectance
|
|
230
|
+
spectral_data = colors.map do |data|
|
|
231
|
+
color = data[:color]
|
|
232
|
+
weight = data[:weight].to_f
|
|
233
|
+
tinting_strength = tinting_strengths[color] || 1.0
|
|
234
|
+
|
|
235
|
+
# Convert to linear RGB
|
|
236
|
+
lrgb_color = color.to_color_space(:lrgb)
|
|
237
|
+
lrgb = lrgb_color.coordinates.map(&:to_f)
|
|
238
|
+
|
|
239
|
+
# Get spectral reflectance and KS values
|
|
240
|
+
reflectance = lrgb_to_reflectance(lrgb)
|
|
241
|
+
ks_values = reflectance.map { |r| ks_from_reflectance(r) }
|
|
242
|
+
|
|
243
|
+
# Calculate XYZ for luminance
|
|
244
|
+
xyz = reflectance_to_xyz(reflectance)
|
|
245
|
+
luminance = luminance_from_xyz(xyz)
|
|
246
|
+
|
|
247
|
+
# Calculate effective concentration
|
|
248
|
+
# Note: weight is already normalized (sums to 1), so we don't square it
|
|
249
|
+
# spectral.js squares 'factor' because it uses unnormalized values
|
|
250
|
+
concentration = weight * (tinting_strength**2) * luminance
|
|
251
|
+
|
|
252
|
+
{
|
|
253
|
+
ks_values: ks_values,
|
|
254
|
+
concentration: concentration
|
|
255
|
+
}
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Mix KS values using weighted average
|
|
259
|
+
total_concentration = spectral_data.sum { |d| d[:concentration] }
|
|
260
|
+
|
|
261
|
+
mixed_reflectance = Array.new(SIZE) do |i|
|
|
262
|
+
ks_mix = spectral_data.sum { |d| d[:ks_values][i] * d[:concentration] }
|
|
263
|
+
ks_mix /= total_concentration
|
|
264
|
+
reflectance_from_ks(ks_mix)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Convert back to RGB
|
|
268
|
+
xyz = reflectance_to_xyz(mixed_reflectance)
|
|
269
|
+
lrgb = xyz_to_lrgb(xyz)
|
|
270
|
+
srgb = lrgb_to_srgb(lrgb)
|
|
271
|
+
|
|
272
|
+
# Return as Color object
|
|
273
|
+
Abachrome::Color.from_rgb(*srgb)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Abachrome
|
|
4
|
+
module ToAbcd
|
|
5
|
+
# Converts the receiver to an AbcDecimal object.
|
|
6
|
+
#
|
|
7
|
+
# This method converts the receiver (typically a numeric value) to an AbcDecimal
|
|
8
|
+
# instance, which provides high precision decimal arithmetic capabilities for
|
|
9
|
+
# color space calculations.
|
|
10
|
+
#
|
|
11
|
+
# @return [Abachrome::AbcDecimal] a new AbcDecimal instance representing the
|
|
12
|
+
# same numeric value as the receiver
|
|
13
|
+
def to_abcd
|
|
14
|
+
AbcDecimal.new(self)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
[Numeric, String, Rational].each do |klass|
|
|
20
|
+
klass.include(Abachrome::ToAbcd)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
data/lib/abachrome.rb
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Abachrome - A Ruby color manipulation library
|
|
4
|
+
#
|
|
5
|
+
# This is the main entry point for the Abachrome library, providing color creation,
|
|
6
|
+
# conversion, and manipulation capabilities across multiple color spaces including
|
|
7
|
+
# sRGB, OKLAB, OKLCH, and linear RGB.
|
|
8
|
+
#
|
|
9
|
+
# Key features:
|
|
10
|
+
# - Create colors from RGB, OKLAB, OKLCH values or hex strings
|
|
11
|
+
# - Convert between different color spaces
|
|
12
|
+
# - Parse colors from hex codes and CSS color names
|
|
13
|
+
# - Register custom color spaces and converters
|
|
14
|
+
# - High-precision decimal arithmetic for accurate color calculations
|
|
15
|
+
#
|
|
16
|
+
# The library uses autoloading for efficient memory usage and provides both
|
|
17
|
+
# functional and object-oriented APIs for color operations.
|
|
18
|
+
|
|
19
|
+
require_relative "abachrome/to_abcd"
|
|
20
|
+
|
|
21
|
+
module Abachrome
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
autoload :AbcDecimal, "abachrome/abc_decimal"
|
|
25
|
+
autoload :Color, "abachrome/color"
|
|
26
|
+
autoload :Palette, "abachrome/palette"
|
|
27
|
+
autoload :ColorSpace, "abachrome/color_space"
|
|
28
|
+
autoload :Converter, "abachrome/converter"
|
|
29
|
+
autoload :Gamut, "abachrome/gamut/base"
|
|
30
|
+
autoload :Spectral, "abachrome/spectral"
|
|
31
|
+
autoload :ToAbcd, "abachrome/to_abcd"
|
|
32
|
+
autoload :VERSION, "abachrome/version"
|
|
33
|
+
|
|
34
|
+
module ColorModels
|
|
35
|
+
autoload :CMYK, "abachrome/color_models/cmyk"
|
|
36
|
+
autoload :HSV, "abachrome/color_models/hsv"
|
|
37
|
+
autoload :Oklab, "abachrome/color_models/oklab"
|
|
38
|
+
autoload :RGB, "abachrome/color_models/rgb"
|
|
39
|
+
autoload :YIQ, "abachrome/color_models/yiq"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module ColorMixins
|
|
43
|
+
autoload :ToLrgb, "abachrome/color_mixins/to_lrgb"
|
|
44
|
+
autoload :ToOklab, "abachrome/color_mixins/to_oklab"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module Converters
|
|
48
|
+
autoload :Base, "abachrome/converters/base"
|
|
49
|
+
autoload :LrgbToOklab, "abachrome/converters/lrgb_to_oklab"
|
|
50
|
+
autoload :OklabToLrgb, "abachrome/converters/oklab_to_lrgb"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module Gamut
|
|
54
|
+
autoload :P3, "abachrome/gamut/p3"
|
|
55
|
+
autoload :Rec2020, "abachrome/gamut/rec2020"
|
|
56
|
+
autoload :SRGB, "abachrome/gamut/srgb"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
module Illuminants
|
|
60
|
+
autoload :Base, "abachrome/illuminants/base"
|
|
61
|
+
autoload :D50, "abachrome/illuminants/d50"
|
|
62
|
+
autoload :D55, "abachrome/illuminants/d55"
|
|
63
|
+
autoload :D65, "abachrome/illuminants/d65"
|
|
64
|
+
autoload :D75, "abachrome/illuminants/d75"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
module Named
|
|
68
|
+
autoload :CSS, "abachrome/named/css"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
module Outputs
|
|
72
|
+
autoload :CSS, "abachrome/outputs/css"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
module Parsers
|
|
76
|
+
autoload :Hex, "abachrome/parsers/hex"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Creates a new color in the specified color space with given coordinates and alpha value.
|
|
80
|
+
#
|
|
81
|
+
# @param space_name [Symbol, String] The name of the color space (e.g., :srgb, :oklch)
|
|
82
|
+
# @param coordinates [Array<Numeric>] The color coordinates in the specified color space
|
|
83
|
+
# @param alpha [Float] The alpha (opacity) value of the color, defaults to 1.0 (fully opaque)
|
|
84
|
+
# @return [Abachrome::Color] A new Color object in the specified color space with the given coordinates
|
|
85
|
+
def create_color(space_name, *coordinates, alpha: 1.0)
|
|
86
|
+
space = ColorSpace.find(space_name)
|
|
87
|
+
Color.new(space, coordinates, alpha)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Creates a color object from RGB values.
|
|
91
|
+
#
|
|
92
|
+
# @param r [Numeric] The red component value (typically 0-255 or 0.0-1.0)
|
|
93
|
+
# @param g [Numeric] The green component value (typically 0-255 or 0.0-1.0)
|
|
94
|
+
# @param b [Numeric] The blue component value (typically 0-255 or 0.0-1.0)
|
|
95
|
+
# @param alpha [Float] The alpha (opacity) component value (0.0-1.0), defaults to 1.0 (fully opaque)
|
|
96
|
+
# @return [Abachrome::Color] A new Color object initialized with the specified RGB values
|
|
97
|
+
def from_rgb(r, g, b, alpha = 1.0)
|
|
98
|
+
Color.from_rgb(r, g, b, alpha)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Creates a color in the OKLAB color space.
|
|
102
|
+
#
|
|
103
|
+
# @param l [Numeric] The lightness component (L) in the OKLAB color space, typically in range 0 to 1
|
|
104
|
+
# @param a [Numeric] The green-red component (a) in the OKLAB color space
|
|
105
|
+
# @param b [Numeric] The blue-yellow component (b) in the OKLAB color space
|
|
106
|
+
# @param alpha [Float] The alpha (opacity) value, ranging from 0.0 (transparent) to 1.0 (opaque), defaults to 1.0
|
|
107
|
+
# @return [Abachrome::Color] A new Color object in the OKLAB color space
|
|
108
|
+
def from_oklab(l, a, b, alpha = 1.0)
|
|
109
|
+
Color.from_oklab(l, a, b, alpha)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Creates a new color from OKLCH color space values.
|
|
113
|
+
#
|
|
114
|
+
# @param l [Numeric] The lightness value, typically in range 0-1
|
|
115
|
+
# @param a [Numeric] The chroma (colorfulness) value
|
|
116
|
+
# @param b [Numeric] The hue angle value in degrees (0-360)
|
|
117
|
+
# @param alpha [Numeric] The alpha (opacity) value, between 0-1 (default: 1.0)
|
|
118
|
+
# @return [Abachrome::Color] A new color object initialized with the given OKLCH values
|
|
119
|
+
def from_oklch(l, a, b, alpha = 1.0)
|
|
120
|
+
Color.from_oklch(l, a, b, alpha)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Creates a color object from a hexadecimal color code string.
|
|
124
|
+
#
|
|
125
|
+
# @param hex_str [String] The hexadecimal color code string to parse. Can be in formats like
|
|
126
|
+
# "#RGB", "#RRGGBB", "RGB", or "RRGGBB", with or without the leading "#" character.
|
|
127
|
+
# @return [Abachrome::Color] A new Color object representing the parsed hexadecimal color.
|
|
128
|
+
# @example
|
|
129
|
+
# Abachrome.from_hex("#ff0000") # => returns a red Color object
|
|
130
|
+
# Abachrome.from_hex("00ff00") # => returns a green Color object
|
|
131
|
+
# @see Abachrome::Parsers::Hex.parse
|
|
132
|
+
def from_hex(hex_str)
|
|
133
|
+
Parsers::Hex.parse(hex_str)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Creates a color object from a CSS color name.
|
|
137
|
+
#
|
|
138
|
+
# @param color_name [String] The CSS color name (e.g., 'red', 'blue', 'cornflowerblue').
|
|
139
|
+
# Case-insensitive.
|
|
140
|
+
# @return [Abachrome::Color, nil] A color object in the RGB color space if the name is valid,
|
|
141
|
+
# nil if the color name is not recognized.
|
|
142
|
+
def from_name(color_name)
|
|
143
|
+
rgb_values = Named::CSS::ColorNames[color_name.downcase]
|
|
144
|
+
return nil unless rgb_values
|
|
145
|
+
|
|
146
|
+
from_rgb(*rgb_values.map { |v| v / 255.0 })
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Creates a color in the YIQ color space.
|
|
150
|
+
#
|
|
151
|
+
# @param y [Numeric] The luma (brightness) component, typically in range 0 to 1
|
|
152
|
+
# @param i [Numeric] The in-phase component (orange-blue), typically in range -0.5957 to 0.5957
|
|
153
|
+
# @param q [Numeric] The quadrature component (purple-green), typically in range -0.5226 to 0.5226
|
|
154
|
+
# @param alpha [Float] The alpha (opacity) value, ranging from 0.0 (transparent) to 1.0 (opaque), defaults to 1.0
|
|
155
|
+
# @return [Abachrome::Color] A new Color object in the YIQ color space
|
|
156
|
+
def from_yiq(y, i, q, alpha = 1.0)
|
|
157
|
+
Color.from_yiq(y, i, q, alpha)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Creates a color in the CMYK color space.
|
|
161
|
+
#
|
|
162
|
+
# @param c [Numeric] The cyan component, typically in range 0 to 1 (or 0 to 100 for percentages)
|
|
163
|
+
# @param m [Numeric] The magenta component, typically in range 0 to 1 (or 0 to 100 for percentages)
|
|
164
|
+
# @param y [Numeric] The yellow component, typically in range 0 to 1 (or 0 to 100 for percentages)
|
|
165
|
+
# @param k [Numeric] The key/black component, typically in range 0 to 1 (or 0 to 100 for percentages)
|
|
166
|
+
# @param alpha [Float] The alpha (opacity) value, ranging from 0.0 (transparent) to 1.0 (opaque), defaults to 1.0
|
|
167
|
+
# @return [Abachrome::Color] A new Color object in the CMYK color space
|
|
168
|
+
def from_cmyk(c, m, y, k, alpha = 1.0)
|
|
169
|
+
Color.from_cmyk(c, m, y, k, alpha)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Mix multiple colors using Kubelka-Munk spectral mixing.
|
|
173
|
+
#
|
|
174
|
+
# This function produces more realistic color mixing than simple RGB or LAB interpolation
|
|
175
|
+
# by simulating how real pigments absorb and scatter light. Based on spectral.js by
|
|
176
|
+
# Ronald van Wijnen (https://github.com/rvanwijnen/spectral.js)
|
|
177
|
+
#
|
|
178
|
+
# @param colors [Array<Hash>] Array of hashes with :color (Abachrome::Color) and :weight (Numeric)
|
|
179
|
+
# @param tinting_strengths [Hash] Optional hash mapping colors to tinting strengths (default: 1.0)
|
|
180
|
+
# @return [Abachrome::Color] The mixed color
|
|
181
|
+
#
|
|
182
|
+
# @example Mix red and blue with equal weights
|
|
183
|
+
# red = Abachrome.from_rgb(1, 0, 0)
|
|
184
|
+
# blue = Abachrome.from_rgb(0, 0, 1)
|
|
185
|
+
# purple = Abachrome.spectral_mix([
|
|
186
|
+
# {color: red, weight: 1},
|
|
187
|
+
# {color: blue, weight: 1}
|
|
188
|
+
# ])
|
|
189
|
+
#
|
|
190
|
+
# @example Mix three colors with different weights
|
|
191
|
+
# mixed = Abachrome.spectral_mix([
|
|
192
|
+
# {color: red, weight: 2},
|
|
193
|
+
# {color: green, weight: 1},
|
|
194
|
+
# {color: blue, weight: 1}
|
|
195
|
+
# ])
|
|
196
|
+
def spectral_mix(colors, tinting_strengths: {})
|
|
197
|
+
Spectral.mix(colors, tinting_strengths: tinting_strengths)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Parses a CSS color string and returns a Color object.
|
|
201
|
+
#
|
|
202
|
+
# @param css_string [String] The CSS color string to parse (e.g., "#ff0000", "rgb(255, 0, 0)", "red")
|
|
203
|
+
# @return [Abachrome::Color, nil] A Color object if parsing succeeds, nil otherwise
|
|
204
|
+
def parse(css_string)
|
|
205
|
+
require_relative "abachrome/parsers/css"
|
|
206
|
+
Parsers::CSS.parse(css_string)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Convert a color from its current color space to another color space.
|
|
210
|
+
#
|
|
211
|
+
# @param color [Abachrome::Color] The color object to convert
|
|
212
|
+
# @param to_space [Symbol, String] The destination color space identifier (e.g. :srgb, :oklch)
|
|
213
|
+
# @return [Abachrome::Color] A new color object in the specified color space
|
|
214
|
+
def convert(color, to_space)
|
|
215
|
+
Converter.convert(color, to_space)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Register a new color space with the Abachrome library.
|
|
219
|
+
#
|
|
220
|
+
# @param name [Symbol, String] The identifier for the color space being registered
|
|
221
|
+
# @param block [Proc] A block that defines the color space properties and conversion rules
|
|
222
|
+
# @return [Abachrome::ColorSpace] The newly registered color space object
|
|
223
|
+
def register_color_space(name, &block)
|
|
224
|
+
ColorSpace.register(name, &block)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Register a new color space converter in the Abachrome system.
|
|
228
|
+
#
|
|
229
|
+
# This method allows registering custom converters between color spaces.
|
|
230
|
+
# Converters are used to transform color representations from one color
|
|
231
|
+
# space to another.
|
|
232
|
+
#
|
|
233
|
+
# @param from_space [Symbol, String] The source color space identifier
|
|
234
|
+
# @param to_space [Symbol, String] The destination color space identifier
|
|
235
|
+
# @param converter [#call] An object responding to #call that performs the conversion
|
|
236
|
+
# @return [void]
|
|
237
|
+
def register_converter(from_space, to_space, converter)
|
|
238
|
+
Converter.register(from_space, to_space, converter)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
data/logo.png
ADDED
|
Binary file
|
data/logo.webp
ADDED
|
Binary file
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Initial Security Assessment for Abachrome
|
|
2
|
+
|
|
3
|
+
## Assessment Date
|
|
4
|
+
2025-10-12
|
|
5
|
+
|
|
6
|
+
## Scope
|
|
7
|
+
This assessment covers the Abachrome Ruby gem, focusing on color manipulation and conversion functionality.
|
|
8
|
+
|
|
9
|
+
## Findings
|
|
10
|
+
|
|
11
|
+
### Positive Security Aspects
|
|
12
|
+
- **No External Dependencies**: Pure Ruby implementation with minimal dependencies
|
|
13
|
+
- **Immutable Objects**: Color objects are immutable, preventing accidental modification
|
|
14
|
+
- **Input Validation**: All parsing operations include validation
|
|
15
|
+
- **No Network Operations**: All computations are local
|
|
16
|
+
- **No File System Access**: No reading/writing of files
|
|
17
|
+
- **No System Calls**: Pure mathematical computations
|
|
18
|
+
|
|
19
|
+
### Potential Risks
|
|
20
|
+
- **Parsing Complex Inputs**: CSS color parsing could be vulnerable to malformed input
|
|
21
|
+
- **BigDecimal Precision**: High precision could lead to DoS via very large numbers
|
|
22
|
+
- **Memory Usage**: Large color palettes could consume significant memory
|
|
23
|
+
|
|
24
|
+
### Recommendations
|
|
25
|
+
1. Implement input length limits for parsing operations
|
|
26
|
+
2. Add timeout protections for complex computations
|
|
27
|
+
3. Validate coordinate ranges strictly
|
|
28
|
+
4. Consider rate limiting for palette operations
|
|
29
|
+
5. Regular dependency updates and security scans
|
|
30
|
+
|
|
31
|
+
## Threat Model
|
|
32
|
+
|
|
33
|
+
### Actors
|
|
34
|
+
- **Users**: Developers using the gem
|
|
35
|
+
- **Attackers**: Malicious users attempting to exploit parsing or computation
|
|
36
|
+
|
|
37
|
+
### Assets
|
|
38
|
+
- System resources (CPU, memory)
|
|
39
|
+
- User data integrity
|
|
40
|
+
|
|
41
|
+
### Threats
|
|
42
|
+
- DoS via computationally expensive inputs
|
|
43
|
+
- Memory exhaustion via large data structures
|
|
44
|
+
- Parsing exploits in CSS color functions
|
|
45
|
+
|
|
46
|
+
### Mitigations
|
|
47
|
+
- Input sanitization and validation
|
|
48
|
+
- Reasonable limits on data sizes
|
|
49
|
+
- Immutable data structures
|
|
50
|
+
- Pure functional operations where possible
|
|
51
|
+
|
|
52
|
+
## Conclusion
|
|
53
|
+
The gem has a strong security posture due to its pure computational nature and lack of external interfaces. Focus should be on input validation and resource limits for production use.
|
data/security/vex.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"document": {
|
|
3
|
+
"id": "abachrome-vex",
|
|
4
|
+
"author": "Durable Programming",
|
|
5
|
+
"timestamp": "2025-10-12T00:00:00Z",
|
|
6
|
+
"version": "1.0"
|
|
7
|
+
},
|
|
8
|
+
"product_tree": {
|
|
9
|
+
"branches": [
|
|
10
|
+
{
|
|
11
|
+
"type": "package-url",
|
|
12
|
+
"name": "pkg:gem/abachrome",
|
|
13
|
+
"product": {
|
|
14
|
+
"name": "Abachrome",
|
|
15
|
+
"version": "*"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"vulnerabilities": []
|
|
21
|
+
}
|