sht_colour/sht/mod.rs
1use super::{rgb, round_denominator};
2use nom::error::Error;
3use num::{rational::Ratio, CheckedAdd, CheckedDiv, CheckedMul, Integer, One, Unsigned, Zero};
4use parser::parse_sht;
5use std::{
6 convert::TryInto,
7 fmt::{Display, Formatter, Result as FMTResult},
8 ops::{Div, Rem},
9 str::FromStr,
10};
11
12/// A representation of a colour in [SHT format](https://omaitzen.com/sht/).
13///
14/// The SHT colour format is intended to be human-readable and human-writable.
15/// For instance, the code for the colour red is `"r"`, and the code for a dark
16/// yellow is `"3y"`. SHT codes cover the same colour space as [`RGB` codes], but
17/// map commonly used colours onto memorable strings.
18///
19/// Extra precision can usually be expressed by appending characters to an
20/// existing code. For instance, darkening the code for red is achieved by
21/// adding a digit to the start, `"9r"`, and `"9r4g"` is the same colour but
22/// with a hint of green.
23///
24/// See the [`Display` implementation] for more details on the format.
25///
26/// # Examples
27/// ```
28/// use sht_colour::{
29/// ChannelRatios::OneBrightestChannel,
30/// ColourChannel::{Green, Red},
31/// Ratio, SHT,
32/// };
33///
34/// // Thid colour is quite red, a bit green, slightly faded
35/// let code = "8r6g3";
36///
37/// // Parse a colour from a string
38/// let parsed_colour = code.parse::<SHT<u8>>().unwrap();
39///
40/// // Construct a colour manually
41/// let shade = Ratio::new(8, 12);
42/// let tint = Ratio::new(3, 12);
43/// let blend = Ratio::new(6, 12);
44/// let constructed_colour = SHT::new(
45/// OneBrightestChannel {
46/// primary: Red,
47/// direction_blend: Some((Green, blend)),
48/// },
49/// shade,
50/// tint,
51/// )
52/// .unwrap();
53///
54/// // Both colours are the same
55/// assert_eq!(constructed_colour, parsed_colour);
56/// // The colour's string representation is the same as the original string
57/// assert_eq!(constructed_colour.to_string(), code);
58/// ```
59///
60/// [`Display` implementation]: SHT#impl-Display
61/// [`RGB` codes]: rgb::HexRGB
62#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
63pub struct SHT<T: Clone + Integer + Unsigned> {
64 /// [`ChannelRatios`] value representing the relative strength of colour
65 /// components in the SHT.
66 channel_ratios: ChannelRatios<T>,
67 /// Overall brightness, measured as strength of strongest colour channel
68 /// relative to weakest.
69 ///
70 /// Has a default value of 1 if unspecified.
71 shade: Ratio<T>,
72 /// Lightness, equal to strength of weakest channel.
73 ///
74 /// Has a default value of 0 if unspecified.
75 tint: Ratio<T>,
76}
77
78/// Part of an [`SHT`] value, representing data about hues and relative strength
79/// of channels.
80///
81/// # Example
82/// ```
83/// use sht_colour::{ChannelRatios::ThreeBrightestChannels, Ratio, SHT};
84///
85/// let colour = "W".parse::<SHT<_>>().unwrap();
86///
87/// let channel_ratios = ThreeBrightestChannels;
88/// let colour_components = (
89/// channel_ratios,
90/// Ratio::from_integer(1_u8),
91/// Ratio::from_integer(1_u8),
92/// );
93///
94/// assert_eq!(colour.components(), colour_components);
95/// ```
96#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
97pub enum ChannelRatios<T: Clone + Integer + Unsigned> {
98 /// Represents colours where one channel (either [red], [blue] or [green])
99 /// is strictly brighter than the other two.
100 ///
101 /// [red]: ColourChannel::Red
102 /// [green]: ColourChannel::Green
103 /// [blue]: ColourChannel::Blue
104 OneBrightestChannel {
105 /// Stores whichever colour channel is brightest.
106 primary: ColourChannel,
107 /// If all three channels have different brightnesses, then this field
108 /// contains whichever channel is *second* brightest, as well as a
109 /// ratio: the brightness of the second brightest channel divided by the
110 /// brightness of the brightest channel. (Both the colour channel and
111 /// its relative strength stored in a tuple.)
112 ///
113 /// Otherwise, if channels other than the brightest channel are equal to
114 /// each other, this field is `None`.
115 direction_blend: Option<(ColourChannel, Ratio<T>)>,
116 },
117 /// Represents colours where two channels (from among [red], [blue] or
118 /// [green]) have the same brightness as each other and have strictly
119 /// greater brightness than the other channel.
120 ///
121 /// [red]: ColourChannel::Red
122 /// [green]: ColourChannel::Green
123 /// [blue]: ColourChannel::Blue
124 TwoBrightestChannels {
125 /// Holds the secondary colour (either [cyan], [yellow] or [magenta])
126 /// that represents whichever combination of two [primary colour
127 /// channels] are brightest.
128 ///
129 /// [primary colour channels]: ColourChannel
130 /// [cyan]: SecondaryColour::Cyan
131 /// [yellow]: SecondaryColour::Yellow
132 /// [magenta]: SecondaryColour::Magenta
133 secondary: SecondaryColour,
134 },
135 /// Represents colours where all three channels ([red], [blue] and [green])
136 /// have the exact same brightness as each other.
137 ///
138 /// [red]: ColourChannel::Red
139 /// [green]: ColourChannel::Green
140 /// [blue]: ColourChannel::Blue
141 ThreeBrightestChannels,
142}
143
144/// Represents a primary colour (using additive mixing).
145#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
146pub enum ColourChannel {
147 /// The colour red.
148 Red,
149 /// The colour green.
150 Green,
151 /// The colour blue.
152 Blue,
153}
154
155/// Represents a secondary colour (using additive mixing).
156#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
157pub enum SecondaryColour {
158 /// The colour cyan, made of green and blue.
159 Cyan,
160 /// The colour yellow, made of red and green.
161 Yellow,
162 /// The colour magenta, made of red and blue.
163 Magenta,
164}
165
166/// Represents possible errors parsing an [`SHT`] from a string.
167#[derive(Debug, PartialEq)]
168#[non_exhaustive]
169pub enum ParsePropertyError {
170 /// Parsed data, but failed to construct an [`SHT`] from it.
171 ValueErrors(Vec<SHTValueError>),
172 /// Could not parse data from the string.
173 ParseFailure(Error<String>),
174 /// Parsed data from the string, but with leftover unparsed characters.
175 InputRemaining(String),
176}
177
178impl From<Error<&str>> for ParsePropertyError {
179 fn from(value: Error<&str>) -> Self {
180 let Error { input, code } = value;
181 ParsePropertyError::ParseFailure(Error::new(input.to_owned(), code))
182 }
183}
184
185/// Represents possible errors when constructing an [`SHT`] from component
186/// values.
187#[derive(Debug, PartialEq, Eq)]
188#[non_exhaustive]
189pub enum SHTValueError {
190 /// `primary` set, while `shade` set
191 /// to 0.
192 PrimaryShadeZero,
193 /// `primary` set, while `tint` set to
194 /// 0.
195 PrimaryTintOne,
196 /// `secondary` set, while `shade` set
197 /// to 0.
198 SecondaryShadeZero,
199 /// `secondary` set, while `tint` set
200 /// to 1.
201 SecondaryTintOne,
202 /// `direction` is equal to `primary`.
203 DirectionEqualsPrimary,
204 /// A [ratio](num::rational::Ratio) is not in `0..1` range
205 /// (inclusive).
206 ValueOutOfBounds,
207 /// `blend` set to 0.
208 BlendZero,
209 /// `blend` set to 1.
210 BlendOne,
211}
212
213impl<T: Clone + Integer + Unsigned> SHT<T> {
214 /// Constructs an [`SHT`] value.
215 ///
216 /// # Arguments
217 ///
218 /// * `channel_ratios` - [`ChannelRatios`] value representing the relative
219 /// strength of colour components in the SHT.
220 /// * `shade` - Overall brightness, measured as strength of strongest colour
221 /// channel relative to weakest.
222 /// * `tint` - Lightness, equal to strength of weakest channel.
223 ///
224 /// # Example
225 /// ```
226 /// use sht_colour::{ChannelRatios::OneBrightestChannel, ColourChannel::Red, Ratio, SHT};
227 ///
228 /// let red_ratio = OneBrightestChannel {
229 /// primary: Red,
230 /// direction_blend: None,
231 /// };
232 /// let dark_red = <SHT<u8>>::new(red_ratio, Ratio::new(4, 12), Ratio::from_integer(0)).unwrap();
233 ///
234 /// assert_eq!(dark_red, "4r".parse().unwrap());
235 /// ```
236 ///
237 /// # Errors
238 /// Will return `Err` if the SHT components are incompatible or impossible.
239 pub fn new(
240 channel_ratios: ChannelRatios<T>,
241 shade: Ratio<T>,
242 tint: Ratio<T>,
243 ) -> Result<Self, Vec<SHTValueError>> {
244 let code = SHT {
245 channel_ratios,
246 shade,
247 tint,
248 };
249 match code.normal() {
250 Ok(code) => Ok(code),
251 Err(errs) => Err(errs),
252 }
253 }
254
255 /// Splits an [`SHT`] value into its struct fields.
256 ///
257 /// # Example
258 /// ```
259 /// use sht_colour::SHT;
260 ///
261 /// let colour = "7r5bE".parse::<SHT<u8>>().unwrap();
262 ///
263 /// let (channel_ratios, shade, tint) = colour.clone().components();
264 /// let new_colour = <SHT<_>>::new(channel_ratios, shade, tint).unwrap();
265 ///
266 /// assert_eq!(colour, new_colour);
267 /// ```
268 pub fn components(self) -> (ChannelRatios<T>, Ratio<T>, Ratio<T>) {
269 let Self {
270 channel_ratios,
271 shade,
272 tint,
273 } = self;
274 (channel_ratios, shade, tint)
275 }
276
277 /// Check whether an [`SHT`] is valid according to the criteria on
278 /// <https://omaitzen.com/sht/spec/>. An `SHT` colour should have a unique
279 /// canonical form under those conditions.
280 ///
281 /// # Errors
282 /// Will return `Err` if the `SHT` is not valid. The `Err` contains a vector
283 /// of all detected inconsistencies in no particular order.
284 fn normal(self) -> Result<Self, Vec<SHTValueError>> {
285 let Self {
286 channel_ratios,
287 shade,
288 tint,
289 } = self;
290 // validate fields:
291 let mut errors = Vec::with_capacity(16); // more than strictly needed
292 match channel_ratios.clone() {
293 ChannelRatios::OneBrightestChannel {
294 primary,
295 direction_blend,
296 } => {
297 // colour has one brightest channel
298 if shade.is_zero() {
299 errors.push(SHTValueError::PrimaryShadeZero);
300 }
301 if tint.is_one() {
302 errors.push(SHTValueError::PrimaryTintOne);
303 }
304 if let Some((direction, blend)) = direction_blend {
305 // colour has a second-brightest channel
306 if direction == primary {
307 errors.push(SHTValueError::DirectionEqualsPrimary);
308 }
309 if blend.is_zero() {
310 errors.push(SHTValueError::BlendZero);
311 }
312 if blend.is_one() {
313 errors.push(SHTValueError::BlendOne);
314 }
315 if blend > Ratio::one() {
316 errors.push(SHTValueError::ValueOutOfBounds);
317 }
318 }
319 }
320 ChannelRatios::TwoBrightestChannels { .. } => {
321 // colour has two brightest channels
322 if shade.is_zero() {
323 errors.push(SHTValueError::SecondaryShadeZero);
324 }
325 if tint.is_one() {
326 errors.push(SHTValueError::SecondaryTintOne);
327 }
328 }
329 ChannelRatios::ThreeBrightestChannels => {}
330 }
331 if tint > Ratio::one() {
332 errors.push(SHTValueError::ValueOutOfBounds);
333 }
334 if shade > Ratio::one() {
335 errors.push(SHTValueError::ValueOutOfBounds);
336 }
337 if errors.is_empty() {
338 Ok(Self {
339 channel_ratios,
340 shade,
341 tint,
342 })
343 } else {
344 Err(errors)
345 }
346 }
347
348 /// Convert a colour from [`SHT`] format to [`HexRGB`].
349 ///
350 /// # Arguments
351 /// * `precision` - How many hex digits to round the result of conversion
352 /// to.
353 ///
354 /// # Example
355 /// ```
356 /// use sht_colour::{rgb::HexRGB, sht::SHT};
357 ///
358 /// let red_rgb = "#F00".parse::<HexRGB<u32>>().unwrap();
359 /// let red_sht = "r".parse::<SHT<u32>>().unwrap();
360 ///
361 /// assert_eq!(red_sht.to_rgb(1), red_rgb);
362 /// ```
363 ///
364 /// [`HexRGB`]: rgb::HexRGB
365 pub fn to_rgb(self, precision: usize) -> rgb::HexRGB<T>
366 where
367 T: Integer + Unsigned + From<u8> + Clone + CheckedMul,
368 {
369 // Round hexadecimal number to precision
370 let round =
371 |ratio: Ratio<T>| round_denominator::<T>(ratio, 16.into(), precision, <_>::one());
372
373 let (channel_ratios, shade, tint) = self.components();
374 let (max, min) = (
375 tint.clone() + shade * (<Ratio<_>>::one() - tint.clone()),
376 tint,
377 );
378
379 let (red, green, blue) = match channel_ratios {
380 ChannelRatios::ThreeBrightestChannels => (min.clone(), min.clone(), min),
381 ChannelRatios::TwoBrightestChannels { secondary } => match secondary {
382 SecondaryColour::Cyan => (min, max.clone(), max),
383 SecondaryColour::Yellow => (max.clone(), max, min),
384 SecondaryColour::Magenta => (max.clone(), min, max),
385 },
386 ChannelRatios::OneBrightestChannel {
387 primary,
388 direction_blend,
389 } => {
390 let (mut red, mut green, mut blue) = (min.clone(), min.clone(), min.clone());
391 if let Some((direction, blend)) = direction_blend {
392 let centremost_channel = min.clone() + blend * (max.clone() - min);
393 match direction {
394 ColourChannel::Red => red = centremost_channel,
395 ColourChannel::Green => green = centremost_channel,
396 ColourChannel::Blue => blue = centremost_channel,
397 }
398 };
399 match primary {
400 ColourChannel::Red => red = max,
401 ColourChannel::Green => green = max,
402 ColourChannel::Blue => blue = max,
403 };
404 (red, green, blue)
405 }
406 };
407 rgb::HexRGB::new(round(red), round(green), round(blue))
408 }
409}
410
411/// Parses an [`SHT`] from a string.
412///
413/// See the [`Display` implementation] for the format.
414///
415/// # Example
416/// ```
417/// use sht_colour::SHT;
418///
419/// let first_colour = "5r600000".parse::<SHT<u8>>().unwrap();
420/// let second_colour = "500r6".parse::<SHT<u8>>().unwrap();
421///
422/// assert_eq!(first_colour, second_colour);
423/// ```
424///
425/// [`Display` implementation]: SHT#impl-Display
426impl<T> FromStr for SHT<T>
427where
428 T: Clone + Integer + Unsigned + FromStr + CheckedMul + CheckedAdd + CheckedDiv,
429 u8: Into<T>,
430{
431 type Err = ParsePropertyError;
432
433 fn from_str(s: &str) -> Result<Self, Self::Err> {
434 parse_sht(s)
435 }
436}
437
438/// Possibly rounds a base 12 number.
439///
440/// If `round_up`, adds 1 to the number.
441/// Othewise, leaves number unchanged.
442/// Number is a slice of u8 digits.
443///
444/// # Example
445/// ```ignore
446/// let arr = [1, 5, 11, 11, 11, 11];
447///
448/// assert_eq!(round(&arr, false), arr);
449/// assert_eq!(round(&arr, true), vec![1, 6]);
450/// ```
451fn round(input: &[u8], round_up: bool) -> Vec<u8> {
452 if round_up {
453 if let Some((&last, rest)) = input.split_last() {
454 let rounded_last = last.checked_add(1).unwrap_or(12);
455 if rounded_last >= 12 {
456 round(rest, round_up)
457 } else {
458 let mut mut_rest = rest.to_vec();
459 mut_rest.push(rounded_last);
460 mut_rest
461 }
462 } else {
463 vec![12]
464 }
465 } else {
466 input.to_vec()
467 }
468}
469
470/// Converts a ratio to a fixed-point base-12 string.
471///
472/// Output uses 'X' to represent decimal 10, and 'E' to represent decimal digit
473/// 11. The output does not use '.' and does not support negative numbers.
474///
475/// # Example
476/// ```ignore
477/// use sht_colour::Ratio;
478///
479/// assert_eq!(duodecimal(Ratio::new(11310, 20736), 2), "67");
480/// ```
481fn duodecimal<T>(mut input: Ratio<T>, precision: usize) -> String
482where
483 T: TryInto<usize> + Integer + Zero + Rem<T, Output = T> + Div<T, Output = T> + Clone,
484 u8: Into<T>,
485{
486 let half = || Ratio::new(1.into(), 2.into());
487 let digit_characters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'X', 'E'];
488 let mut digits = Vec::with_capacity(precision);
489 if input >= <_>::one() {
490 return "W".to_owned();
491 }
492 let mut round_up = false;
493 for digits_left in (0..precision).rev() {
494 let scaled = input * Ratio::from_integer(12.into());
495 input = scaled.fract();
496 if digits_left.is_zero() {
497 // round because no more digits
498 // comparing remainder to 0.5
499 round_up = input >= half();
500 }
501 let integer_part = scaled.to_integer();
502 let next_digit = match integer_part.try_into() {
503 Ok(n) if n < 12 => n
504 .try_into()
505 .expect("usize < 12 could not be converted to u8"),
506 _ => 12_u8,
507 };
508 digits.push(next_digit);
509 if input.is_zero() {
510 break;
511 }
512 }
513 // possibly round up, then convert &[u8] to digit String
514 round(&digits, round_up)
515 .iter()
516 .map(|&c| digit_characters.get(usize::from(c)).unwrap_or(&'W'))
517 .collect()
518}
519
520/// Formats the colour per the [`SHT`] format on <https://omaitzen.com/sht/spec/>:
521///
522/// Supports an optional `precision` parameter, which determines the maximum
523/// number of digits.
524///
525/// # Format
526///
527/// > ```text
528/// > [<shade>] [<primary> [<blend> <direction>] | <secondary>] [<tint>]
529/// > ```
530///
531/// Here `<shade>`, `<blend>` and `<tint>` are numbers between 0 and 1
532/// inclusive, `<primary>` and `<direction>` are primary colours, and
533/// `<secondary>` is a secondary colour.
534///
535/// Numbers are represented using one or more base-12 digits (where `'X'` and
536/// `'E'` are 10 and 11 respectively). Tint is represented by `'W'` if it is
537/// equal to 12/12, i.e. the colour is pure white.
538///
539/// Primary colours are `'r'`, `'g'` or `'b'`, representing red, blue and green
540/// respectively.
541///
542/// Secondary colours are `'c'`, `'y'` or `'m'`, representing cyan, yellow and
543/// magenta respectively.
544///
545/// # Example
546/// ```
547/// use sht_colour::SHT;
548///
549/// let colour = "8r6g3".parse::<SHT<u8>>().unwrap();
550///
551/// assert_eq!(format!("{}", colour), "8r6g3");
552/// ```
553impl<T> Display for SHT<T>
554where
555 T: TryInto<usize> + Unsigned + Integer + Clone + Display + One,
556 u8: Into<T>,
557{
558 fn fmt(&self, formatter: &mut Formatter) -> FMTResult {
559 let precision = formatter.precision().unwrap_or(2);
560
561 let ratio_to_str = |ratio: Ratio<T>| duodecimal(ratio, precision);
562 let primary_to_str = |primary| match primary {
563 ColourChannel::Red => "r".to_owned(),
564 ColourChannel::Green => "g".to_owned(),
565 ColourChannel::Blue => "b".to_owned(),
566 };
567 let secondary_to_str = |secondary| match secondary {
568 SecondaryColour::Cyan => "c".to_owned(),
569 SecondaryColour::Yellow => "y".to_owned(),
570 SecondaryColour::Magenta => "m".to_owned(),
571 };
572
573 let (channel_ratios, shade_ratio, tint_ratio) = self.clone().components();
574 let tint = (!tint_ratio.is_zero()).then(|| tint_ratio);
575 let shade = (!shade_ratio.is_one()).then(|| shade_ratio);
576 let (primary, secondary, direction, blend) = match channel_ratios {
577 ChannelRatios::OneBrightestChannel {
578 primary,
579 direction_blend,
580 } => {
581 if let Some((direction, blend)) = direction_blend {
582 (Some(primary), None, Some(direction), Some(blend))
583 } else {
584 (Some(primary), None, None, None)
585 }
586 }
587 ChannelRatios::TwoBrightestChannels { secondary } => {
588 (None, Some(secondary), None, None)
589 }
590 ChannelRatios::ThreeBrightestChannels => (None, None, None, None),
591 };
592 write!(
593 formatter,
594 "{}{}{}{}{}{}",
595 shade.map_or_else(String::new, ratio_to_str),
596 primary.map_or_else(String::new, primary_to_str),
597 blend.map_or_else(String::new, ratio_to_str),
598 direction.map_or_else(String::new, primary_to_str),
599 secondary.map_or_else(String::new, secondary_to_str),
600 tint.map_or_else(String::new, ratio_to_str)
601 )
602 }
603}
604
605impl<T> Default for SHT<T>
606where
607 T: Clone + Integer + Unsigned + One + Zero,
608{
609 fn default() -> Self {
610 SHT {
611 channel_ratios: ChannelRatios::default(),
612 shade: Ratio::one(),
613 tint: Ratio::zero(),
614 }
615 }
616}
617
618impl<T> Default for ChannelRatios<T>
619where
620 T: Clone + Integer + Unsigned + One + Zero,
621{
622 fn default() -> Self {
623 ChannelRatios::OneBrightestChannel {
624 primary: ColourChannel::default(),
625 direction_blend: None,
626 }
627 }
628}
629
630impl Default for ColourChannel {
631 fn default() -> Self {
632 ColourChannel::Red
633 }
634}
635
636impl Default for SecondaryColour {
637 fn default() -> Self {
638 SecondaryColour::Cyan
639 }
640}
641
642#[cfg(test)]
643mod tests;
644
645/// Contains functions for parsing [`SHT`] values and their components from
646/// strings.
647mod parser;