|
| 1 | +// Copyright (C) 2023-2025 Lightpanda (Selecy SAS) |
| 2 | +// |
| 3 | +// Francis Bouvier <francis@lightpanda.io> |
| 4 | +// Pierre Tachoire <pierre@lightpanda.io> |
| 5 | +// |
| 6 | +// This program is free software: you can redistribute it and/or modify |
| 7 | +// it under the terms of the GNU Affero General Public License as |
| 8 | +// published by the Free Software Foundation, either version 3 of the |
| 9 | +// License, or (at your option) any later version. |
| 10 | +// |
| 11 | +// This program is distributed in the hope that it will be useful, |
| 12 | +// but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | +// GNU Affero General Public License for more details. |
| 15 | +// |
| 16 | +// You should have received a copy of the GNU Affero General Public License |
| 17 | +// along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 18 | + |
| 19 | +const std = @import("std"); |
| 20 | +const Io = std.Io; |
| 21 | + |
| 22 | +const CSSParser = @import("CSSParser.zig"); |
| 23 | +const isHexColor = @import("CSSStyleDeclaration.zig").isHexColor; |
| 24 | + |
| 25 | +pub const RGBA = packed struct(u32) { |
| 26 | + r: u8, |
| 27 | + g: u8, |
| 28 | + b: u8, |
| 29 | + /// Opaque by default. |
| 30 | + a: u8 = std.math.maxInt(u8), |
| 31 | + |
| 32 | + pub const Named = struct { |
| 33 | + // Basic colors (CSS Level 1) |
| 34 | + pub const black: RGBA = .init(0, 0, 0, 1); |
| 35 | + pub const silver: RGBA = .init(192, 192, 192, 1); |
| 36 | + pub const gray: RGBA = .init(128, 128, 128, 1); |
| 37 | + pub const white: RGBA = .init(255, 255, 255, 1); |
| 38 | + pub const maroon: RGBA = .init(128, 0, 0, 1); |
| 39 | + pub const red: RGBA = .init(255, 0, 0, 1); |
| 40 | + pub const purple: RGBA = .init(128, 0, 128, 1); |
| 41 | + pub const fuchsia: RGBA = .init(255, 0, 255, 1); |
| 42 | + pub const green: RGBA = .init(0, 128, 0, 1); |
| 43 | + pub const lime: RGBA = .init(0, 255, 0, 1); |
| 44 | + pub const olive: RGBA = .init(128, 128, 0, 1); |
| 45 | + pub const yellow: RGBA = .init(255, 255, 0, 1); |
| 46 | + pub const navy: RGBA = .init(0, 0, 128, 1); |
| 47 | + pub const blue: RGBA = .init(0, 0, 255, 1); |
| 48 | + pub const teal: RGBA = .init(0, 128, 128, 1); |
| 49 | + pub const aqua: RGBA = .init(0, 255, 255, 1); |
| 50 | + |
| 51 | + // Extended colors (CSS Level 2+) |
| 52 | + pub const aliceblue: RGBA = .init(240, 248, 255, 1); |
| 53 | + pub const antiquewhite: RGBA = .init(250, 235, 215, 1); |
| 54 | + pub const aquamarine: RGBA = .init(127, 255, 212, 1); |
| 55 | + pub const azure: RGBA = .init(240, 255, 255, 1); |
| 56 | + pub const beige: RGBA = .init(245, 245, 220, 1); |
| 57 | + pub const bisque: RGBA = .init(255, 228, 196, 1); |
| 58 | + pub const blanchedalmond: RGBA = .init(255, 235, 205, 1); |
| 59 | + pub const blueviolet: RGBA = .init(138, 43, 226, 1); |
| 60 | + pub const brown: RGBA = .init(165, 42, 42, 1); |
| 61 | + pub const burlywood: RGBA = .init(222, 184, 135, 1); |
| 62 | + pub const cadetblue: RGBA = .init(95, 158, 160, 1); |
| 63 | + pub const chartreuse: RGBA = .init(127, 255, 0, 1); |
| 64 | + pub const chocolate: RGBA = .init(210, 105, 30, 1); |
| 65 | + pub const coral: RGBA = .init(255, 127, 80, 1); |
| 66 | + pub const cornflowerblue: RGBA = .init(100, 149, 237, 1); |
| 67 | + pub const cornsilk: RGBA = .init(255, 248, 220, 1); |
| 68 | + pub const crimson: RGBA = .init(220, 20, 60, 1); |
| 69 | + pub const cyan: RGBA = .init(0, 255, 255, 1); // Synonym of aqua |
| 70 | + pub const darkblue: RGBA = .init(0, 0, 139, 1); |
| 71 | + pub const darkcyan: RGBA = .init(0, 139, 139, 1); |
| 72 | + pub const darkgoldenrod: RGBA = .init(184, 134, 11, 1); |
| 73 | + pub const darkgray: RGBA = .init(169, 169, 169, 1); |
| 74 | + pub const darkgreen: RGBA = .init(0, 100, 0, 1); |
| 75 | + pub const darkgrey: RGBA = .init(169, 169, 169, 1); // Synonym of darkgray |
| 76 | + pub const darkkhaki: RGBA = .init(189, 183, 107, 1); |
| 77 | + pub const darkmagenta: RGBA = .init(139, 0, 139, 1); |
| 78 | + pub const darkolivegreen: RGBA = .init(85, 107, 47, 1); |
| 79 | + pub const darkorange: RGBA = .init(255, 140, 0, 1); |
| 80 | + pub const darkorchid: RGBA = .init(153, 50, 204, 1); |
| 81 | + pub const darkred: RGBA = .init(139, 0, 0, 1); |
| 82 | + pub const darksalmon: RGBA = .init(233, 150, 122, 1); |
| 83 | + pub const darkseagreen: RGBA = .init(143, 188, 143, 1); |
| 84 | + pub const darkslateblue: RGBA = .init(72, 61, 139, 1); |
| 85 | + pub const darkslategray: RGBA = .init(47, 79, 79, 1); |
| 86 | + pub const darkslategrey: RGBA = .init(47, 79, 79, 1); // Synonym of darkslategray |
| 87 | + pub const darkturquoise: RGBA = .init(0, 206, 209, 1); |
| 88 | + pub const darkviolet: RGBA = .init(148, 0, 211, 1); |
| 89 | + pub const deeppink: RGBA = .init(255, 20, 147, 1); |
| 90 | + pub const deepskyblue: RGBA = .init(0, 191, 255, 1); |
| 91 | + pub const dimgray: RGBA = .init(105, 105, 105, 1); |
| 92 | + pub const dimgrey: RGBA = .init(105, 105, 105, 1); // Synonym of dimgray |
| 93 | + pub const dodgerblue: RGBA = .init(30, 144, 255, 1); |
| 94 | + pub const firebrick: RGBA = .init(178, 34, 34, 1); |
| 95 | + pub const floralwhite: RGBA = .init(255, 250, 240, 1); |
| 96 | + pub const forestgreen: RGBA = .init(34, 139, 34, 1); |
| 97 | + pub const gainsboro: RGBA = .init(220, 220, 220, 1); |
| 98 | + pub const ghostwhite: RGBA = .init(248, 248, 255, 1); |
| 99 | + pub const gold: RGBA = .init(255, 215, 0, 1); |
| 100 | + pub const goldenrod: RGBA = .init(218, 165, 32, 1); |
| 101 | + pub const greenyellow: RGBA = .init(173, 255, 47, 1); |
| 102 | + pub const grey: RGBA = .init(128, 128, 128, 1); // Synonym of gray |
| 103 | + pub const honeydew: RGBA = .init(240, 255, 240, 1); |
| 104 | + pub const hotpink: RGBA = .init(255, 105, 180, 1); |
| 105 | + pub const indianred: RGBA = .init(205, 92, 92, 1); |
| 106 | + pub const indigo: RGBA = .init(75, 0, 130, 1); |
| 107 | + pub const ivory: RGBA = .init(255, 255, 240, 1); |
| 108 | + pub const khaki: RGBA = .init(240, 230, 140, 1); |
| 109 | + pub const lavender: RGBA = .init(230, 230, 250, 1); |
| 110 | + pub const lavenderblush: RGBA = .init(255, 240, 245, 1); |
| 111 | + pub const lawngreen: RGBA = .init(124, 252, 0, 1); |
| 112 | + pub const lemonchiffon: RGBA = .init(255, 250, 205, 1); |
| 113 | + pub const lightblue: RGBA = .init(173, 216, 230, 1); |
| 114 | + pub const lightcoral: RGBA = .init(240, 128, 128, 1); |
| 115 | + pub const lightcyan: RGBA = .init(224, 255, 255, 1); |
| 116 | + pub const lightgoldenrodyellow: RGBA = .init(250, 250, 210, 1); |
| 117 | + pub const lightgray: RGBA = .init(211, 211, 211, 1); |
| 118 | + pub const lightgreen: RGBA = .init(144, 238, 144, 1); |
| 119 | + pub const lightgrey: RGBA = .init(211, 211, 211, 1); // Synonym of lightgray |
| 120 | + pub const lightpink: RGBA = .init(255, 182, 193, 1); |
| 121 | + pub const lightsalmon: RGBA = .init(255, 160, 122, 1); |
| 122 | + pub const lightseagreen: RGBA = .init(32, 178, 170, 1); |
| 123 | + pub const lightskyblue: RGBA = .init(135, 206, 250, 1); |
| 124 | + pub const lightslategray: RGBA = .init(119, 136, 153, 1); |
| 125 | + pub const lightslategrey: RGBA = .init(119, 136, 153, 1); // Synonym of lightslategray |
| 126 | + pub const lightsteelblue: RGBA = .init(176, 196, 222, 1); |
| 127 | + pub const lightyellow: RGBA = .init(255, 255, 224, 1); |
| 128 | + pub const limegreen: RGBA = .init(50, 205, 50, 1); |
| 129 | + pub const linen: RGBA = .init(250, 240, 230, 1); |
| 130 | + pub const magenta: RGBA = .init(255, 0, 255, 1); // Synonym of fuchsia |
| 131 | + pub const mediumaquamarine: RGBA = .init(102, 205, 170, 1); |
| 132 | + pub const mediumblue: RGBA = .init(0, 0, 205, 1); |
| 133 | + pub const mediumorchid: RGBA = .init(186, 85, 211, 1); |
| 134 | + pub const mediumpurple: RGBA = .init(147, 112, 219, 1); |
| 135 | + pub const mediumseagreen: RGBA = .init(60, 179, 113, 1); |
| 136 | + pub const mediumslateblue: RGBA = .init(123, 104, 238, 1); |
| 137 | + pub const mediumspringgreen: RGBA = .init(0, 250, 154, 1); |
| 138 | + pub const mediumturquoise: RGBA = .init(72, 209, 204, 1); |
| 139 | + pub const mediumvioletred: RGBA = .init(199, 21, 133, 1); |
| 140 | + pub const midnightblue: RGBA = .init(25, 25, 112, 1); |
| 141 | + pub const mintcream: RGBA = .init(245, 255, 250, 1); |
| 142 | + pub const mistyrose: RGBA = .init(255, 228, 225, 1); |
| 143 | + pub const moccasin: RGBA = .init(255, 228, 181, 1); |
| 144 | + pub const navajowhite: RGBA = .init(255, 222, 173, 1); |
| 145 | + pub const oldlace: RGBA = .init(253, 245, 230, 1); |
| 146 | + pub const olivedrab: RGBA = .init(107, 142, 35, 1); |
| 147 | + pub const orange: RGBA = .init(255, 165, 0, 1); |
| 148 | + pub const orangered: RGBA = .init(255, 69, 0, 1); |
| 149 | + pub const orchid: RGBA = .init(218, 112, 214, 1); |
| 150 | + pub const palegoldenrod: RGBA = .init(238, 232, 170, 1); |
| 151 | + pub const palegreen: RGBA = .init(152, 251, 152, 1); |
| 152 | + pub const paleturquoise: RGBA = .init(175, 238, 238, 1); |
| 153 | + pub const palevioletred: RGBA = .init(219, 112, 147, 1); |
| 154 | + pub const papayawhip: RGBA = .init(255, 239, 213, 1); |
| 155 | + pub const peachpuff: RGBA = .init(255, 218, 185, 1); |
| 156 | + pub const peru: RGBA = .init(205, 133, 63, 1); |
| 157 | + pub const pink: RGBA = .init(255, 192, 203, 1); |
| 158 | + pub const plum: RGBA = .init(221, 160, 221, 1); |
| 159 | + pub const powderblue: RGBA = .init(176, 224, 230, 1); |
| 160 | + pub const rebeccapurple: RGBA = .init(102, 51, 153, 1); |
| 161 | + pub const rosybrown: RGBA = .init(188, 143, 143, 1); |
| 162 | + pub const royalblue: RGBA = .init(65, 105, 225, 1); |
| 163 | + pub const saddlebrown: RGBA = .init(139, 69, 19, 1); |
| 164 | + pub const salmon: RGBA = .init(250, 128, 114, 1); |
| 165 | + pub const sandybrown: RGBA = .init(244, 164, 96, 1); |
| 166 | + pub const seagreen: RGBA = .init(46, 139, 87, 1); |
| 167 | + pub const seashell: RGBA = .init(255, 245, 238, 1); |
| 168 | + pub const sienna: RGBA = .init(160, 82, 45, 1); |
| 169 | + pub const skyblue: RGBA = .init(135, 206, 235, 1); |
| 170 | + pub const slateblue: RGBA = .init(106, 90, 205, 1); |
| 171 | + pub const slategray: RGBA = .init(112, 128, 144, 1); |
| 172 | + pub const slategrey: RGBA = .init(112, 128, 144, 1); // Synonym of slategray |
| 173 | + pub const snow: RGBA = .init(255, 250, 250, 1); |
| 174 | + pub const springgreen: RGBA = .init(0, 255, 127, 1); |
| 175 | + pub const steelblue: RGBA = .init(70, 130, 180, 1); |
| 176 | + pub const tan: RGBA = .init(210, 180, 140, 1); |
| 177 | + pub const thistle: RGBA = .init(216, 191, 216, 1); |
| 178 | + pub const tomato: RGBA = .init(255, 99, 71, 1); |
| 179 | + pub const transparent: RGBA = .init(0, 0, 0, 0); |
| 180 | + pub const turquoise: RGBA = .init(64, 224, 208, 1); |
| 181 | + pub const violet: RGBA = .init(238, 130, 238, 1); |
| 182 | + pub const wheat: RGBA = .init(245, 222, 179, 1); |
| 183 | + pub const whitesmoke: RGBA = .init(245, 245, 245, 1); |
| 184 | + pub const yellowgreen: RGBA = .init(154, 205, 50, 1); |
| 185 | + }; |
| 186 | + |
| 187 | + pub fn init(r: u8, g: u8, b: u8, a: f32) RGBA { |
| 188 | + const clamped = std.math.clamp(a, 0, 1); |
| 189 | + return .{ .r = r, .g = g, .b = b, .a = @intFromFloat(clamped * 255) }; |
| 190 | + } |
| 191 | + |
| 192 | + /// Finds a color by its name. |
| 193 | + pub fn find(name: []const u8) ?RGBA { |
| 194 | + const match = std.meta.stringToEnum(std.meta.DeclEnum(Named), name) orelse return null; |
| 195 | + |
| 196 | + return switch (match) { |
| 197 | + inline else => |comptime_enum| @field(Named, @tagName(comptime_enum)), |
| 198 | + }; |
| 199 | + } |
| 200 | + |
| 201 | + /// Parses the given color. |
| 202 | + /// Currently we only parse hex colors and named colors; other variants |
| 203 | + /// require CSS evaluation. |
| 204 | + pub fn parse(input: []const u8) !RGBA { |
| 205 | + if (!isHexColor(input)) { |
| 206 | + // Try named colors. |
| 207 | + return find(input) orelse return error.Invalid; |
| 208 | + } |
| 209 | + |
| 210 | + const slice = input[1..]; |
| 211 | + switch (slice.len) { |
| 212 | + // This means the digit for a color is repeated. |
| 213 | + // Given HEX is #f0c, its interpreted the same as #FF00CC. |
| 214 | + 3 => { |
| 215 | + const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16); |
| 216 | + const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16); |
| 217 | + const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16); |
| 218 | + return .{ .r = r, .g = g, .b = b, .a = 255 }; |
| 219 | + }, |
| 220 | + 4 => { |
| 221 | + const r = try std.fmt.parseInt(u8, &.{ slice[0], slice[0] }, 16); |
| 222 | + const g = try std.fmt.parseInt(u8, &.{ slice[1], slice[1] }, 16); |
| 223 | + const b = try std.fmt.parseInt(u8, &.{ slice[2], slice[2] }, 16); |
| 224 | + const a = try std.fmt.parseInt(u8, &.{ slice[3], slice[3] }, 16); |
| 225 | + return .{ .r = r, .g = g, .b = b, .a = a }; |
| 226 | + }, |
| 227 | + // Regular HEX format. |
| 228 | + 6 => { |
| 229 | + const r = try std.fmt.parseInt(u8, slice[0..2], 16); |
| 230 | + const g = try std.fmt.parseInt(u8, slice[2..4], 16); |
| 231 | + const b = try std.fmt.parseInt(u8, slice[4..6], 16); |
| 232 | + return .{ .r = r, .g = g, .b = b, .a = 255 }; |
| 233 | + }, |
| 234 | + 8 => { |
| 235 | + const r = try std.fmt.parseInt(u8, slice[0..2], 16); |
| 236 | + const g = try std.fmt.parseInt(u8, slice[2..4], 16); |
| 237 | + const b = try std.fmt.parseInt(u8, slice[4..6], 16); |
| 238 | + const a = try std.fmt.parseInt(u8, slice[6..8], 16); |
| 239 | + return .{ .r = r, .g = g, .b = b, .a = a }; |
| 240 | + }, |
| 241 | + else => return error.Invalid, |
| 242 | + } |
| 243 | + } |
| 244 | + |
| 245 | + /// By default, browsers prefer lowercase formatting. |
| 246 | + const format_upper = false; |
| 247 | + |
| 248 | + /// Formats the `Color` according to web expectations. |
| 249 | + /// If color is opaque, HEX is preferred; RGBA otherwise. |
| 250 | + pub fn format(self: *const RGBA, writer: *Io.Writer) Io.Writer.Error!void { |
| 251 | + if (self.isOpaque()) { |
| 252 | + // Convert RGB to HEX. |
| 253 | + // https://gristle.tripod.com/hexconv.html |
| 254 | + // Hexadecimal characters up to 15. |
| 255 | + const char: []const u8 = "0123456789" ++ if (format_upper) "ABCDEF" else "abcdef"; |
| 256 | + // This variant always prefers 6 digit format, +1 is for hash char. |
| 257 | + const buffer = [7]u8{ |
| 258 | + '#', |
| 259 | + char[self.r >> 4], |
| 260 | + char[self.r & 15], |
| 261 | + char[self.g >> 4], |
| 262 | + char[self.g & 15], |
| 263 | + char[self.b >> 4], |
| 264 | + char[self.b & 15], |
| 265 | + }; |
| 266 | + |
| 267 | + return writer.writeAll(&buffer); |
| 268 | + } |
| 269 | + |
| 270 | + // Prefer RGBA format for everything else. |
| 271 | + return writer.print("rgba({d}, {d}, {d}, {d:.2})", .{ self.r, self.g, self.b, self.normalizedAlpha() }); |
| 272 | + } |
| 273 | + |
| 274 | + /// Returns true if `Color` is opaque. |
| 275 | + pub inline fn isOpaque(self: *const RGBA) bool { |
| 276 | + return self.a == std.math.maxInt(u8); |
| 277 | + } |
| 278 | + |
| 279 | + /// Returns the normalized alpha value. |
| 280 | + pub inline fn normalizedAlpha(self: *const RGBA) f32 { |
| 281 | + return @as(f32, @floatFromInt(self.a)) / 255; |
| 282 | + } |
| 283 | +}; |
0 commit comments