gamma_table_macros/
lib.rs

1//! # Gamma Table Macros
2//!
3//! This crate provides a procedural macro for generating gamma lookup tables.
4//! The generated table can be used for fast brightness/gamma correction in embedded, graphics, or image processing applications.
5//!
6//! # Examples
7//!
8//! Basic gamma encoding table:
9//! ```
10//! use gamma_table_macros::gamma_table;
11//!
12//! gamma_table! {
13//!     name: GAMMA_TABLE_22,
14//!     entry_type: u8,
15//!     gamma: 2.2,
16//!     size: 256
17//! }
18//! ```
19#![warn(missing_docs)]
20#![warn(clippy::all)]
21#![warn(clippy::pedantic)]
22extern crate proc_macro;
23
24use proc_macro2::TokenStream;
25use quote::quote;
26use syn::{parse_macro_input, Error, LitBool, LitFloat, LitInt};
27
28/// Generates a gamma lookup table as a procedural macro.
29///
30/// This macro generates a `const` array with gamma-encoded or gamma-corrected values at compile time.
31/// The generated table can be used for fast brightness/gamma correction in embedded, graphics, or image processing applications.
32///
33/// # Parameters
34/// - `name`: `IDENT`\
35///   The name of the generated constant table (e.g., `GAMMA_TABLE_22`).
36/// - `entry_type`: `Type`\
37///   The unsigned integer type for table entries (`u8`, `u16`, `u32`, or `u64`).
38/// - `gamma`: `float`\
39///   The gamma value to use for encoding or decoding. Must be positive.
40/// - `size`: `integer`\
41///   The number of entries in the table. Must be at least 3.
42/// - `max_value`: `integer` (optional, default `size-1`)\
43///   The maximum output value for the table.
44///   Useful for brightness limiting or matching hardware constraints.
45/// - `decoding`: `bool` (optional, default false)\
46///   If `true`, generates a gamma correction (decoding) table using `input^(1/gamma)`.\
47///   If `false` or omitted, generates a gamma encoding table using `input^gamma`.
48///
49/// # Gamma Processing
50/// - **Gamma Encoding (default):**\
51///   `output = (input / max_input) ^ gamma * max_value`\
52///   Makes mid-tones darker, suitable for preparing data for display.
53/// - **Gamma Decoding:**\
54///   `output = (input / max_input) ^ (1/gamma) * max_value`\
55///   Makes mid-tones brighter, suitable for correcting gamma-encoded data.
56///
57/// # Output
58/// Generates a `const` array named as specified by `name`, with type `[entry_type; size]`.
59///
60/// # Errors
61/// - Fails to compile if required parameters are missing or have invalid types.
62/// - Fails if `gamma` is not positive.
63/// - Fails if `size` is less than 3.
64/// - Fails if `max_value` exceeds the maximum for the chosen `entry_type`.
65///
66/// # Examples
67/// Basic gamma encoding table:
68/// ```
69/// use gamma_table_macros::gamma_table;
70///
71/// gamma_table! {
72///     name: GAMMA_TABLE_22,
73///     entry_type: u8,
74///     gamma: 2.2,
75///     size: 256
76/// }
77/// ```
78///
79/// Gamma correction (decoding) table with brightness limiting:
80/// ```
81/// use gamma_table_macros::gamma_table;
82///
83/// gamma_table! {
84///     name: GAMMA_CORRECTED,
85///     entry_type: u16,
86///     gamma: 2.4,
87///     size: 1024,
88///     max_value: 1000,
89///     decoding: true
90/// }
91/// ```
92///
93/// # Usage
94/// The generated table can be used as a `const` array in your code:
95/// ```ignore
96/// // After macro expansion:
97/// pub const GAMMA_TABLE_22: [u8; 256] = [0, 0, 0, 0, 0, /* ... 251 more values */ ];
98/// ```
99#[proc_macro]
100pub fn gamma_table(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
101    let input = parse_macro_input!(input as GammaTableInput);
102
103    match generate_gamma_table(&input) {
104        Ok(tokens) => tokens.into(),
105        Err(err) => err.to_compile_error().into(),
106    }
107}
108
109struct GammaTableInput {
110    name: syn::Ident,
111    entry_type: syn::Type,
112    gamma: f64,
113    size: usize,
114    max_value: Option<u64>,
115    decoding: Option<bool>,
116}
117
118impl syn::parse::Parse for GammaTableInput {
119    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
120        let mut name = None;
121        let mut entry_type = None;
122        let mut gamma = None;
123        let mut size = None;
124        let mut max_value = None;
125        let mut decoding = None;
126
127        while !input.is_empty() {
128            let ident: syn::Ident = input.parse()?;
129            input.parse::<syn::Token![:]>()?;
130
131            match ident.to_string().as_str() {
132                "name" => {
133                    let value: syn::Ident = input.parse()?;
134                    name = Some(value);
135                }
136                "entry_type" => {
137                    let value: syn::Type = input.parse()?;
138                    entry_type = Some(value);
139                }
140                "gamma" => {
141                    let value: LitFloat = input.parse()?;
142                    gamma = Some(value.base10_parse()?);
143                }
144                "size" => {
145                    let value: LitInt = input.parse()?;
146                    size = Some(value.base10_parse()?);
147                }
148                "max_value" => {
149                    let value: LitInt = input.parse()?;
150                    max_value = Some(value.base10_parse()?);
151                }
152                "decoding" => {
153                    let value: LitBool = input.parse()?;
154                    decoding = Some(value.value);
155                }
156                _ => {
157                    return Err(Error::new(
158                        ident.span(),
159                        format!("Unknown parameter: {ident}"),
160                    ))
161                }
162            }
163
164            if input.peek(syn::Token![,]) {
165                input.parse::<syn::Token![,]>()?;
166            }
167        }
168
169        Ok(GammaTableInput {
170            name: name
171                .ok_or_else(|| Error::new(input.span(), "Missing required parameter: name"))?,
172            entry_type: entry_type.ok_or_else(|| {
173                Error::new(input.span(), "Missing required parameter: entry_type")
174            })?,
175            gamma: gamma
176                .ok_or_else(|| Error::new(input.span(), "Missing required parameter: gamma"))?,
177            size: size
178                .ok_or_else(|| Error::new(input.span(), "Missing required parameter: size"))?,
179            max_value,
180            decoding,
181        })
182    }
183}
184
185fn get_integer_type_max_value(entry_type: &syn::Type) -> Option<u64> {
186    // Extract the type name from syn::Type
187    if let syn::Type::Path(type_path) = entry_type {
188        if let Some(segment) = type_path.path.segments.last() {
189            match segment.ident.to_string().as_str() {
190                "u8" => Some(u64::from(u8::MAX)),
191                "u16" => Some(u64::from(u16::MAX)),
192                "u32" => Some(u64::from(u32::MAX)),
193                "u64" => Some(u64::MAX),
194                _ => None, // Unknown or unsupported type
195            }
196        } else {
197            None
198        }
199    } else {
200        None
201    }
202}
203
204fn generate_gamma_table(input: &GammaTableInput) -> syn::Result<TokenStream> {
205    let name = &input.name;
206    let entry_type = &input.entry_type;
207    let gamma = input.gamma;
208    let size = input.size;
209    let max_value = input.max_value.unwrap_or((size - 1) as u64);
210    let decoding = input.decoding.unwrap_or(false);
211
212    // Validate input parameters
213    if gamma <= 0.0 {
214        return Err(Error::new(name.span(), "Gamma value must be positive"));
215    }
216    if size < 3 {
217        return Err(Error::new(
218            name.span(),
219            "Size must be at least 3 to create a meaningful gamma table. Smaller sizes only have min and max values.",
220        ));
221    }
222
223    // Validate that max_value fits in the target integer type
224    if let Some(type_max) = get_integer_type_max_value(entry_type) {
225        if max_value > type_max {
226            return Err(Error::new(
227                name.span(),
228                format!(
229                    "max_value ({}) exceeds the maximum value ({}) that can be stored in entry_type {}",
230                    max_value,
231                    type_max,
232                    quote!(#entry_type)
233                ),
234            ));
235        }
236    } else {
237        return Err(Error::new(
238            name.span(),
239            format!(
240                "Unsupported entry_type: {}. Supported types are: u8, u16, u32, u64",
241                quote!(#entry_type)
242            ),
243        ));
244    }
245
246    // Generate the lookup table values
247    let values = generate_table_values(size, gamma, max_value, decoding);
248
249    // Convert values to tokens with proper casting
250    let value_tokens: Vec<TokenStream> = values
251        .iter()
252        .map(|&v| quote! { #v as #entry_type })
253        .collect();
254
255    Ok(quote! {
256        const #name: [#entry_type; #size] = [#(#value_tokens),*];
257    })
258}
259
260fn generate_table_values(size: usize, gamma: f64, max_value: u64, decoding: bool) -> Vec<u64> {
261    let mut values = Vec::with_capacity(size);
262
263    // Choose gamma exponent based on mode
264    let gamma_exponent = if decoding {
265        1.0 / gamma // Gamma correction/decoding: input^(1/gamma)
266    } else {
267        gamma // Gamma encoding (default): input^gamma
268    };
269
270    // Direct gamma processing for each entry
271    for i in 0..size {
272        #[allow(clippy::cast_precision_loss)]
273        let normalized_input = i as f64 / (size - 1) as f64;
274        let processed = normalized_input.powf(gamma_exponent);
275        // we know the the sign is positive, and the result values will fit in a u64, and we are rounding
276        #[allow(
277            clippy::cast_precision_loss,
278            clippy::cast_possible_truncation,
279            clippy::cast_sign_loss
280        )]
281        let output_value = (processed * max_value as f64).round() as u64;
282        values.push(output_value.min(max_value));
283    }
284
285    values
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn test_gamma_encoding_default() {
294        // Test gamma encoding (default behavior)
295        let values = generate_table_values(256, 2.2, 255, false);
296        assert_eq!(values.len(), 256);
297        assert_eq!(values[0], 0);
298        assert_eq!(values[255], 255);
299
300        // Values should be monotonically increasing
301        for i in 1..values.len() {
302            assert!(values[i] >= values[i - 1]);
303        }
304    }
305
306    #[test]
307    fn test_gamma_decoding() {
308        // Test gamma correction/decoding
309        let values = generate_table_values(256, 2.2, 255, true);
310        assert_eq!(values.len(), 256);
311        assert_eq!(values[0], 0);
312        assert_eq!(values[255], 255);
313
314        // Values should be monotonically increasing
315        for i in 1..values.len() {
316            assert!(values[i] >= values[i - 1]);
317        }
318    }
319
320    #[test]
321    fn test_encoding_vs_decoding_difference() {
322        let encoding_values = generate_table_values(10, 2.2, 100, false);
323        let decoding_values = generate_table_values(10, 2.2, 100, true);
324
325        // Encoding and decoding should produce different results for mid-values
326        assert_ne!(encoding_values[5], decoding_values[5]);
327
328        // But endpoints should be the same
329        assert_eq!(encoding_values[0], decoding_values[0]); // Both 0
330        assert_eq!(encoding_values[9], decoding_values[9]); // Both 100
331    }
332
333    #[test]
334    fn test_default_max_value() {
335        // Test that max_value defaults to size-1
336        let values = generate_table_values(10, 1.0, 9, false);
337        assert_eq!(values[0], 0);
338        assert_eq!(values[9], 9); // size-1
339    }
340
341    #[test]
342    fn test_minimum_size_validation() {
343        // Test that size must be at least 3
344        let input = GammaTableInput {
345            name: syn::parse_str("TEST_TABLE").unwrap(),
346            entry_type: syn::parse_str("u8").unwrap(),
347            gamma: 2.2,
348            size: 2,
349            max_value: None,
350            decoding: None,
351        };
352
353        let result = generate_gamma_table(&input);
354        assert!(result.is_err());
355        assert!(result
356            .unwrap_err()
357            .to_string()
358            .contains("Size must be at least 3"));
359
360        // Test that size 3 works
361        let input = GammaTableInput {
362            name: syn::parse_str("TEST_TABLE").unwrap(),
363            entry_type: syn::parse_str("u8").unwrap(),
364            gamma: 2.2,
365            size: 3,
366            max_value: None,
367            decoding: None,
368        };
369
370        let result = generate_gamma_table(&input);
371        assert!(result.is_ok());
372    }
373
374    #[test]
375    fn test_negative_gamma_validation() {
376        // Test that gamma must be positive
377        let input = GammaTableInput {
378            name: syn::parse_str("TEST_TABLE").unwrap(),
379            entry_type: syn::parse_str("u8").unwrap(),
380            gamma: -1.0,
381            size: 10,
382            max_value: None,
383            decoding: None,
384        };
385
386        let result = generate_gamma_table(&input);
387        assert!(result.is_err());
388        assert!(result
389            .unwrap_err()
390            .to_string()
391            .contains("Gamma value must be positive"));
392
393        // Test that gamma = 0 is also invalid
394        let input = GammaTableInput {
395            name: syn::parse_str("TEST_TABLE").unwrap(),
396            entry_type: syn::parse_str("u8").unwrap(),
397            gamma: 0.0,
398            size: 10,
399            max_value: None,
400            decoding: None,
401        };
402
403        let result = generate_gamma_table(&input);
404        assert!(result.is_err());
405        assert!(result
406            .unwrap_err()
407            .to_string()
408            .contains("Gamma value must be positive"));
409    }
410
411    #[test]
412    fn test_parsing_unknown_parameter() {
413        // Test unknown parameter error
414        let tokens: proc_macro2::TokenStream = quote! {
415            name: TEST_TABLE,
416            entry_type: u8,
417            gamma: 2.2,
418            size: 10,
419            unknown_param: 42
420        };
421
422        let result = syn::parse2::<GammaTableInput>(tokens);
423        assert!(result.is_err());
424    }
425
426    #[test]
427    fn test_parsing_missing_required_parameters() {
428        // Test missing name
429        let tokens: proc_macro2::TokenStream = quote! {
430            entry_type: u8,
431            gamma: 2.2,
432            size: 10
433        };
434        let result = syn::parse2::<GammaTableInput>(tokens);
435        assert!(result.is_err());
436
437        // Test missing entry_type
438        let tokens: proc_macro2::TokenStream = quote! {
439            name: TEST_TABLE,
440            gamma: 2.2,
441            size: 10
442        };
443        let result = syn::parse2::<GammaTableInput>(tokens);
444        assert!(result.is_err());
445
446        // Test missing gamma
447        let tokens: proc_macro2::TokenStream = quote! {
448            name: TEST_TABLE,
449            entry_type: u8,
450            size: 10
451        };
452        let result = syn::parse2::<GammaTableInput>(tokens);
453        assert!(result.is_err());
454
455        // Test missing size
456        let tokens: proc_macro2::TokenStream = quote! {
457            name: TEST_TABLE,
458            entry_type: u8,
459            gamma: 2.2
460        };
461        let result = syn::parse2::<GammaTableInput>(tokens);
462        assert!(result.is_err());
463    }
464
465    #[test]
466    fn test_parsing_invalid_parameter_types() {
467        // Test sending wrong types for each parameter
468
469        // name expects IDENT, sending int
470        let tokens: proc_macro2::TokenStream = quote! {
471            name: 123,
472            entry_type: u8,
473            gamma: 2.2,
474            size: 10
475        };
476        let result = syn::parse2::<GammaTableInput>(tokens);
477        assert!(result.is_err());
478
479        // entry_type expects Type, sending string
480        let tokens: proc_macro2::TokenStream = quote! {
481            name: TEST_TABLE,
482            entry_type: "u8",
483            gamma: 2.2,
484            size: 10
485        };
486        let result = syn::parse2::<GammaTableInput>(tokens);
487        assert!(result.is_err());
488
489        // gamma expects LitFloat, sending string
490        let tokens: proc_macro2::TokenStream = quote! {
491            name: TEST_TABLE,
492            entry_type: u8,
493            gamma: "2.2",
494            size: 10
495        };
496        let result = syn::parse2::<GammaTableInput>(tokens);
497        assert!(result.is_err());
498
499        // size expects LitInt, sending float
500        let tokens: proc_macro2::TokenStream = quote! {
501            name: TEST_TABLE,
502            entry_type: u8,
503            gamma: 2.2,
504            size: 10.5
505        };
506        let result = syn::parse2::<GammaTableInput>(tokens);
507        assert!(result.is_err());
508
509        // size expects LitInt, sending string
510        let tokens: proc_macro2::TokenStream = quote! {
511            name: TEST_TABLE,
512            entry_type: u8,
513            gamma: 2.2,
514            size: "10"
515        };
516        let result = syn::parse2::<GammaTableInput>(tokens);
517        assert!(result.is_err());
518
519        // max_value expects LitInt, sending float
520        let tokens: proc_macro2::TokenStream = quote! {
521            name: TEST_TABLE,
522            entry_type: u8,
523            gamma: 2.2,
524            size: 10,
525            max_value: 255.5
526        };
527        let result = syn::parse2::<GammaTableInput>(tokens);
528        assert!(result.is_err());
529
530        // max_value expects LitInt, sending string
531        let tokens: proc_macro2::TokenStream = quote! {
532            name: TEST_TABLE,
533            entry_type: u8,
534            gamma: 2.2,
535            size: 10,
536            max_value: "255"
537        };
538        let result = syn::parse2::<GammaTableInput>(tokens);
539        assert!(result.is_err());
540
541        // decoding expects LitBool, sending float
542        let tokens: proc_macro2::TokenStream = quote! {
543            name: TEST_TABLE,
544            entry_type: u8,
545            gamma: 2.2,
546            size: 10,
547            decoding: 1.0
548        };
549        let result = syn::parse2::<GammaTableInput>(tokens);
550        assert!(result.is_err());
551
552        // decoding expects LitBool, sending string
553        let tokens: proc_macro2::TokenStream = quote! {
554            name: TEST_TABLE,
555            entry_type: u8,
556            gamma: 2.2,
557            size: 10,
558            decoding: "true"
559        };
560        let result = syn::parse2::<GammaTableInput>(tokens);
561        assert!(result.is_err());
562    }
563
564    #[test]
565    fn test_max_value_overflow_validation() {
566        // Test u8 overflow
567        let input = GammaTableInput {
568            name: syn::parse_str("TEST_TABLE").unwrap(),
569            entry_type: syn::parse_str("u8").unwrap(),
570            gamma: 2.2,
571            size: 10,
572            max_value: Some(300), // Exceeds u8::MAX (255)
573            decoding: None,
574        };
575        let result = generate_gamma_table(&input);
576        assert!(result.is_err());
577        assert!(result
578            .unwrap_err()
579            .to_string()
580            .contains("max_value (300) exceeds the maximum value (255)"));
581
582        // Test u16 overflow
583        let input = GammaTableInput {
584            name: syn::parse_str("TEST_TABLE").unwrap(),
585            entry_type: syn::parse_str("u16").unwrap(),
586            gamma: 2.2,
587            size: 10,
588            max_value: Some(70000), // Exceeds u16::MAX (65535)
589            decoding: None,
590        };
591        let result = generate_gamma_table(&input);
592        assert!(result.is_err());
593        assert!(result
594            .unwrap_err()
595            .to_string()
596            .contains("max_value (70000) exceeds the maximum value (65535)"));
597
598        // Test u32 overflow
599        let input = GammaTableInput {
600            name: syn::parse_str("TEST_TABLE").unwrap(),
601            entry_type: syn::parse_str("u32").unwrap(),
602            gamma: 2.2,
603            size: 10,
604            max_value: Some(5000000000), // Exceeds u32::MAX (4294967295)
605            decoding: None,
606        };
607        let result = generate_gamma_table(&input);
608        assert!(result.is_err());
609        assert!(result
610            .unwrap_err()
611            .to_string()
612            .contains("max_value (5000000000) exceeds the maximum value (4294967295)"));
613
614        // Test valid max_value for u8
615        let input = GammaTableInput {
616            name: syn::parse_str("TEST_TABLE").unwrap(),
617            entry_type: syn::parse_str("u8").unwrap(),
618            gamma: 2.2,
619            size: 10,
620            max_value: Some(255), // Valid for u8
621            decoding: None,
622        };
623        let result = generate_gamma_table(&input);
624        assert!(result.is_ok());
625
626        // Test valid max_value for u32
627        let input = GammaTableInput {
628            name: syn::parse_str("TEST_TABLE").unwrap(),
629            entry_type: syn::parse_str("u32").unwrap(),
630            gamma: 2.2,
631            size: 10,
632            max_value: Some(1000000), // Valid for u32
633            decoding: None,
634        };
635        let result = generate_gamma_table(&input);
636        assert!(result.is_ok());
637
638        // Test valid max_value for u64
639        let input = GammaTableInput {
640            name: syn::parse_str("TEST_TABLE").unwrap(),
641            entry_type: syn::parse_str("u64").unwrap(),
642            gamma: 2.2,
643            size: 10,
644            max_value: Some(1000000), // Valid for u64
645            decoding: None,
646        };
647        let result = generate_gamma_table(&input);
648        assert!(result.is_ok());
649
650        // Test unsupported entry type
651        let input = GammaTableInput {
652            name: syn::parse_str("TEST_TABLE").unwrap(),
653            entry_type: syn::parse_str("i32").unwrap(), // Unsupported type
654            gamma: 2.2,
655            size: 10,
656            max_value: Some(100),
657            decoding: None,
658        };
659        let result = generate_gamma_table(&input);
660        assert!(result.is_err());
661        assert!(result
662            .unwrap_err()
663            .to_string()
664            .contains("Unsupported entry_type"));
665
666        // Test another unsupported entry type
667        let input = GammaTableInput {
668            name: syn::parse_str("TEST_TABLE").unwrap(),
669            entry_type: syn::parse_str("f32").unwrap(), // Unsupported type
670            gamma: 2.2,
671            size: 10,
672            max_value: Some(100),
673            decoding: None,
674        };
675        let result = generate_gamma_table(&input);
676        assert!(result.is_err());
677        assert!(result
678            .unwrap_err()
679            .to_string()
680            .contains("Unsupported entry_type"));
681    }
682}