/src/serenity/Userland/Libraries/LibGfx/ICC/Profile.h
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2022-2023, Nico Weber <thakis@chromium.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #pragma once |
8 | | |
9 | | #include <AK/Error.h> |
10 | | #include <AK/Format.h> |
11 | | #include <AK/HashMap.h> |
12 | | #include <AK/NonnullRefPtr.h> |
13 | | #include <AK/RefCounted.h> |
14 | | #include <AK/Span.h> |
15 | | #include <LibCrypto/Hash/MD5.h> |
16 | | #include <LibGfx/Bitmap.h> |
17 | | #include <LibGfx/CIELAB.h> |
18 | | #include <LibGfx/ICC/DistinctFourCC.h> |
19 | | #include <LibGfx/ICC/TagTypes.h> |
20 | | #include <LibGfx/Matrix3x3.h> |
21 | | #include <LibGfx/Vector3.h> |
22 | | #include <LibURL/URL.h> |
23 | | |
24 | | namespace Gfx::ICC { |
25 | | |
26 | | URL::URL device_manufacturer_url(DeviceManufacturer); |
27 | | URL::URL device_model_url(DeviceModel); |
28 | | |
29 | | // ICC v4, 7.2.4 Profile version field |
30 | | class Version { |
31 | | public: |
32 | 447 | Version() = default; |
33 | | Version(u8 major, u8 minor_and_bugfix) |
34 | 425 | : m_major_version(major) |
35 | 425 | , m_minor_and_bugfix_version(minor_and_bugfix) |
36 | 425 | { |
37 | 425 | } |
38 | | |
39 | 252 | u8 major_version() const { return m_major_version; } |
40 | 0 | u8 minor_version() const { return m_minor_and_bugfix_version >> 4; } |
41 | 0 | u8 bugfix_version() const { return m_minor_and_bugfix_version & 0xf; } |
42 | | |
43 | 0 | u8 minor_and_bugfix_version() const { return m_minor_and_bugfix_version; } |
44 | | |
45 | | private: |
46 | | u8 m_major_version = 0; |
47 | | u8 m_minor_and_bugfix_version = 0; |
48 | | }; |
49 | | |
50 | | // ICC v4, 7.2.11 Profile flags field |
51 | | class Flags { |
52 | | public: |
53 | | Flags(); |
54 | | |
55 | | // "The profile flags field contains flags." |
56 | | Flags(u32); |
57 | | |
58 | 0 | u32 bits() const { return m_bits; } |
59 | | |
60 | | // "These can indicate various hints for the CMM such as distributed processing and caching options." |
61 | | // "The least-significant 16 bits are reserved for the ICC." |
62 | 0 | u16 color_management_module_bits() const { return bits() >> 16; } |
63 | 0 | u16 icc_bits() const { return bits() & 0xffff; } |
64 | | |
65 | | // "Bit position 0: Embedded profile (0 if not embedded, 1 if embedded in file)" |
66 | 0 | bool is_embedded_in_file() const { return (icc_bits() & 1) != 0; } |
67 | | |
68 | | // "Bit position 1: Profile cannot be used independently of the embedded colour data (set to 1 if true, 0 if false)" |
69 | | // Double negation isn't unconfusing, so this function uses the inverted, positive sense. |
70 | 0 | bool can_be_used_independently_of_embedded_color_data() const { return (icc_bits() & 2) == 0; } |
71 | | |
72 | | static constexpr u32 KnownBitsMask = 3; |
73 | | |
74 | | private: |
75 | | u32 m_bits = 0; |
76 | | }; |
77 | | |
78 | | // ICC v4, 7.2.14 Device attributes field |
79 | | class DeviceAttributes { |
80 | | public: |
81 | | DeviceAttributes(); |
82 | | |
83 | | // "The device attributes field shall contain flags used to identify attributes |
84 | | // unique to the particular device setup for which the profile is applicable." |
85 | | DeviceAttributes(u64); |
86 | | |
87 | 0 | u64 bits() const { return m_bits; } |
88 | | |
89 | | // "The least-significant 32 bits of this 64-bit value are defined by the ICC. " |
90 | 0 | u32 icc_bits() const { return bits() & 0xffff'ffff; } |
91 | | |
92 | | // "Notice that bits 0, 1, 2, and 3 describe the media, not the device." |
93 | | |
94 | | // "0": "Reflective (0) or transparency (1)" |
95 | | enum class MediaReflectivity { |
96 | | Reflective, |
97 | | Transparent, |
98 | | }; |
99 | 0 | MediaReflectivity media_reflectivity() const { return MediaReflectivity(icc_bits() & 1); } |
100 | | |
101 | | // "1": "Glossy (0) or matte (1)" |
102 | | enum class MediaGlossiness { |
103 | | Glossy, |
104 | | Matte, |
105 | | }; |
106 | 0 | MediaGlossiness media_glossiness() const { return MediaGlossiness((icc_bits() >> 1) & 1); } |
107 | | |
108 | | // "2": "Media polarity, positive (0) or negative (1)" |
109 | | enum class MediaPolarity { |
110 | | Positive, |
111 | | Negative, |
112 | | }; |
113 | 0 | MediaPolarity media_polarity() const { return MediaPolarity((icc_bits() >> 2) & 1); } |
114 | | |
115 | | // "3": "Colour media (0), black & white media (1)" |
116 | | enum class MediaColor { |
117 | | Colored, |
118 | | BlackAndWhite, |
119 | | }; |
120 | 0 | MediaColor media_color() const { return MediaColor((icc_bits() >> 3) & 1); } |
121 | | |
122 | | // "4 to 31": Reserved (set to binary zero)" |
123 | | |
124 | | // "32 to 63": "Use not defined by ICC (vendor specific" |
125 | 0 | u32 vendor_bits() const { return bits() >> 32; } |
126 | | |
127 | | static constexpr u64 KnownBitsMask = 0xf; |
128 | | |
129 | | private: |
130 | | u64 m_bits = 0; |
131 | | }; |
132 | | |
133 | | // Time is in UTC. |
134 | | // Per spec, month is 1-12, day is 1-31, hours is 0-23, minutes 0-59, seconds 0-59 (i.e. no leap seconds). |
135 | | // But in practice, some profiles have invalid dates, like 0-0-0 0:0:0. |
136 | | // For valid profiles, the conversion to time_t will succeed. |
137 | | struct DateTime { |
138 | | u16 year = 1970; |
139 | | u16 month = 1; // One-based. |
140 | | u16 day = 1; // One-based. |
141 | | u16 hours = 0; |
142 | | u16 minutes = 0; |
143 | | u16 seconds = 0; |
144 | | |
145 | | ErrorOr<time_t> to_time_t() const; |
146 | | static ErrorOr<DateTime> from_time_t(time_t); |
147 | | }; |
148 | | |
149 | | struct ProfileHeader { |
150 | | u32 on_disk_size { 0 }; |
151 | | Optional<PreferredCMMType> preferred_cmm_type; |
152 | | Version version; |
153 | | DeviceClass device_class {}; |
154 | | ColorSpace data_color_space {}; |
155 | | ColorSpace connection_space {}; |
156 | | DateTime creation_timestamp; |
157 | | Optional<PrimaryPlatform> primary_platform {}; |
158 | | Flags flags; |
159 | | Optional<DeviceManufacturer> device_manufacturer; |
160 | | Optional<DeviceModel> device_model; |
161 | | DeviceAttributes device_attributes; |
162 | | RenderingIntent rendering_intent {}; |
163 | | XYZ pcs_illuminant; |
164 | | Optional<Creator> creator; |
165 | | Optional<Crypto::Hash::MD5::DigestType> id; |
166 | | }; |
167 | | |
168 | | // FIXME: This doesn't belong here. |
169 | | class MatrixMatrixConversion { |
170 | | public: |
171 | | MatrixMatrixConversion(LutCurveType source_red_TRC, |
172 | | LutCurveType source_green_TRC, |
173 | | LutCurveType source_blue_TRC, |
174 | | FloatMatrix3x3 matrix, |
175 | | LutCurveType destination_red_TRC, |
176 | | LutCurveType destination_green_TRC, |
177 | | LutCurveType destination_blue_TRC); |
178 | | |
179 | | Color map(FloatVector3) const; |
180 | | |
181 | | private: |
182 | | LutCurveType m_source_red_TRC; |
183 | | LutCurveType m_source_green_TRC; |
184 | | LutCurveType m_source_blue_TRC; |
185 | | FloatMatrix3x3 m_matrix; |
186 | | LutCurveType m_destination_red_TRC; |
187 | | LutCurveType m_destination_green_TRC; |
188 | | LutCurveType m_destination_blue_TRC; |
189 | | }; |
190 | | |
191 | | inline Color MatrixMatrixConversion::map(FloatVector3 in_rgb) const |
192 | 0 | { |
193 | 0 | auto evaluate_curve = [](TagData const& trc, float f) { |
194 | 0 | if (trc.type() == CurveTagData::Type) |
195 | 0 | return static_cast<CurveTagData const&>(trc).evaluate(f); |
196 | 0 | return static_cast<ParametricCurveTagData const&>(trc).evaluate(f); |
197 | 0 | }; |
198 | |
|
199 | 0 | auto evaluate_curve_inverse = [](TagData const& trc, float f) { |
200 | 0 | if (trc.type() == CurveTagData::Type) |
201 | 0 | return static_cast<CurveTagData const&>(trc).evaluate_inverse(f); |
202 | 0 | return static_cast<ParametricCurveTagData const&>(trc).evaluate_inverse(f); |
203 | 0 | }; |
204 | |
|
205 | 0 | FloatVector3 linear_rgb = { |
206 | 0 | evaluate_curve(m_source_red_TRC, in_rgb[0]), |
207 | 0 | evaluate_curve(m_source_green_TRC, in_rgb[1]), |
208 | 0 | evaluate_curve(m_source_blue_TRC, in_rgb[2]), |
209 | 0 | }; |
210 | 0 | linear_rgb = m_matrix * linear_rgb; |
211 | |
|
212 | 0 | linear_rgb.clamp(0.f, 1.f); |
213 | 0 | float device_r = evaluate_curve_inverse(m_destination_red_TRC, linear_rgb[0]); |
214 | 0 | float device_g = evaluate_curve_inverse(m_destination_green_TRC, linear_rgb[1]); |
215 | 0 | float device_b = evaluate_curve_inverse(m_destination_blue_TRC, linear_rgb[2]); |
216 | |
|
217 | 0 | u8 out_r = round(255 * device_r); |
218 | 0 | u8 out_g = round(255 * device_g); |
219 | 0 | u8 out_b = round(255 * device_b); |
220 | |
|
221 | 0 | return Color(out_r, out_g, out_b); |
222 | 0 | } |
223 | | |
224 | | class Profile : public RefCounted<Profile> { |
225 | | public: |
226 | | static ErrorOr<NonnullRefPtr<Profile>> try_load_from_externally_owned_memory(ReadonlyBytes); |
227 | | static ErrorOr<ProfileHeader> read_header(ReadonlyBytes); |
228 | | static ErrorOr<NonnullRefPtr<Profile>> create(ProfileHeader const& header, OrderedHashMap<TagSignature, NonnullRefPtr<TagData>> tag_table); |
229 | | |
230 | 0 | Optional<PreferredCMMType> preferred_cmm_type() const { return m_header.preferred_cmm_type; } |
231 | 252 | Version version() const { return m_header.version; } |
232 | 124 | DeviceClass device_class() const { return m_header.device_class; } |
233 | 0 | ColorSpace data_color_space() const { return m_header.data_color_space; } |
234 | | |
235 | | // For non-DeviceLink profiles, always PCSXYZ or PCSLAB. |
236 | 16 | ColorSpace connection_space() const { return m_header.connection_space; } |
237 | | |
238 | 0 | u32 on_disk_size() const { return m_header.on_disk_size; } |
239 | 0 | DateTime creation_timestamp() const { return m_header.creation_timestamp; } |
240 | 0 | Optional<PrimaryPlatform> primary_platform() const { return m_header.primary_platform; } |
241 | 0 | Flags flags() const { return m_header.flags; } |
242 | 0 | Optional<DeviceManufacturer> device_manufacturer() const { return m_header.device_manufacturer; } |
243 | 0 | Optional<DeviceModel> device_model() const { return m_header.device_model; } |
244 | 0 | DeviceAttributes device_attributes() const { return m_header.device_attributes; } |
245 | 0 | RenderingIntent rendering_intent() const { return m_header.rendering_intent; } |
246 | 0 | XYZ const& pcs_illuminant() const { return m_header.pcs_illuminant; } |
247 | 0 | Optional<Creator> creator() const { return m_header.creator; } |
248 | 183 | Optional<Crypto::Hash::MD5::DigestType> const& id() const { return m_header.id; } |
249 | | |
250 | | static Crypto::Hash::MD5::DigestType compute_id(ReadonlyBytes); |
251 | | |
252 | | template<typename Callback> |
253 | | void for_each_tag(Callback callback) const |
254 | | { |
255 | | for (auto const& tag : m_tag_table) |
256 | | callback(tag.key, tag.value); |
257 | | } |
258 | | |
259 | | template<FallibleFunction<TagSignature, NonnullRefPtr<TagData>> Callback> |
260 | | ErrorOr<void> try_for_each_tag(Callback&& callback) const |
261 | | { |
262 | | for (auto const& tag : m_tag_table) |
263 | | TRY(callback(tag.key, tag.value)); |
264 | | return {}; |
265 | | } |
266 | | |
267 | | Optional<TagData const&> tag_data(TagSignature signature) const |
268 | 0 | { |
269 | 0 | return m_tag_table.get(signature).map([](auto it) -> TagData const& { return *it; }); |
270 | 0 | } |
271 | | |
272 | | Optional<String> tag_string_data(TagSignature signature) const; |
273 | | |
274 | 0 | size_t tag_count() const { return m_tag_table.size(); } |
275 | | |
276 | | // Only versions 2 and 4 are in use. |
277 | 98 | bool is_v2() const { return version().major_version() == 2; } |
278 | 154 | bool is_v4() const { return version().major_version() == 4; } |
279 | | |
280 | | // FIXME: The color conversion stuff should be in some other class. |
281 | | |
282 | | // Converts an 8-bits-per-channel color to the profile connection space. |
283 | | // The color's number of channels must match number_of_components_in_color_space(data_color_space()). |
284 | | // Do not call for DeviceLink or NamedColor profiles. (XXX others?) |
285 | | // Call connection_space() to find out the space the result is in. |
286 | | ErrorOr<FloatVector3> to_pcs(ReadonlyBytes) const; |
287 | | |
288 | | // Converts from the profile connection space to an 8-bits-per-channel color. |
289 | | // The notes on `to_pcs()` apply to this too. |
290 | | ErrorOr<void> from_pcs(Profile const& source_profile, FloatVector3, Bytes) const; |
291 | | ErrorOr<void> from_pcs(ColorSpace source_connection_space, XYZ const& source_illuminant, FloatVector3, Bytes) const; |
292 | | |
293 | | ErrorOr<CIELAB> to_lab(ReadonlyBytes) const; |
294 | | |
295 | | ErrorOr<void> convert_image(Bitmap&, Profile const& source_profile) const; |
296 | | ErrorOr<void> convert_cmyk_image(Bitmap&, CMYKBitmap const&, Profile const& source_profile) const; |
297 | | |
298 | | ErrorOr<void> convert_cmyk_image_to_cmyk_image(CMYKBitmap&, Profile const& source_profile) const; |
299 | | ErrorOr<void> convert_image_to_cmyk_image(CMYKBitmap&, Bitmap const&, Profile const& source_profile) const; |
300 | | |
301 | | // Only call these if you know that this is an RGB matrix-based profile. |
302 | | XYZ const& red_matrix_column() const; |
303 | | XYZ const& green_matrix_column() const; |
304 | | XYZ const& blue_matrix_column() const; |
305 | | |
306 | | Optional<MatrixMatrixConversion> matrix_matrix_conversion(Profile const& source_profile) const; |
307 | | |
308 | | private: |
309 | | Profile(ProfileHeader const& header, OrderedHashMap<TagSignature, NonnullRefPtr<TagData>> tag_table) |
310 | 70 | : m_header(header) |
311 | 70 | , m_tag_table(move(tag_table)) |
312 | 70 | { |
313 | 70 | } |
314 | | |
315 | | XYZ const& xyz_data(TagSignature tag) const |
316 | 0 | { |
317 | 0 | auto const& data = *m_tag_table.get(tag).value(); |
318 | 0 | VERIFY(data.type() == XYZTagData::Type); |
319 | 0 | return static_cast<XYZTagData const&>(data).xyz(); |
320 | 0 | } |
321 | | |
322 | | ErrorOr<void> check_required_tags(); |
323 | | ErrorOr<void> check_tag_types(); |
324 | | |
325 | | ProfileHeader m_header; |
326 | | OrderedHashMap<TagSignature, NonnullRefPtr<TagData>> m_tag_table; |
327 | | |
328 | | // FIXME: The color conversion stuff should be in some other class. |
329 | | ErrorOr<FloatVector3> to_pcs_a_to_b(TagData const& tag_data, ReadonlyBytes) const; |
330 | | ErrorOr<void> from_pcs_b_to_a(TagData const& tag_data, FloatVector3 const&, Bytes) const; |
331 | | ErrorOr<void> convert_image_matrix_matrix(Gfx::Bitmap&, MatrixMatrixConversion const&) const; |
332 | | |
333 | | // Cached values. |
334 | | bool m_cached_has_any_a_to_b_tag { false }; |
335 | | bool m_cached_has_a_to_b0_tag { false }; |
336 | | bool m_cached_has_any_b_to_a_tag { false }; |
337 | | bool m_cached_has_b_to_a0_tag { false }; |
338 | | bool m_cached_has_all_rgb_matrix_tags { false }; |
339 | | |
340 | | // Only valid for RGB matrix-based profiles. |
341 | | ErrorOr<FloatMatrix3x3> xyz_to_rgb_matrix() const; |
342 | | FloatMatrix3x3 rgb_to_xyz_matrix() const; |
343 | | |
344 | | mutable Optional<FloatMatrix3x3> m_cached_xyz_to_rgb_matrix; |
345 | | |
346 | | struct OneElementCLUTCache { |
347 | | Vector<u8, 4> key; |
348 | | FloatVector3 value; |
349 | | }; |
350 | | mutable Optional<OneElementCLUTCache> m_to_pcs_clut_cache; |
351 | | }; |
352 | | |
353 | | } |
354 | | |
355 | | template<> |
356 | | struct AK::Formatter<Gfx::ICC::Version> : Formatter<FormatString> { |
357 | | ErrorOr<void> format(FormatBuilder& builder, Gfx::ICC::Version const& version) |
358 | 0 | { |
359 | 0 | return Formatter<FormatString>::format(builder, "{}.{}.{}"sv, version.major_version(), version.minor_version(), version.bugfix_version()); |
360 | 0 | } |
361 | | }; |