image_charts/lib.rs
1#![allow(non_snake_case, dead_code)]
2
3//! Official Image-Charts.com API client library
4//!
5//! Official [Image Charts](https://image-charts.com/) API client.
6//! Generate URLs of static image charts.
7//! Embed them everywhere in emails, pdf reports, chat bots...!
8//!
9//! # Features
10//!
11//! - `async` (default): Async API using tokio and reqwest
12//! - `blocking`: Blocking/synchronous API using reqwest blocking
13//! - `full`: Both async and blocking APIs
14//!
15//! # Example
16//!
17//! ```rust
18//! use image_charts::ImageCharts;
19//!
20//! let url = ImageCharts::new()
21//! .cht("p")
22//! .chd("t:60,40")
23//! .chs("100x100")
24//! .to_url();
25//!
26//! println!("{}", url);
27//! ```
28
29use std::collections::HashMap;
30use std::time::Duration;
31use thiserror::Error;
32
33/// Error type for ImageCharts operations
34#[derive(Error, Debug)]
35#[error("{message}")]
36pub struct ImageChartsError {
37 /// Error message
38 pub message: String,
39 /// Error code from Image-Charts API
40 pub code: Option<String>,
41 /// HTTP status code
42 pub status_code: Option<u16>,
43}
44
45impl ImageChartsError {
46 fn new(message: impl Into<String>) -> Self {
47 Self {
48 message: message.into(),
49 code: None,
50 status_code: None,
51 }
52 }
53
54 fn with_code(mut self, code: impl Into<String>) -> Self {
55 self.code = Some(code.into());
56 self
57 }
58
59 fn with_status(mut self, status: u16) -> Self {
60 self.status_code = Some(status);
61 self
62 }
63}
64
65#[derive(Debug, Clone, serde::Deserialize)]
66struct ValidationError {
67 message: String,
68}
69
70/// Configuration for ImageCharts client
71#[derive(Debug, Clone)]
72pub struct ImageChartsConfig {
73 /// Protocol (http or https)
74 pub protocol: String,
75 /// API host
76 pub host: String,
77 /// API port
78 pub port: u16,
79 /// API pathname
80 pub pathname: String,
81 /// Request timeout
82 pub timeout: Duration,
83 /// Enterprise secret key for signing
84 pub secret: Option<String>,
85 /// Custom user-agent string
86 pub user_agent: Option<String>,
87}
88
89impl Default for ImageChartsConfig {
90 fn default() -> Self {
91 Self {
92 protocol: "https".to_string(),
93 host: "image-charts.com".to_string(),
94 port: 443,
95 pathname: "/chart".to_string(),
96 timeout: Duration::from_millis(5000),
97 secret: None,
98 user_agent: None,
99 }
100 }
101}
102
103/// Builder for ImageCharts API requests
104///
105/// Use the fluent API to configure chart parameters, then call one of the
106/// output methods (`to_url`, `to_buffer`, `to_file`, `to_data_uri`) to
107/// generate the chart.
108///
109/// # Example
110///
111/// ```rust
112/// use image_charts::ImageCharts;
113///
114/// let chart = ImageCharts::new()
115/// .cht("p") // Pie chart
116/// .chd("t:60,40") // Data
117/// .chs("400x300") // Size
118/// .chl("Hello|World") // Labels
119/// .to_url();
120/// ```
121#[derive(Debug, Clone)]
122pub struct ImageCharts {
123 config: ImageChartsConfig,
124 query: HashMap<String, String>,
125}
126
127impl Default for ImageCharts {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133impl ImageCharts {
134 /// Create a new ImageCharts instance with default configuration
135 ///
136 /// # Example
137 ///
138 /// ```rust
139 /// use image_charts::ImageCharts;
140 ///
141 /// let chart = ImageCharts::new();
142 /// ```
143 pub fn new() -> Self {
144 Self::with_config(ImageChartsConfig::default())
145 }
146
147 /// Create a new ImageCharts instance with custom configuration
148 ///
149 /// # Example
150 ///
151 /// ```rust
152 /// use image_charts::{ ImageCharts, ImageChartsConfig };
153 /// use std::time::Duration;
154 ///
155 /// let config = ImageChartsConfig {
156 /// timeout: Duration::from_secs(10),
157 /// ..Default::default()
158 /// };
159 /// let chart = ImageCharts::with_config(config);
160 /// ```
161 pub fn with_config(config: ImageChartsConfig) -> Self {
162 Self {
163 config,
164 query: HashMap::new(),
165 }
166 }
167
168 /// Create a new ImageCharts instance for Enterprise usage with a secret key
169 ///
170 /// # Example
171 ///
172 /// ```rust
173 /// use image_charts::ImageCharts;
174 ///
175 /// let chart = ImageCharts::with_secret("my-secret-key");
176 /// ```
177 pub fn with_secret(secret: impl Into<String>) -> Self {
178 Self::with_config(ImageChartsConfig {
179 secret: Some(secret.into()),
180 ..Default::default()
181 })
182 }
183
184 /// Create a new ImageCharts builder for advanced configuration
185 ///
186 /// # Example
187 ///
188 /// ```rust
189 /// use image_charts::ImageCharts;
190 /// use std::time::Duration;
191 ///
192 /// let chart = ImageCharts::builder()
193 /// .secret("my-secret")
194 /// .timeout(Duration::from_secs(30))
195 /// .build();
196 /// ```
197 pub fn builder() -> ImageChartsBuilder {
198 ImageChartsBuilder::default()
199 }
200
201 fn clone_with(&self, key: impl Into<String>, value: impl Into<String>) -> Self {
202 let mut new_instance = self.clone();
203 new_instance.query.insert(key.into(), value.into());
204 new_instance
205 }
206
207
208 /// bvg= grouped bar chart, bvs= stacked bar chart, lc=line chart, ls=sparklines, p=pie chart. gv=graph viz
209 /// Three-dimensional pie chart (p3) will be rendered in 2D, concentric pie chart are not supported.
210 /// [Optional, line charts only] You can add :nda after the chart type in line charts to hide the default axes.
211 ///
212 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-type/)
213 ///
214 /// # Examples
215 ///
216 /// ```rust
217 /// use image_charts::ImageCharts;
218 /// let chart = ImageCharts::new().cht("bvg");
219 /// ```
220 ///
221 /// ```rust
222 /// use image_charts::ImageCharts;
223 /// let chart = ImageCharts::new().cht("p");
224 /// ```
225 pub fn cht(self, value: impl Into<String>) -> Self {
226 self.clone_with("cht", value)
227 }
228 /// chart data
229 ///
230 /// [Reference documentation](https://documentation.image-charts.com/reference/data-format/)
231 ///
232 /// # Examples
233 ///
234 /// ```rust
235 /// use image_charts::ImageCharts;
236 /// let chart = ImageCharts::new().chd("a:-100,200.5,75.55,110");
237 /// ```
238 ///
239 /// ```rust
240 /// use image_charts::ImageCharts;
241 /// let chart = ImageCharts::new().chd("t:10,20,30|15,25,35");
242 /// ```
243 pub fn chd(self, value: impl Into<String>) -> Self {
244 self.clone_with("chd", value)
245 }
246 /// You can configure some charts to scale automatically to fit their data with chds=a. The chart will be scaled so that the largest value is at the top of the chart and the smallest (or zero, if all values are greater than zero) will be at the bottom. Otherwise the "≶series_1_min>,≶series_1_max>,...,≶series_n_min>,≶series_n_max>" format set one or more minimum and maximum permitted values for each data series, separated by commas. You must supply both a max and a min. If you supply fewer pairs than there are data series, the last pair is applied to all remaining data series. Note that this does not change the axis range; to change the axis range, you must set the chxr parameter. Valid values range from (+/-)9.999e(+/-)199. You can specify values in either standard or E notation.
247 ///
248 /// [Reference documentation](https://documentation.image-charts.com/reference/data-format/#text-format-with-custom-scaling)
249 ///
250 /// # Examples
251 ///
252 /// ```rust
253 /// use image_charts::ImageCharts;
254 /// let chart = ImageCharts::new().chds("-80,140");
255 /// ```
256 pub fn chds(self, value: impl Into<String>) -> Self {
257 self.clone_with("chds", value)
258 }
259 /// How to encode the data in the QR code. 'UTF-8' is the default and only supported value. Contact our team if you wish to have support for Shift_JIS and/or ISO-8859-1.
260 ///
261 /// [Reference documentation](https://documentation.image-charts.com/qr-codes/#data-encoding)
262 ///
263 /// # Examples
264 ///
265 /// ```rust
266 /// use image_charts::ImageCharts;
267 /// let chart = ImageCharts::new().choe("UTF-8");
268 /// ```
269 pub fn choe(self, value: impl Into<String>) -> Self {
270 self.clone_with("choe", value)
271 }
272 /// QRCode error correction level and optional margin
273 ///
274 /// [Reference documentation](https://documentation.image-charts.com/qr-codes/#error-correction-level-and-margin)
275 ///
276 /// # Examples
277 ///
278 /// ```rust
279 /// use image_charts::ImageCharts;
280 /// let chart = ImageCharts::new().chld("L|4");
281 /// ```
282 ///
283 /// ```rust
284 /// use image_charts::ImageCharts;
285 /// let chart = ImageCharts::new().chld("M|10");
286 /// ```
287 ///
288 /// Default: `"L|4"`
289 pub fn chld(self, value: impl Into<String>) -> Self {
290 self.clone_with("chld", value)
291 }
292 /// You can specify the range of values that appear on each axis independently, using the chxr parameter. Note that this does not change the scale of the chart elements (use chds for that), only the scale of the axis labels.
293 ///
294 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-axis/#axis-range)
295 ///
296 /// # Examples
297 ///
298 /// ```rust
299 /// use image_charts::ImageCharts;
300 /// let chart = ImageCharts::new().chxr("0,0,200");
301 /// ```
302 ///
303 /// ```rust
304 /// use image_charts::ImageCharts;
305 /// let chart = ImageCharts::new().chxr("0,10,50,5");
306 /// ```
307 pub fn chxr(self, value: impl Into<String>) -> Self {
308 self.clone_with("chxr", value)
309 }
310 /// Some clients like Flowdock/Facebook messenger and so on, needs an URL to ends with a valid image extension file to display the image, use this parameter at the end your URL to support them. Valid values are ".png", ".svg" and ".gif".
311 /// Only QRCodes and GraphViz support svg output.
312 ///
313 /// [Reference documentation](https://documentation.image-charts.com/reference/output-format/)
314 ///
315 /// # Examples
316 ///
317 /// ```rust
318 /// use image_charts::ImageCharts;
319 /// let chart = ImageCharts::new().chof(".png");
320 /// ```
321 ///
322 /// ```rust
323 /// use image_charts::ImageCharts;
324 /// let chart = ImageCharts::new().chof(".svg");
325 /// ```
326 ///
327 /// Default: `".png"`
328 pub fn chof(self, value: impl Into<String>) -> Self {
329 self.clone_with("chof", value)
330 }
331 /// Maximum chart size for all charts except maps is 998,001 pixels total (Google Image Charts was limited to 300,000), and maximum width or length is 999 pixels.
332 ///
333 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-size/)
334 ///
335 /// # Examples
336 ///
337 /// ```rust
338 /// use image_charts::ImageCharts;
339 /// let chart = ImageCharts::new().chs("400x400");
340 /// ```
341 pub fn chs(self, value: impl Into<String>) -> Self {
342 self.clone_with("chs", value)
343 }
344 /// Format: <data_series_1_label>|...|<data_series_n_label>. The text for the legend entries. Each label applies to the corresponding series in the chd array. Use a + mark for a space. If you do not specify this parameter, the chart will not get a legend. There is no way to specify a line break in a label. The legend will typically expand to hold your legend text, and the chart area will shrink to accommodate the legend.
345 ///
346 /// [Reference documentation](https://documentation.image-charts.com/reference/legend-text-and-style/)
347 pub fn chdl(self, value: impl Into<String>) -> Self {
348 self.clone_with("chdl", value)
349 }
350 /// Specifies the color and font size of the legend text. <color>,<size>
351 ///
352 /// [Reference documentation](https://documentation.image-charts.com/reference/legend-text-and-style/)
353 ///
354 /// # Examples
355 ///
356 /// ```rust
357 /// use image_charts::ImageCharts;
358 /// let chart = ImageCharts::new().chdls("9e9e9e,17");
359 /// ```
360 ///
361 /// Default: `"000000"`
362 pub fn chdls(self, value: impl Into<String>) -> Self {
363 self.clone_with("chdls", value)
364 }
365 /// Solid or dotted grid lines
366 ///
367 /// [Reference documentation](https://documentation.image-charts.com/reference/grid-lines/)
368 ///
369 /// # Examples
370 ///
371 /// ```rust
372 /// use image_charts::ImageCharts;
373 /// let chart = ImageCharts::new().chg("1,1");
374 /// ```
375 ///
376 /// ```rust
377 /// use image_charts::ImageCharts;
378 /// let chart = ImageCharts::new().chg("0,1,1,5");
379 /// ```
380 pub fn chg(self, value: impl Into<String>) -> Self {
381 self.clone_with("chg", value)
382 }
383 /// You can specify the colors of a specific series using the chco parameter.
384 /// Format should be <series_2>,...,<series_m>, with each color in RRGGBB format hexadecimal number.
385 /// The exact syntax and meaning can vary by chart type; see your specific chart type for details.
386 /// Each entry in this string is an RRGGBB[AA] format hexadecimal number.
387 /// If there are more series or elements in the chart than colors specified in your string, the API typically cycles through element colors from the start of that series (for elements) or for series colors from the start of the series list.
388 /// Again, see individual chart documentation for details.
389 ///
390 /// [Reference documentation](https://documentation.image-charts.com/bar-charts/#examples)
391 ///
392 /// # Examples
393 ///
394 /// ```rust
395 /// use image_charts::ImageCharts;
396 /// let chart = ImageCharts::new().chco("FFC48C");
397 /// ```
398 ///
399 /// ```rust
400 /// use image_charts::ImageCharts;
401 /// let chart = ImageCharts::new().chco("FF0000,00FF00,0000FF");
402 /// ```
403 ///
404 /// Default: `"F56991,FF9F80,FFC48C,D1F2A5,EFFAB4"`
405 pub fn chco(self, value: impl Into<String>) -> Self {
406 self.clone_with("chco", value)
407 }
408 /// chart title
409 ///
410 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-title/)
411 ///
412 /// # Examples
413 ///
414 /// ```rust
415 /// use image_charts::ImageCharts;
416 /// let chart = ImageCharts::new().chtt("My beautiful chart");
417 /// ```
418 pub fn chtt(self, value: impl Into<String>) -> Self {
419 self.clone_with("chtt", value)
420 }
421 /// Format should be "<color>,<font_size>[,<opt_alignment>,<opt_font_family>,<opt_font_style>]", opt_alignement is not supported
422 ///
423 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-title/)
424 ///
425 /// # Examples
426 ///
427 /// ```rust
428 /// use image_charts::ImageCharts;
429 /// let chart = ImageCharts::new().chts("00FF00,17");
430 /// ```
431 pub fn chts(self, value: impl Into<String>) -> Self {
432 self.clone_with("chts", value)
433 }
434 /// Specify which axes you want (from: "x", "y", "t" and "r"). You can use several of them, separated by a coma; for example: "x,x,y,r". Order is important.
435 ///
436 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-axis/#visible-axes)
437 ///
438 /// # Examples
439 ///
440 /// ```rust
441 /// use image_charts::ImageCharts;
442 /// let chart = ImageCharts::new().chxt("y");
443 /// ```
444 ///
445 /// ```rust
446 /// use image_charts::ImageCharts;
447 /// let chart = ImageCharts::new().chxt("x,y");
448 /// ```
449 pub fn chxt(self, value: impl Into<String>) -> Self {
450 self.clone_with("chxt", value)
451 }
452 /// Specify one parameter set for each axis that you want to label. Format "<axis_index>:|<label_1>|...|<label_n>|...|<axis_index>:|<label_1>|...|<label_n>". Separate multiple sets of labels using the pipe character ( | ).
453 ///
454 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-axis/#custom-axis-labels)
455 pub fn chxl(self, value: impl Into<String>) -> Self {
456 self.clone_with("chxl", value)
457 }
458 /// You can specify the range of values that appear on each axis independently, using the chxr parameter. Note that this does not change the scale of the chart elements (use chds for that), only the scale of the axis labels.
459 ///
460 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-axis/#axis-label-styles)
461 ///
462 /// # Examples
463 ///
464 /// ```rust
465 /// use image_charts::ImageCharts;
466 /// let chart = ImageCharts::new().chxs("1,0000DD");
467 /// ```
468 ///
469 /// ```rust
470 /// use image_charts::ImageCharts;
471 /// let chart = ImageCharts::new().chxs("1N*cUSD*Mil,FF0000");
472 /// ```
473 pub fn chxs(self, value: impl Into<String>) -> Self {
474 self.clone_with("chxs", value)
475 }
476 ///
477 /// format should be either:
478 /// - line fills (fill the area below a data line with a solid color): chm=<b_or_B>,<color>,<start_line_index>,<end_line_index>,<0> |...| <b_or_B>,<color>,<start_line_index>,<end_line_index>,<0>
479 /// - line marker (add a line that traces data in your chart): chm=D,<color>,<series_index>,<which_points>,<width>,<opt_z_order>
480 /// - Text and Data Value Markers: chm=N<formatting_string>,<color>,<series_index>,<which_points>,<width>,<opt_z_order>,<font_family>,<font_style>
481 ///
482 ///
483 /// [Reference documentation](https://documentation.image-charts.com/reference/compound-charts/)
484 pub fn chm(self, value: impl Into<String>) -> Self {
485 self.clone_with("chm", value)
486 }
487 /// line thickness and solid/dashed style
488 ///
489 /// [Reference documentation](https://documentation.image-charts.com/line-charts/#line-styles)
490 ///
491 /// # Examples
492 ///
493 /// ```rust
494 /// use image_charts::ImageCharts;
495 /// let chart = ImageCharts::new().chls("10");
496 /// ```
497 ///
498 /// ```rust
499 /// use image_charts::ImageCharts;
500 /// let chart = ImageCharts::new().chls("3,6,3|5");
501 /// ```
502 pub fn chls(self, value: impl Into<String>) -> Self {
503 self.clone_with("chls", value)
504 }
505 /// If specified it will override "chdl" values
506 ///
507 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-label/)
508 pub fn chl(self, value: impl Into<String>) -> Self {
509 self.clone_with("chl", value)
510 }
511 /// Position and style of labels on data
512 ///
513 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-label/#positionning-and-formatting)
514 pub fn chlps(self, value: impl Into<String>) -> Self {
515 self.clone_with("chlps", value)
516 }
517 /// chart margins
518 ///
519 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-margin/)
520 ///
521 /// # Examples
522 ///
523 /// ```rust
524 /// use image_charts::ImageCharts;
525 /// let chart = ImageCharts::new().chma("30,30,30,30");
526 /// ```
527 ///
528 /// ```rust
529 /// use image_charts::ImageCharts;
530 /// let chart = ImageCharts::new().chma("40,20");
531 /// ```
532 pub fn chma(self, value: impl Into<String>) -> Self {
533 self.clone_with("chma", value)
534 }
535 /// Position of the legend and order of the legend entries
536 ///
537 /// [Reference documentation](https://documentation.image-charts.com/reference/legend-text-and-style/)
538 ///
539 /// Default: `"r"`
540 pub fn chdlp(self, value: impl Into<String>) -> Self {
541 self.clone_with("chdlp", value)
542 }
543 /// Background Fills
544 ///
545 /// [Reference documentation](https://documentation.image-charts.com/reference/background-fill/)
546 ///
547 /// # Examples
548 ///
549 /// ```rust
550 /// use image_charts::ImageCharts;
551 /// let chart = ImageCharts::new().chf("b0,lg,0,f44336,0.3,03a9f4,0.8");
552 /// ```
553 ///
554 /// Default: `"bg,s,FFFFFF"`
555 pub fn chf(self, value: impl Into<String>) -> Self {
556 self.clone_with("chf", value)
557 }
558 /// Bar corner radius. Display bars with rounded corner.
559 ///
560 /// [Reference documentation](https://documentation.image-charts.com/bar-charts/#rounded-bar)
561 ///
562 /// # Examples
563 ///
564 /// ```rust
565 /// use image_charts::ImageCharts;
566 /// let chart = ImageCharts::new().chbr("5");
567 /// ```
568 ///
569 /// ```rust
570 /// use image_charts::ImageCharts;
571 /// let chart = ImageCharts::new().chbr("10");
572 /// ```
573 pub fn chbr(self, value: impl Into<String>) -> Self {
574 self.clone_with("chbr", value)
575 }
576 /// gif configuration
577 ///
578 /// [Reference documentation](https://documentation.image-charts.com/reference/animation/)
579 ///
580 /// # Examples
581 ///
582 /// ```rust
583 /// use image_charts::ImageCharts;
584 /// let chart = ImageCharts::new().chan("1200");
585 /// ```
586 pub fn chan(self, value: impl Into<String>) -> Self {
587 self.clone_with("chan", value)
588 }
589 /// doughnut chart inside label
590 ///
591 /// [Reference documentation](https://documentation.image-charts.com/pie-charts/#inside-label)
592 ///
593 /// # Examples
594 ///
595 /// ```rust
596 /// use image_charts::ImageCharts;
597 /// let chart = ImageCharts::new().chli("45%");
598 /// ```
599 pub fn chli(self, value: impl Into<String>) -> Self {
600 self.clone_with("chli", value)
601 }
602 /// image-charts enterprise `account_id`
603 ///
604 /// [Reference documentation](https://documentation.image-charts.com/enterprise/)
605 ///
606 /// # Examples
607 ///
608 /// ```rust
609 /// use image_charts::ImageCharts;
610 /// let chart = ImageCharts::new().icac("accountId");
611 /// ```
612 pub fn icac(self, value: impl Into<String>) -> Self {
613 self.clone_with("icac", value)
614 }
615 /// HMAC-SHA256 signature required to activate paid features
616 ///
617 /// [Reference documentation](https://documentation.image-charts.com/enterprise/)
618 ///
619 /// # Examples
620 ///
621 /// ```rust
622 /// use image_charts::ImageCharts;
623 /// let chart = ImageCharts::new().ichm("0785cf22a0381c2e0239e27c126de4181f501d117c2c81745611e9db928b0376");
624 /// ```
625 pub fn ichm(self, value: impl Into<String>) -> Self {
626 self.clone_with("ichm", value)
627 }
628 /// How to use icff to define font family as Google Font : https://developers.google.com/fonts/docs/css2
629 ///
630 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-font/)
631 ///
632 /// # Examples
633 ///
634 /// ```rust
635 /// use image_charts::ImageCharts;
636 /// let chart = ImageCharts::new().icff("Abel");
637 /// ```
638 ///
639 /// ```rust
640 /// use image_charts::ImageCharts;
641 /// let chart = ImageCharts::new().icff("Akronim");
642 /// ```
643 pub fn icff(self, value: impl Into<String>) -> Self {
644 self.clone_with("icff", value)
645 }
646 /// Default font style for all text
647 ///
648 /// [Reference documentation](https://documentation.image-charts.com/reference/chart-font/)
649 ///
650 /// # Examples
651 ///
652 /// ```rust
653 /// use image_charts::ImageCharts;
654 /// let chart = ImageCharts::new().icfs("normal");
655 /// ```
656 ///
657 /// ```rust
658 /// use image_charts::ImageCharts;
659 /// let chart = ImageCharts::new().icfs("italic");
660 /// ```
661 pub fn icfs(self, value: impl Into<String>) -> Self {
662 self.clone_with("icfs", value)
663 }
664 /// localization (ISO 639-1)
665 ///
666 /// # Examples
667 ///
668 /// ```rust
669 /// use image_charts::ImageCharts;
670 /// let chart = ImageCharts::new().iclocale("en");
671 /// ```
672 pub fn iclocale(self, value: impl Into<String>) -> Self {
673 self.clone_with("iclocale", value)
674 }
675 /// Retina is a marketing term coined by Apple that refers to devices and monitors that have a resolution and pixel density so high — roughly 300 or more pixels per inch – that a person is unable to discern the individual pixels at a normal viewing distance.
676 /// In order to generate beautiful charts for these Retina displays, Image-Charts supports a retina mode that can be activated through the icretina=1 parameter
677 ///
678 /// [Reference documentation](https://documentation.image-charts.com/reference/retina/)
679 pub fn icretina(self, value: impl Into<String>) -> Self {
680 self.clone_with("icretina", value)
681 }
682 /// Background color for QR Codes
683 ///
684 /// [Reference documentation](https://documentation.image-charts.com/qr-codes/#background-color)
685 ///
686 /// # Examples
687 ///
688 /// ```rust
689 /// use image_charts::ImageCharts;
690 /// let chart = ImageCharts::new().icqrb("FFFFFF");
691 /// ```
692 ///
693 /// Default: `"FFFFFF"`
694 pub fn icqrb(self, value: impl Into<String>) -> Self {
695 self.clone_with("icqrb", value)
696 }
697 /// Foreground color for QR Codes
698 ///
699 /// [Reference documentation](https://documentation.image-charts.com/qr-codes/#foreground-color)
700 ///
701 /// # Examples
702 ///
703 /// ```rust
704 /// use image_charts::ImageCharts;
705 /// let chart = ImageCharts::new().icqrf("000000");
706 /// ```
707 ///
708 /// Default: `"000000"`
709 pub fn icqrf(self, value: impl Into<String>) -> Self {
710 self.clone_with("icqrf", value)
711 }
712
713
714 /// Get the full Image-Charts API URL (signed and encoded if necessary)
715 ///
716 /// This method returns the complete URL that can be used to fetch the chart image.
717 /// If an enterprise account ID (`icac`) is set and a secret is configured,
718 /// the URL will be automatically signed with HMAC-SHA256.
719 ///
720 /// # Example
721 ///
722 /// ```rust
723 /// use image_charts::ImageCharts;
724 ///
725 /// let url = ImageCharts::new()
726 /// .cht("p")
727 /// .chd("t:60,40")
728 /// .chs("100x100")
729 /// .to_url();
730 ///
731 /// assert!(url.starts_with("https://image-charts.com/chart?"));
732 /// ```
733 pub fn to_url(&self) -> String {
734 let mut pairs: Vec<(&String, &String)> = self.query.iter().collect();
735 pairs.sort_by(|a, b| a.0.cmp(b.0));
736
737 let mut query_string = pairs
738 .iter()
739 .map(|(k, v)| format!("{}={}", k, urlencoding::encode(v)))
740 .collect::<Vec<_>>()
741 .join("&");
742
743 if self.query.contains_key("icac") {
744 if let Some(ref secret) = self.config.secret {
745 if !secret.is_empty() {
746 let signature = self.sign(&query_string, secret);
747 query_string.push_str(&format!("&ichm={}", signature));
748 }
749 }
750 }
751
752 // Only include port if it's not the default for the protocol
753 let port_str = match (self.config.protocol.as_str(), self.config.port) {
754 ("https", 443) | ("http", 80) => String::new(),
755 (_, port) => format!(":{}", port),
756 };
757
758 format!(
759 "{}://{}{}{}?{}",
760 self.config.protocol,
761 self.config.host,
762 port_str,
763 self.config.pathname,
764 query_string
765 )
766 }
767
768 fn sign(&self, data: &str, secret: &str) -> String {
769 use hmac::{Hmac, Mac};
770 use sha2::Sha256;
771
772 type HmacSha256 = Hmac<Sha256>;
773 let mut mac =
774 HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
775 mac.update(data.as_bytes());
776 let result = mac.finalize();
777 hex::encode(result.into_bytes())
778 }
779
780 fn get_mime_type(&self) -> &str {
781 if self.query.contains_key("chan") {
782 "image/gif"
783 } else {
784 "image/png"
785 }
786 }
787
788 fn get_file_format(&self) -> &str {
789 if self.query.contains_key("chan") {
790 "gif"
791 } else {
792 "png"
793 }
794 }
795
796 fn build_user_agent(&self) -> String {
797 let default_ua = format!(
798 "rust-image_charts/{}{}",
799 env!("CARGO_PKG_VERSION"),
800 self.query
801 .get("icac")
802 .map(|icac| format!(" ({})", icac))
803 .unwrap_or_default()
804 );
805 self.config.user_agent.clone().unwrap_or(default_ua)
806 }
807
808 fn parse_error_response(
809 status: u16,
810 error_code: Option<String>,
811 validation_header: Option<&str>,
812 ) -> ImageChartsError {
813 let validation_message = validation_header
814 .and_then(|v| serde_json::from_str::<Vec<ValidationError>>(v).ok())
815 .map(|errors| {
816 errors
817 .into_iter()
818 .map(|e| e.message)
819 .collect::<Vec<_>>()
820 .join("\n")
821 });
822
823 let message = validation_message
824 .or_else(|| error_code.clone())
825 .unwrap_or_else(|| format!("HTTP {}", status));
826
827 let mut err = ImageChartsError::new(message).with_status(status);
828 if let Some(code) = error_code {
829 err = err.with_code(code);
830 }
831 err
832 }
833}
834
835// Async implementation
836#[cfg(feature = "async")]
837impl ImageCharts {
838 /// Do an async request to Image-Charts API and return the image as bytes
839 ///
840 /// # Example
841 ///
842 /// ```rust,no_run
843 /// use image_charts::ImageCharts;
844 ///
845 /// #[tokio::main]
846 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
847 /// let buffer = ImageCharts::new()
848 /// .cht("p")
849 /// .chd("t:60,40")
850 /// .chs("100x100")
851 /// .to_buffer()
852 /// .await?;
853 ///
854 /// println!("Image size: {} bytes", buffer.len());
855 /// Ok(())
856 /// }
857 /// ```
858 pub async fn to_buffer(&self) -> Result<Vec<u8>, ImageChartsError> {
859 let client = reqwest::Client::builder()
860 .timeout(self.config.timeout)
861 .build()
862 .map_err(|e| ImageChartsError::new(e.to_string()))?;
863
864 let response = client
865 .get(self.to_url())
866 .header("User-Agent", self.build_user_agent())
867 .send()
868 .await
869 .map_err(|e| {
870 let mut err = ImageChartsError::new(e.to_string());
871 if let Some(status) = e.status() {
872 err = err.with_status(status.as_u16());
873 }
874 err
875 })?;
876
877 let status = response.status().as_u16();
878 if (200..300).contains(&status) {
879 response
880 .bytes()
881 .await
882 .map(|b| b.to_vec())
883 .map_err(|e| ImageChartsError::new(e.to_string()).with_status(status))
884 } else {
885 let error_code = response
886 .headers()
887 .get("x-ic-error-code")
888 .and_then(|v| v.to_str().ok())
889 .map(String::from);
890 let validation_header = response
891 .headers()
892 .get("x-ic-error-validation")
893 .and_then(|v| v.to_str().ok())
894 .map(String::from);
895
896 Err(Self::parse_error_response(
897 status,
898 error_code,
899 validation_header.as_deref(),
900 ))
901 }
902 }
903
904 /// Do an async request and write the image to a file
905 ///
906 /// # Example
907 ///
908 /// ```rust,no_run
909 /// use image_charts::ImageCharts;
910 ///
911 /// #[tokio::main]
912 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
913 /// ImageCharts::new()
914 /// .cht("p")
915 /// .chd("t:60,40")
916 /// .chs("100x100")
917 /// .to_file("chart.png")
918 /// .await?;
919 ///
920 /// println!("Chart saved to chart.png");
921 /// Ok(())
922 /// }
923 /// ```
924 pub async fn to_file(&self, path: impl AsRef<std::path::Path>) -> Result<(), ImageChartsError> {
925 let buffer = self.to_buffer().await?;
926 tokio::fs::write(path, buffer)
927 .await
928 .map_err(|e| ImageChartsError::new(e.to_string()))
929 }
930
931 /// Do an async request and return a base64-encoded data URI
932 ///
933 /// The returned string can be used directly in HTML `<img>` tags or CSS.
934 ///
935 /// # Example
936 ///
937 /// ```rust,no_run
938 /// use image_charts::ImageCharts;
939 ///
940 /// #[tokio::main]
941 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
942 /// let data_uri = ImageCharts::new()
943 /// .cht("p")
944 /// .chd("t:60,40")
945 /// .chs("100x100")
946 /// .to_data_uri()
947 /// .await?;
948 ///
949 /// println!("<img src=\"{}\" />", data_uri);
950 /// Ok(())
951 /// }
952 /// ```
953 pub async fn to_data_uri(&self) -> Result<String, ImageChartsError> {
954 use base64::{engine::general_purpose::STANDARD, Engine as _};
955 let buffer = self.to_buffer().await?;
956 let encoded = STANDARD.encode(&buffer);
957 Ok(format!("data:{};base64,{}", self.get_mime_type(), encoded))
958 }
959}
960
961// Blocking implementation
962#[cfg(feature = "blocking")]
963impl ImageCharts {
964 /// Do a blocking request to Image-Charts API and return the image as bytes
965 ///
966 /// # Example
967 ///
968 /// ```rust,no_run
969 /// use image_charts::ImageCharts;
970 ///
971 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
972 /// let buffer = ImageCharts::new()
973 /// .cht("p")
974 /// .chd("t:60,40")
975 /// .chs("100x100")
976 /// .to_buffer_blocking()?;
977 ///
978 /// println!("Image size: {} bytes", buffer.len());
979 /// Ok(())
980 /// }
981 /// ```
982 pub fn to_buffer_blocking(&self) -> Result<Vec<u8>, ImageChartsError> {
983 let client = reqwest::blocking::Client::builder()
984 .timeout(self.config.timeout)
985 .build()
986 .map_err(|e| ImageChartsError::new(e.to_string()))?;
987
988 let response = client
989 .get(self.to_url())
990 .header("User-Agent", self.build_user_agent())
991 .send()
992 .map_err(|e| {
993 let mut err = ImageChartsError::new(e.to_string());
994 if let Some(status) = e.status() {
995 err = err.with_status(status.as_u16());
996 }
997 err
998 })?;
999
1000 let status = response.status().as_u16();
1001 if (200..300).contains(&status) {
1002 response
1003 .bytes()
1004 .map(|b| b.to_vec())
1005 .map_err(|e| ImageChartsError::new(e.to_string()).with_status(status))
1006 } else {
1007 let error_code = response
1008 .headers()
1009 .get("x-ic-error-code")
1010 .and_then(|v| v.to_str().ok())
1011 .map(String::from);
1012 let validation_header = response
1013 .headers()
1014 .get("x-ic-error-validation")
1015 .and_then(|v| v.to_str().ok())
1016 .map(String::from);
1017
1018 Err(Self::parse_error_response(
1019 status,
1020 error_code,
1021 validation_header.as_deref(),
1022 ))
1023 }
1024 }
1025
1026 /// Do a blocking request and write the image to a file
1027 ///
1028 /// # Example
1029 ///
1030 /// ```rust,no_run
1031 /// use image_charts::ImageCharts;
1032 ///
1033 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
1034 /// ImageCharts::new()
1035 /// .cht("p")
1036 /// .chd("t:60,40")
1037 /// .chs("100x100")
1038 /// .to_file_blocking("chart.png")?;
1039 ///
1040 /// println!("Chart saved to chart.png");
1041 /// Ok(())
1042 /// }
1043 /// ```
1044 pub fn to_file_blocking(
1045 &self,
1046 path: impl AsRef<std::path::Path>,
1047 ) -> Result<(), ImageChartsError> {
1048 let buffer = self.to_buffer_blocking()?;
1049 std::fs::write(path, buffer).map_err(|e| ImageChartsError::new(e.to_string()))
1050 }
1051
1052 /// Do a blocking request and return a base64-encoded data URI
1053 ///
1054 /// # Example
1055 ///
1056 /// ```rust,no_run
1057 /// use image_charts::ImageCharts;
1058 ///
1059 /// fn main() -> Result<(), Box<dyn std::error::Error>> {
1060 /// let data_uri = ImageCharts::new()
1061 /// .cht("p")
1062 /// .chd("t:60,40")
1063 /// .chs("100x100")
1064 /// .to_data_uri_blocking()?;
1065 ///
1066 /// println!("<img src=\"{}\" />", data_uri);
1067 /// Ok(())
1068 /// }
1069 /// ```
1070 pub fn to_data_uri_blocking(&self) -> Result<String, ImageChartsError> {
1071 use base64::{engine::general_purpose::STANDARD, Engine as _};
1072 let buffer = self.to_buffer_blocking()?;
1073 let encoded = STANDARD.encode(&buffer);
1074 Ok(format!("data:{};base64,{}", self.get_mime_type(), encoded))
1075 }
1076}
1077
1078/// Builder for ImageChartsConfig
1079///
1080/// Provides a fluent API to configure ImageCharts instances.
1081///
1082/// # Example
1083///
1084/// ```rust
1085/// use image_charts::ImageCharts;
1086/// use std::time::Duration;
1087///
1088/// let chart = ImageCharts::builder()
1089/// .secret("my-secret-key")
1090/// .timeout(Duration::from_secs(30))
1091/// .host("custom.image-charts.com")
1092/// .build()
1093/// .cht("p")
1094/// .chd("t:60,40")
1095/// .chs("100x100");
1096/// ```
1097#[derive(Debug, Default)]
1098pub struct ImageChartsBuilder {
1099 protocol: Option<String>,
1100 host: Option<String>,
1101 port: Option<u16>,
1102 pathname: Option<String>,
1103 timeout: Option<Duration>,
1104 secret: Option<String>,
1105 user_agent: Option<String>,
1106}
1107
1108impl ImageChartsBuilder {
1109 /// Set the protocol (http or https)
1110 pub fn protocol(mut self, protocol: impl Into<String>) -> Self {
1111 self.protocol = Some(protocol.into());
1112 self
1113 }
1114
1115 /// Set the API host
1116 pub fn host(mut self, host: impl Into<String>) -> Self {
1117 self.host = Some(host.into());
1118 self
1119 }
1120
1121 /// Set the API port
1122 pub fn port(mut self, port: u16) -> Self {
1123 self.port = Some(port);
1124 self
1125 }
1126
1127 /// Set the API pathname
1128 pub fn pathname(mut self, pathname: impl Into<String>) -> Self {
1129 self.pathname = Some(pathname.into());
1130 self
1131 }
1132
1133 /// Set the request timeout
1134 pub fn timeout(mut self, timeout: Duration) -> Self {
1135 self.timeout = Some(timeout);
1136 self
1137 }
1138
1139 /// Set the enterprise secret key for URL signing
1140 pub fn secret(mut self, secret: impl Into<String>) -> Self {
1141 self.secret = Some(secret.into());
1142 self
1143 }
1144
1145 /// Set a custom user-agent string
1146 pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
1147 self.user_agent = Some(user_agent.into());
1148 self
1149 }
1150
1151 /// Build the ImageCharts instance
1152 pub fn build(self) -> ImageCharts {
1153 let default = ImageChartsConfig::default();
1154 ImageCharts::with_config(ImageChartsConfig {
1155 protocol: self.protocol.unwrap_or(default.protocol),
1156 host: self.host.unwrap_or(default.host),
1157 port: self.port.unwrap_or(default.port),
1158 pathname: self.pathname.unwrap_or(default.pathname),
1159 timeout: self.timeout.unwrap_or(default.timeout),
1160 secret: self.secret,
1161 user_agent: self.user_agent,
1162 })
1163 }
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168 use super::*;
1169
1170 fn create_image_charts() -> ImageCharts {
1171 match std::env::var("IMAGE_CHARTS_USER_AGENT") {
1172 Ok(ua) => ImageCharts::builder().user_agent(ua).build(),
1173 Err(_) => ImageCharts::new(),
1174 }
1175 }
1176
1177 fn create_image_charts_with_secret(secret: &str) -> ImageCharts {
1178 match std::env::var("IMAGE_CHARTS_USER_AGENT") {
1179 Ok(ua) => ImageCharts::builder().secret(secret).user_agent(ua).build(),
1180 Err(_) => ImageCharts::with_secret(secret),
1181 }
1182 }
1183
1184 #[test]
1185 fn test_to_url_basic() {
1186 let url = ImageCharts::new().cht("p").chd("t:1,2,3").to_url();
1187 assert!(url.contains("cht=p"));
1188 assert!(url.contains("chd=t%3A1%2C2%2C3"));
1189 }
1190
1191 #[test]
1192 fn test_to_url_includes_protocol_host() {
1193 let url = ImageCharts::new().cht("p").to_url();
1194 // Default port (443 for https) should not be included in the URL
1195 assert!(url.starts_with("https://image-charts.com/chart?"));
1196 }
1197
1198 #[test]
1199 fn test_to_url_includes_custom_port() {
1200 let config = ImageChartsConfig {
1201 port: 8080,
1202 ..Default::default()
1203 };
1204 let url = ImageCharts::with_config(config).cht("p").to_url();
1205 // Non-default port should be included in the URL
1206 assert!(url.starts_with("https://image-charts.com:8080/chart?"));
1207 }
1208
1209 #[test]
1210 fn test_to_url_with_signature() {
1211 let url = ImageCharts::with_secret("plop")
1212 .cht("p")
1213 .chd("t:1,2,3")
1214 .chs("100x100")
1215 .icac("test_fixture")
1216 .to_url();
1217 assert!(url.contains("ichm="));
1218 }
1219
1220 #[test]
1221 fn test_signature_value() {
1222 // Test that HMAC signature matches expected value
1223 let chart = ImageCharts::with_secret("plop")
1224 .chs("100x100")
1225 .cht("p")
1226 .chd("t:1,2,3")
1227 .icac("test_fixture");
1228
1229 let url = chart.to_url();
1230 // The signature should be present
1231 assert!(url.contains("ichm="));
1232 }
1233
1234 #[test]
1235 fn test_default_config() {
1236 let config = ImageChartsConfig::default();
1237 assert_eq!(config.protocol, "https");
1238 assert_eq!(config.host, "image-charts.com");
1239 assert_eq!(config.port, 443);
1240 assert_eq!(config.pathname, "/chart");
1241 assert_eq!(config.timeout, Duration::from_millis(5000));
1242 }
1243
1244 #[test]
1245 fn test_builder_pattern() {
1246 let chart = ImageCharts::builder()
1247 .secret("test-secret")
1248 .timeout(Duration::from_secs(10))
1249 .host("custom.host.com")
1250 .build();
1251
1252 assert_eq!(chart.config.host, "custom.host.com");
1253 assert_eq!(chart.config.timeout, Duration::from_secs(10));
1254 assert_eq!(chart.config.secret, Some("test-secret".to_string()));
1255 }
1256
1257 #[test]
1258 fn test_fluent_api() {
1259 let url = ImageCharts::new()
1260 .cht("bvg")
1261 .chd("a:10,20,30")
1262 .chs("300x200")
1263 .chxt("x,y")
1264 .to_url();
1265
1266 assert!(url.contains("cht=bvg"));
1267 assert!(url.contains("chs=300x200"));
1268 }
1269
1270 #[test]
1271 fn test_get_mime_type_png() {
1272 let chart = ImageCharts::new().cht("p").chs("100x100");
1273 assert_eq!(chart.get_mime_type(), "image/png");
1274 }
1275
1276 #[test]
1277 fn test_get_mime_type_gif() {
1278 let chart = ImageCharts::new().cht("p").chs("100x100").chan("100");
1279 assert_eq!(chart.get_mime_type(), "image/gif");
1280 }
1281
1282 #[cfg(feature = "blocking")]
1283 mod blocking_tests {
1284 use super::*;
1285
1286 #[test]
1287 fn test_to_buffer_blocking_rejects_without_chs() {
1288 let result = create_image_charts().cht("p").chd("t:1,2,3").to_buffer_blocking();
1289 assert!(result.is_err());
1290 }
1291
1292 #[test]
1293 fn test_to_buffer_blocking_works() {
1294 // Add delay to avoid rate limiting
1295 std::thread::sleep(std::time::Duration::from_secs(3));
1296
1297 let result = create_image_charts()
1298 .cht("p")
1299 .chd("t:1,2,3")
1300 .chs("100x100")
1301 .to_buffer_blocking();
1302 assert!(result.is_ok());
1303 let buffer = result.unwrap();
1304 assert!(!buffer.is_empty());
1305 }
1306
1307 #[test]
1308 fn test_to_data_uri_blocking_works() {
1309 std::thread::sleep(std::time::Duration::from_secs(3));
1310
1311 let result = create_image_charts()
1312 .cht("p")
1313 .chd("t:1,2,3")
1314 .chs("100x100")
1315 .to_data_uri_blocking();
1316 assert!(result.is_ok());
1317 let data_uri = result.unwrap();
1318 assert!(data_uri.starts_with("data:image/png;base64,"));
1319 }
1320 }
1321
1322 #[cfg(feature = "async")]
1323 mod async_tests {
1324 use super::*;
1325
1326 #[tokio::test]
1327 async fn test_to_buffer_async_rejects_without_chs() {
1328 let result = create_image_charts()
1329 .cht("p")
1330 .chd("t:1,2,3")
1331 .to_buffer()
1332 .await;
1333 assert!(result.is_err());
1334 }
1335
1336 #[tokio::test]
1337 async fn test_to_buffer_async_works() {
1338 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
1339
1340 let result = create_image_charts()
1341 .cht("p")
1342 .chd("t:1,2,3")
1343 .chs("100x100")
1344 .to_buffer()
1345 .await;
1346 assert!(result.is_ok());
1347 let buffer = result.unwrap();
1348 assert!(!buffer.is_empty());
1349 }
1350
1351 #[tokio::test]
1352 async fn test_to_data_uri_async_works() {
1353 tokio::time::sleep(std::time::Duration::from_secs(3)).await;
1354
1355 let result = create_image_charts()
1356 .cht("p")
1357 .chd("t:1,2,3")
1358 .chs("100x100")
1359 .to_data_uri()
1360 .await;
1361 assert!(result.is_ok());
1362 let data_uri = result.unwrap();
1363 assert!(data_uri.starts_with("data:image/png;base64,"));
1364 }
1365 }
1366}