Serenity Operating System
1/*
2 * Copyright (c) 2021-2022, Matthew Olsson <mattco@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <LibPDF/ColorSpace.h>
8#include <LibPDF/CommonNames.h>
9#include <LibPDF/Document.h>
10#include <LibPDF/ObjectDerivatives.h>
11
12namespace PDF {
13
14#define ENUMERATE(name, ever_needs_parameters) \
15 ColorSpaceFamily ColorSpaceFamily::name { #name, ever_needs_parameters };
16ENUMERATE_COLOR_SPACE_FAMILIES(ENUMERATE);
17#undef ENUMERATE
18
19PDFErrorOr<ColorSpaceFamily> ColorSpaceFamily::get(DeprecatedFlyString const& family_name)
20{
21#define ENUMERATE(f_name, ever_needs_parameters) \
22 if (family_name == f_name.name()) { \
23 return ColorSpaceFamily::f_name; \
24 }
25 ENUMERATE_COLOR_SPACE_FAMILIES(ENUMERATE)
26#undef ENUMERATE
27 return Error(Error::Type::MalformedPDF, DeprecatedString::formatted("Unknown ColorSpace family {}", family_name));
28}
29
30PDFErrorOr<NonnullRefPtr<ColorSpace>> ColorSpace::create(DeprecatedFlyString const& name)
31{
32 // Simple color spaces with no parameters, which can be specified directly
33 if (name == CommonNames::DeviceGray)
34 return DeviceGrayColorSpace::the();
35 if (name == CommonNames::DeviceRGB)
36 return DeviceRGBColorSpace::the();
37 if (name == CommonNames::DeviceCMYK)
38 return DeviceCMYKColorSpace::the();
39 if (name == CommonNames::Pattern)
40 TODO();
41 VERIFY_NOT_REACHED();
42}
43
44PDFErrorOr<NonnullRefPtr<ColorSpace>> ColorSpace::create(Document* document, NonnullRefPtr<ArrayObject> color_space_array)
45{
46 auto color_space_name = TRY(color_space_array->get_name_at(document, 0))->name();
47
48 Vector<Value> parameters;
49 parameters.ensure_capacity(color_space_array->size() - 1);
50 for (size_t i = 1; i < color_space_array->size(); i++)
51 parameters.unchecked_append(color_space_array->at(i));
52
53 if (color_space_name == CommonNames::CalRGB)
54 return TRY(CalRGBColorSpace::create(document, move(parameters)));
55
56 if (color_space_name == CommonNames::ICCBased)
57 return TRY(ICCBasedColorSpace::create(document, move(parameters)));
58
59 dbgln("Unknown color space: {}", color_space_name);
60 TODO();
61}
62
63NonnullRefPtr<DeviceGrayColorSpace> DeviceGrayColorSpace::the()
64{
65 static auto instance = adopt_ref(*new DeviceGrayColorSpace());
66 return instance;
67}
68
69Color DeviceGrayColorSpace::color(Vector<Value> const& arguments) const
70{
71 VERIFY(arguments.size() == 1);
72 auto gray = static_cast<u8>(arguments[0].to_float() * 255.0f);
73 return Color(gray, gray, gray);
74}
75
76Vector<float> DeviceGrayColorSpace::default_decode() const
77{
78 return { 0.0f, 1.0f };
79}
80
81NonnullRefPtr<DeviceRGBColorSpace> DeviceRGBColorSpace::the()
82{
83 static auto instance = adopt_ref(*new DeviceRGBColorSpace());
84 return instance;
85}
86
87Color DeviceRGBColorSpace::color(Vector<Value> const& arguments) const
88{
89 VERIFY(arguments.size() == 3);
90 auto r = static_cast<u8>(arguments[0].to_float() * 255.0f);
91 auto g = static_cast<u8>(arguments[1].to_float() * 255.0f);
92 auto b = static_cast<u8>(arguments[2].to_float() * 255.0f);
93 return Color(r, g, b);
94}
95
96Vector<float> DeviceRGBColorSpace::default_decode() const
97{
98 return { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f };
99}
100
101NonnullRefPtr<DeviceCMYKColorSpace> DeviceCMYKColorSpace::the()
102{
103 static auto instance = adopt_ref(*new DeviceCMYKColorSpace());
104 return instance;
105}
106
107Color DeviceCMYKColorSpace::color(Vector<Value> const& arguments) const
108{
109 VERIFY(arguments.size() == 4);
110 auto c = arguments[0].to_float();
111 auto m = arguments[1].to_float();
112 auto y = arguments[2].to_float();
113 auto k = arguments[3].to_float();
114 return Color::from_cmyk(c, m, y, k);
115}
116
117Vector<float> DeviceCMYKColorSpace::default_decode() const
118{
119 return { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f };
120}
121
122PDFErrorOr<NonnullRefPtr<CalRGBColorSpace>> CalRGBColorSpace::create(Document* document, Vector<Value>&& parameters)
123{
124 if (parameters.size() != 1)
125 return Error { Error::Type::MalformedPDF, "RGB color space expects one parameter" };
126
127 auto param = parameters[0];
128 if (!param.has<NonnullRefPtr<Object>>() || !param.get<NonnullRefPtr<Object>>()->is<DictObject>())
129 return Error { Error::Type::MalformedPDF, "RGB color space expects a dict parameter" };
130
131 auto dict = param.get<NonnullRefPtr<Object>>()->cast<DictObject>();
132 if (!dict->contains(CommonNames::WhitePoint))
133 return Error { Error::Type::MalformedPDF, "RGB color space expects a Whitepoint key" };
134
135 auto white_point_array = TRY(dict->get_array(document, CommonNames::WhitePoint));
136 if (white_point_array->size() != 3)
137 return Error { Error::Type::MalformedPDF, "RGB color space expects 3 Whitepoint parameters" };
138
139 auto color_space = adopt_ref(*new CalRGBColorSpace());
140
141 color_space->m_whitepoint[0] = white_point_array->at(0).to_float();
142 color_space->m_whitepoint[1] = white_point_array->at(1).to_float();
143 color_space->m_whitepoint[2] = white_point_array->at(2).to_float();
144
145 if (color_space->m_whitepoint[1] != 1.0f)
146 return Error { Error::Type::MalformedPDF, "RGB color space expects 2nd Whitepoint to be 1.0" };
147
148 if (dict->contains(CommonNames::BlackPoint)) {
149 auto black_point_array = TRY(dict->get_array(document, CommonNames::BlackPoint));
150 if (black_point_array->size() == 3) {
151 color_space->m_blackpoint[0] = black_point_array->at(0).to_float();
152 color_space->m_blackpoint[1] = black_point_array->at(1).to_float();
153 color_space->m_blackpoint[2] = black_point_array->at(2).to_float();
154 }
155 }
156
157 if (dict->contains(CommonNames::Gamma)) {
158 auto gamma_array = TRY(dict->get_array(document, CommonNames::Gamma));
159 if (gamma_array->size() == 3) {
160 color_space->m_gamma[0] = gamma_array->at(0).to_float();
161 color_space->m_gamma[1] = gamma_array->at(1).to_float();
162 color_space->m_gamma[2] = gamma_array->at(2).to_float();
163 }
164 }
165
166 if (dict->contains(CommonNames::Matrix)) {
167 auto matrix_array = TRY(dict->get_array(document, CommonNames::Matrix));
168 if (matrix_array->size() == 3) {
169 color_space->m_matrix[0] = matrix_array->at(0).to_float();
170 color_space->m_matrix[1] = matrix_array->at(1).to_float();
171 color_space->m_matrix[2] = matrix_array->at(2).to_float();
172 color_space->m_matrix[3] = matrix_array->at(3).to_float();
173 color_space->m_matrix[4] = matrix_array->at(4).to_float();
174 color_space->m_matrix[5] = matrix_array->at(5).to_float();
175 color_space->m_matrix[6] = matrix_array->at(6).to_float();
176 color_space->m_matrix[7] = matrix_array->at(7).to_float();
177 color_space->m_matrix[8] = matrix_array->at(8).to_float();
178 }
179 }
180
181 return color_space;
182}
183
184constexpr Array<float, 3> matrix_multiply(Array<float, 9> a, Array<float, 3> b)
185{
186 return Array<float, 3> {
187 a[0] * b[0] + a[1] * b[1] + a[2] * b[2],
188 a[3] * b[0] + a[4] * b[1] + a[5] * b[2],
189 a[6] * b[0] + a[7] * b[1] + a[8] * b[2]
190 };
191}
192
193// Converts to a flat XYZ space with white point = (1, 1, 1)
194// Step 2 of https://www.adobe.com/content/dam/acom/en/devnet/photoshop/sdk/AdobeBPC.pdf
195constexpr Array<float, 3> flatten_and_normalize_whitepoint(Array<float, 3> whitepoint, Array<float, 3> xyz)
196{
197 VERIFY(whitepoint[1] == 1.0f);
198
199 return {
200 (1.0f / whitepoint[0]) * xyz[0],
201 xyz[1],
202 (1.0f / whitepoint[2]) * xyz[2],
203 };
204}
205
206constexpr float decode_l(float input)
207{
208 constexpr float decode_l_scaling_constant = 0.00110705646f; // (((8 + 16) / 116) ^ 3) / 8
209
210 if (input < 0.0f)
211 return -decode_l(-input);
212 if (input >= 0.0f && input <= 8.0f)
213 return input * decode_l_scaling_constant;
214 return powf(((input + 16.0f) / 116.0f), 3.0f);
215}
216
217constexpr Array<float, 3> scale_black_point(Array<float, 3> blackpoint, Array<float, 3> xyz)
218{
219 auto y_dst = decode_l(0); // DestinationBlackPoint is just [0, 0, 0]
220 auto y_src = decode_l(blackpoint[0]);
221 auto scale = (1 - y_dst) / (1 - y_src);
222 auto offset = 1 - scale;
223
224 return {
225 xyz[0] * scale + offset,
226 xyz[1] * scale + offset,
227 xyz[2] * scale + offset,
228 };
229}
230
231// https://en.wikipedia.org/wiki/Illuminant_D65
232constexpr Array<float, 3> convert_to_d65(Array<float, 3> whitepoint, Array<float, 3> xyz)
233{
234 constexpr float d65x = 0.95047f;
235 constexpr float d65y = 1.0f;
236 constexpr float d65z = 1.08883f;
237
238 return {
239 (xyz[0] * d65x) / whitepoint[0],
240 (xyz[1] * d65y) / whitepoint[1],
241 (xyz[2] * d65z) / whitepoint[2],
242 };
243}
244
245// https://en.wikipedia.org/wiki/SRGB
246constexpr Array<float, 3> convert_to_srgb(Array<float, 3> xyz)
247{
248 // See the sRGB D65 [M]^-1 matrix in the following page
249 // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
250 constexpr Array<float, 9> conversion_matrix = {
251 3.2404542,
252 -1.5371385,
253 -0.4985314,
254 -0.969266,
255 1.8760108,
256 0.0415560,
257 0.0556434,
258 -0.2040259,
259 1.0572252,
260 };
261
262 return matrix_multiply(conversion_matrix, xyz);
263}
264
265Color CalRGBColorSpace::color(Vector<Value> const& arguments) const
266{
267 VERIFY(arguments.size() == 3);
268 auto a = clamp(arguments[0].to_float(), 0.0f, 1.0f);
269 auto b = clamp(arguments[1].to_float(), 0.0f, 1.0f);
270 auto c = clamp(arguments[2].to_float(), 0.0f, 1.0f);
271
272 auto agr = powf(a, m_gamma[0]);
273 auto bgg = powf(b, m_gamma[1]);
274 auto cgb = powf(c, m_gamma[2]);
275
276 auto x = m_matrix[0] * agr + m_matrix[3] * bgg + m_matrix[6] * cgb;
277 auto y = m_matrix[1] * agr + m_matrix[4] * bgg + m_matrix[7] * cgb;
278 auto z = m_matrix[2] * agr + m_matrix[5] * bgg + m_matrix[8] * cgb;
279
280 auto flattened_xyz = flatten_and_normalize_whitepoint(m_whitepoint, { x, y, z });
281 auto scaled_black_point_xyz = scale_black_point(m_blackpoint, flattened_xyz);
282 auto d65_normalized = convert_to_d65(m_whitepoint, scaled_black_point_xyz);
283 auto srgb = convert_to_srgb(d65_normalized);
284
285 auto red = static_cast<u8>(srgb[0] * 255.0f);
286 auto green = static_cast<u8>(srgb[1] * 255.0f);
287 auto blue = static_cast<u8>(srgb[2] * 255.0f);
288
289 return Color(red, green, blue);
290}
291
292Vector<float> CalRGBColorSpace::default_decode() const
293{
294 return { 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f };
295}
296
297PDFErrorOr<NonnullRefPtr<ColorSpace>> ICCBasedColorSpace::create(Document* document, Vector<Value>&& parameters)
298{
299 if (parameters.is_empty())
300 return Error { Error::Type::MalformedPDF, "ICCBased color space expected one parameter" };
301
302 auto param = TRY(document->resolve(parameters[0]));
303 if (!param.has<NonnullRefPtr<Object>>() || !param.get<NonnullRefPtr<Object>>()->is<StreamObject>())
304 return Error { Error::Type::MalformedPDF, "ICCBased color space expects a stream parameter" };
305
306 auto dict = param.get<NonnullRefPtr<Object>>()->cast<StreamObject>()->dict();
307
308 DeprecatedFlyString name;
309 if (!dict->contains(CommonNames::Alternate)) {
310 auto number_of_components = dict->get_value(CommonNames::N).to_int();
311 if (number_of_components == 1)
312 name = CommonNames::DeviceGray;
313 else if (number_of_components == 3)
314 name = CommonNames::DeviceRGB;
315 else if (number_of_components == 4)
316 name = CommonNames::DeviceCMYK;
317 else
318 VERIFY_NOT_REACHED();
319 return ColorSpace::create(name);
320 }
321
322 auto alternate_color_space_object = MUST(dict->get_object(document, CommonNames::Alternate));
323 if (alternate_color_space_object->is<NameObject>()) {
324 return ColorSpace::create(alternate_color_space_object->cast<NameObject>()->name());
325 }
326
327 dbgln("Alternate color spaces in array format not supported yet ");
328 TODO();
329}
330
331Color ICCBasedColorSpace::color(Vector<Value> const&) const
332{
333 VERIFY_NOT_REACHED();
334}
335
336Vector<float> ICCBasedColorSpace::default_decode() const
337{
338 VERIFY_NOT_REACHED();
339}
340
341}