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;