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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/.rubocop.yml +10 -0
  4. data/CHANGELOG.md +21 -0
  5. data/CLA.md +45 -0
  6. data/CODE-OF-CONDUCT.md +9 -0
  7. data/LICENSE +19 -0
  8. data/README.md +315 -0
  9. data/Rakefile +15 -0
  10. data/SECURITY.md +94 -0
  11. data/abachrome-float.gemspec +36 -0
  12. data/demos/ncurses/plasma.rb +124 -0
  13. data/devenv.lock +171 -0
  14. data/devenv.nix +52 -0
  15. data/devenv.yaml +8 -0
  16. data/lib/abachrome/color.rb +197 -0
  17. data/lib/abachrome/color_mixins/blend.rb +100 -0
  18. data/lib/abachrome/color_mixins/lighten.rb +90 -0
  19. data/lib/abachrome/color_mixins/spectral_mix.rb +70 -0
  20. data/lib/abachrome/color_mixins/to_colorspace.rb +107 -0
  21. data/lib/abachrome/color_mixins/to_grayscale.rb +87 -0
  22. data/lib/abachrome/color_mixins/to_lrgb.rb +121 -0
  23. data/lib/abachrome/color_mixins/to_oklab.rb +117 -0
  24. data/lib/abachrome/color_mixins/to_oklch.rb +110 -0
  25. data/lib/abachrome/color_mixins/to_srgb.rb +142 -0
  26. data/lib/abachrome/color_models/cmyk.rb +159 -0
  27. data/lib/abachrome/color_models/hsv.rb +49 -0
  28. data/lib/abachrome/color_models/lms.rb +38 -0
  29. data/lib/abachrome/color_models/oklab.rb +37 -0
  30. data/lib/abachrome/color_models/oklch.rb +91 -0
  31. data/lib/abachrome/color_models/rgb.rb +58 -0
  32. data/lib/abachrome/color_models/xyz.rb +31 -0
  33. data/lib/abachrome/color_models/yiq.rb +37 -0
  34. data/lib/abachrome/color_space.rb +199 -0
  35. data/lib/abachrome/converter.rb +117 -0
  36. data/lib/abachrome/converters/base.rb +128 -0
  37. data/lib/abachrome/converters/cmyk_to_srgb.rb +42 -0
  38. data/lib/abachrome/converters/lms_to_lrgb.rb +40 -0
  39. data/lib/abachrome/converters/lms_to_srgb.rb +27 -0
  40. data/lib/abachrome/converters/lms_to_xyz.rb +34 -0
  41. data/lib/abachrome/converters/lrgb_to_lms.rb +3 -0
  42. data/lib/abachrome/converters/lrgb_to_oklab.rb +57 -0
  43. data/lib/abachrome/converters/lrgb_to_srgb.rb +59 -0
  44. data/lib/abachrome/converters/lrgb_to_xyz.rb +33 -0
  45. data/lib/abachrome/converters/oklab_to_lms.rb +44 -0
  46. data/lib/abachrome/converters/oklab_to_lrgb.rb +71 -0
  47. data/lib/abachrome/converters/oklab_to_oklch.rb +56 -0
  48. data/lib/abachrome/converters/oklab_to_srgb.rb +46 -0
  49. data/lib/abachrome/converters/oklch_to_lrgb.rb +79 -0
  50. data/lib/abachrome/converters/oklch_to_oklab.rb +52 -0
  51. data/lib/abachrome/converters/oklch_to_srgb.rb +46 -0
  52. data/lib/abachrome/converters/oklch_to_xyz.rb +70 -0
  53. data/lib/abachrome/converters/srgb_to_cmyk.rb +64 -0
  54. data/lib/abachrome/converters/srgb_to_lrgb.rb +55 -0
  55. data/lib/abachrome/converters/srgb_to_oklab.rb +45 -0
  56. data/lib/abachrome/converters/srgb_to_oklch.rb +47 -0
  57. data/lib/abachrome/converters/srgb_to_yiq.rb +49 -0
  58. data/lib/abachrome/converters/xyz_to_lms.rb +34 -0
  59. data/lib/abachrome/converters/xyz_to_oklab.rb +42 -0
  60. data/lib/abachrome/converters/yiq_to_srgb.rb +47 -0
  61. data/lib/abachrome/gamut/base.rb +74 -0
  62. data/lib/abachrome/gamut/p3.rb +27 -0
  63. data/lib/abachrome/gamut/rec2020.rb +25 -0
  64. data/lib/abachrome/gamut/srgb.rb +49 -0
  65. data/lib/abachrome/illuminants/base.rb +35 -0
  66. data/lib/abachrome/illuminants/d50.rb +33 -0
  67. data/lib/abachrome/illuminants/d55.rb +29 -0
  68. data/lib/abachrome/illuminants/d65.rb +37 -0
  69. data/lib/abachrome/illuminants/d75.rb +29 -0
  70. data/lib/abachrome/named/css.rb +157 -0
  71. data/lib/abachrome/named/tailwind.rb +301 -0
  72. data/lib/abachrome/outputs/css.rb +119 -0
  73. data/lib/abachrome/palette.rb +244 -0
  74. data/lib/abachrome/palette_mixins/interpolate.rb +53 -0
  75. data/lib/abachrome/palette_mixins/resample.rb +61 -0
  76. data/lib/abachrome/palette_mixins/stretch_luminance.rb +72 -0
  77. data/lib/abachrome/parsers/css.rb +452 -0
  78. data/lib/abachrome/parsers/hex.rb +52 -0
  79. data/lib/abachrome/parsers/tailwind.rb +45 -0
  80. data/lib/abachrome/spectral.rb +276 -0
  81. data/lib/abachrome/to_abcd.rb +23 -0
  82. data/lib/abachrome/version.rb +7 -0
  83. data/lib/abachrome.rb +242 -0
  84. data/logo.png +0 -0
  85. data/logo.webp +0 -0
  86. data/security/assesments/2025-10-12-SECURITY_ASSESSMENT.md +53 -0
  87. data/security/vex.json +21 -0
  88. 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.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Abachrome
4
+ VERSION = "0.1.6"
5
+ end
6
+
7
+ # 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
+ }