fast_xlsx 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0642c1f4069d2b6537757d873ed2e591ddd3c7dab25f4bd1c867fad4fe8f0ead
4
- data.tar.gz: d0b7696cc3ee573ec02821a3b210ef34ce68fb821c83666f2fa0938c8bbabc16
3
+ metadata.gz: 5f9a163e801092064c80b6b921d5ca3c75987420652654809e0fea86d957be4e
4
+ data.tar.gz: e96a6a4b451d600c982291d1a391787f767f4c4d7bf4126f309e8608de7dba76
5
5
  SHA512:
6
- metadata.gz: 40fbd10f5ad43e8fc7a6bedccdc68cee4a324fa5ae9586dd7e9281272c6c2b1a465049c53fd67f042c9dffe00ca7c21c1512d4e1ffcbd01e2ea5ce52dc658938
7
- data.tar.gz: 5a6ca0279e195f3b109d23f0bf1afbed5680c6f6fd844aa0e30411468e1f8d7203680d30bba838cdb15b2dfac1f9c170a9b9576ed5db0650439c331fd2ff11be
6
+ metadata.gz: 3bca3c0050c69442f2688c02def0a9733a0495f3059c28e539591751b01e36f7f2eecacad4d889d783c944dd4e17b61a26f1143939a955ca1b9a51432232a1f8
7
+ data.tar.gz: '09c47cee091843afb86a244e489b502df740bdea08633a06c9bdf8308c5a10a98ac7135d7ca62a8f6f0816bbda2ed1e52d88a7d2155e004672b116eb4ebc622b'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2026-11-03
4
+
5
+ ### Added
6
+
7
+ - `Worksheet#embed_image` places an image in a cell, where it fits the cell and moves and sizes with it (Excel 365's "Place in Cell"; older Excel shows `#VALUE!`). It takes a path or an IO like `insert_image`, plus `alt_text:` and `format:`. It raises `FastXlsx::Error` in `memory: :constant` and `:low` mode, which rust_xlsxwriter does not support for embedded images.
8
+ - `conditional_format` types `:top` and `:bottom` (`value:` count, or a percentage with `percent: true`), `:average` (`criteria: :above`, `:below`, `:above_or_equal`, `:below_or_equal`, and `std_dev: 1..3`), `:duplicate`, `:unique`, `:blank`, `:not_blank`, `:error`, `:not_error`, and `:date` (`criteria: :yesterday`, `:today`, `:tomorrow`, `:last_7_days`, `:last_week`, `:this_week`, `:next_week`, `:last_month`, `:this_month`, `:next_month`).
9
+ - `conditional_format` type `:icon_set`, with `icons:` (e.g. `:three_arrows`, `:five_boxes`; default `:three_traffic_lights`), `reverse: true` and `icons_only: true`.
10
+ - Custom colors for data bars and color scales: `type: :data_bar, color: "#63BE7B"`, and `type: :color_scale, colors: [min, max]` or `[min, mid, max]`. `colors: 2` and `3` work as before. A data bar's `color:` sets both its fill and its border.
11
+
12
+ ### Deprecated
13
+
14
+ - `colors:` on `conditional_format` `type: :data_bar` was always ignored; it now prints a warning and will raise `ArgumentError` in 2.0. Use `color:` for the bar color.
15
+
3
16
  ## [1.0.0] - 2026-10-18
4
17
 
5
18
  The first stable release, with the same API as 0.8.1. From now on fast_xlsx follows Semantic Versioning: no breaking change before 2.0, and anything to be removed first prints a deprecation warning for at least one minor release.
data/Cargo.lock CHANGED
@@ -207,9 +207,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
207
207
 
208
208
  [[package]]
209
209
  name = "magnus"
210
- version = "0.9.2"
210
+ version = "0.9.3"
211
211
  source = "registry+https://github.com/rust-lang/crates.io-index"
212
- checksum = "b9068710e00d21be762edca59b9c4c8946dbdb29820d4f76ad559afea07982c9"
212
+ checksum = "40ef424404d166cbb71c8b8d19c9c589c748c4547c1afa1928c5504299f6c713"
213
213
  dependencies = [
214
214
  "magnus-macros",
215
215
  "rb-sys",
@@ -353,9 +353,9 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
353
353
 
354
354
  [[package]]
355
355
  name = "rust_xlsxwriter"
356
- version = "0.100.0"
356
+ version = "0.101.0"
357
357
  source = "registry+https://github.com/rust-lang/crates.io-index"
358
- checksum = "c44904ffb6c57b148ffcc8ce3011c28bc49b97e1a8343b2d632283cd566e1393"
358
+ checksum = "49aa1d1558e9465f5053bf9fb5361fe75db827c6a4423d5e5fe72096622f8908"
359
359
  dependencies = [
360
360
  "tempfile",
361
361
  "zip",
data/README.md CHANGED
@@ -254,7 +254,7 @@ ws.autofilter(0..100, 0..3) # rows and columns, each an Integer or a Range
254
254
  ws.merge_range(0, 0..3, "Q3 report", title) # row 1, columns A–D
255
255
  ```
256
256
 
257
- Methods that take one cell (`write`, `write_comment`, `insert_image`, `insert_chart`, `freeze_panes`) take `(row, col)` or a reference like `"B2"`.
257
+ Methods that take one cell (`write`, `write_comment`, `insert_image`, `embed_image`, `insert_chart`, `freeze_panes`) take `(row, col)` or a reference like `"B2"`.
258
258
 
259
259
  `autofit` only sees rows still in memory, so in `:constant` / `:low` memory mode it ignores rows already written to disk; set widths with `column_width` instead.
260
260
 
@@ -315,8 +315,14 @@ ws.conditional_format(0, 1, 99, 1, type: :cell, criteria: :<, value: 0, format:
315
315
  ws.conditional_format(0, 1, 99, 1, type: :cell, criteria: :between, value: [1, 10], format: red)
316
316
  ws.conditional_format(0, 0, 99, 0, type: :text, criteria: :contains, value: "error", format: red)
317
317
  ws.conditional_format(0, 0, 99, 3, type: :formula, value: "=$D1>100", format: red)
318
- ws.conditional_format(0, 2, 99, 2, type: :data_bar)
319
- ws.conditional_format(0, 2, 99, 2, type: :color_scale) # 3-color; colors: 2 for 2-color
318
+ ws.conditional_format(0, 1, 99, 1, type: :top, value: 10, format: red) # or :bottom; percent: true for a %
319
+ ws.conditional_format(0, 1, 99, 1, type: :average, criteria: :below, std_dev: 1, format: red)
320
+ ws.conditional_format(0, 0, 99, 0, type: :duplicate, format: red)
321
+ ws.conditional_format(0, 4, 99, 4, type: :date, criteria: :last_7_days, format: red)
322
+ ws.conditional_format(0, 2, 99, 2, type: :data_bar) # or color: "#63BE7B"
323
+ ws.conditional_format(0, 2, 99, 2, type: :color_scale) # 3-color; colors: 2 for 2-color
324
+ ws.conditional_format(0, 2, 99, 2, type: :color_scale, colors: %w[#F8696B #FFEB84 #63BE7B]) # min, mid, max
325
+ ws.conditional_format(0, 2, 99, 2, type: :icon_set, icons: :three_arrows) # reverse: true, icons_only: true
320
326
  ```
321
327
 
322
328
  | `type` | `criteria` | `value` |
@@ -324,7 +330,14 @@ ws.conditional_format(0, 2, 99, 2, type: :color_scale) # 3-color; col
324
330
  | `:cell` | `:==`, `:!=`, `:>`, `:>=`, `:<`, `:<=`, `:between`, `:not_between` | number or string; `[min, max]` for the range criteria |
325
331
  | `:text` | `:contains`, `:not_contains`, `:begins_with`, `:ends_with` | string |
326
332
  | `:formula` | — | formula string, relative to the top-left cell |
333
+ | `:top`, `:bottom` | — | how many, 1–1000 (default 10); with `percent: true`, a percentage 1–100 |
334
+ | `:average` | `:above` (default), `:below`, `:above_or_equal`, `:below_or_equal`; `std_dev: 1`, `2` or `3` with `:above`/`:below` | — |
335
+ | `:duplicate`, `:unique`, `:blank`, `:not_blank`, `:error`, `:not_error` | — | — |
336
+ | `:date` | `:yesterday`, `:today`, `:tomorrow`, `:last_7_days`, `:last_week`, `:this_week`, `:next_week`, `:last_month`, `:this_month`, `:next_month` | — |
327
337
  | `:data_bar`, `:color_scale` | — | — |
338
+ | `:icon_set` | — | — |
339
+
340
+ Colors are `"#RRGGBB"` or `0xRRGGBB`. `:data_bar` takes `color:`, which sets both the bar's fill and its border; `:color_scale` takes `colors:`, either `2` or `3` or the 2 or 3 colors from minimum to maximum. `:icon_set` takes `icons:` (default `:three_traffic_lights`): `:three_arrows`, `:three_arrows_gray`, `:three_flags`, `:three_traffic_lights`, `:three_traffic_lights_with_rim`, `:three_signs`, `:three_symbols_circled`, `:three_symbols`, `:three_stars`, `:three_triangles`, `:four_arrows`, `:four_arrows_gray`, `:four_red_to_black`, `:four_histograms`, `:four_traffic_lights`, `:five_arrows`, `:five_arrows_gray`, `:five_histograms`, `:five_quadrants`, `:five_boxes`; `reverse: true` reverses their order and `icons_only: true` hides the cell values.
328
341
 
329
342
  ### Data validation
330
343
 
@@ -350,6 +363,7 @@ ws.write_comment(0, 0, "Checked by finance", author: "Zac") # Excel shows it a
350
363
  ws.insert_image(0, 0, "logo.png") # top-left corner in A1
351
364
  ws.insert_image(0, 5, StringIO.new(blob.download), scale: 0.5, x_offset: 10, y_offset: 4, alt_text: "Logo")
352
365
  ws.insert_image(10, 0, "chart.png", width: 320, height: 180) # pixel size; one of them keeps the aspect ratio
366
+ ws.embed_image("B12", "photo.png", alt_text: "Photo", format: { align: :center }) # in the cell, sized with it (Excel 365; not in memory: :constant/:low)
353
367
  ```
354
368
 
355
369
  A String is always treated as a path, so wrap raw bytes (such as Active Storage's `blob.download`) in a `StringIO`.
@@ -14,7 +14,7 @@ magnus = { version = "0.9", features = ["rb-sys"] }
14
14
  rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
15
15
  # No "zlib" feature: without it zip compresses with zlib-rs, which saves
16
16
  # 20-30% faster than the system zlib (and needs no C library).
17
- rust_xlsxwriter = { version = "0.100.0", features = ["constant_memory"] }
17
+ rust_xlsxwriter = { version = "0.101.0", features = ["constant_memory"] }
18
18
 
19
19
  [build-dependencies]
20
20
  rb-sys-env = "0.2.2"
@@ -10,8 +10,12 @@ use magnus::{
10
10
  };
11
11
  use rust_xlsxwriter::{
12
12
  Color, ConditionalFormat, ConditionalFormat2ColorScale, ConditionalFormat3ColorScale,
13
+ ConditionalFormatAverage, ConditionalFormatAverageRule, ConditionalFormatBlank,
13
14
  ConditionalFormatCell, ConditionalFormatCellRule, ConditionalFormatDataBar,
14
- ConditionalFormatFormula, ConditionalFormatText, ConditionalFormatTextRule,
15
+ ConditionalFormatDate, ConditionalFormatDateRule, ConditionalFormatDuplicate,
16
+ ConditionalFormatError, ConditionalFormatFormula, ConditionalFormatIconSet,
17
+ ConditionalFormatIconType, ConditionalFormatText,
18
+ ConditionalFormatTextRule, ConditionalFormatTop, ConditionalFormatTopRule,
15
19
  ConditionalFormatValue, DataValidation, DataValidationRule, FormatAlign, FormatBorder,
16
20
  FormatScript, FormatUnderline, IgnoreError, IntoDataValidationValue, IntoExcelData, XlsxError,
17
21
  };
@@ -1675,15 +1679,6 @@ impl Worksheet {
1675
1679
  lc: u16,
1676
1680
  options: RHash,
1677
1681
  ) -> Result<Obj<Self>, Error> {
1678
- check_keys(
1679
- ruby,
1680
- options,
1681
- &["type", "criteria", "value", "format", "colors"],
1682
- "conditional_format",
1683
- )?;
1684
- let format = opt::<&Format>(ruby, options, "format")?;
1685
- let value = raw_opt(ruby, options, "value");
1686
- let criteria = raw_opt(ruby, options, "criteria");
1687
1682
  let kind = choice(
1688
1683
  ruby,
1689
1684
  "conditional format type",
@@ -1694,8 +1689,72 @@ impl Worksheet {
1694
1689
  ("formula", "formula"),
1695
1690
  ("data_bar", "data_bar"),
1696
1691
  ("color_scale", "color_scale"),
1692
+ ("top", "top"),
1693
+ ("bottom", "bottom"),
1694
+ ("average", "average"),
1695
+ ("duplicate", "duplicate"),
1696
+ ("unique", "unique"),
1697
+ ("blank", "blank"),
1698
+ ("not_blank", "not_blank"),
1699
+ ("error", "error"),
1700
+ ("not_error", "not_error"),
1701
+ ("date", "date"),
1702
+ ("icon_set", "icon_set"),
1697
1703
  ],
1698
1704
  )?;
1705
+ // Each type takes only the options it uses, except the types released
1706
+ // before :top etc., which keep ignoring unused options (SemVer).
1707
+ let allowed: &[&str] = match kind {
1708
+ "top" | "bottom" => &["type", "value", "percent", "format"],
1709
+ "average" => &["type", "criteria", "std_dev", "format"],
1710
+ "date" => &["type", "criteria", "format"],
1711
+ "duplicate" | "unique" | "blank" | "not_blank" | "error" | "not_error" => {
1712
+ &["type", "format"]
1713
+ }
1714
+ "icon_set" => &["type", "icons", "reverse", "icons_only"],
1715
+ // colors: is deprecated on :data_bar; the Ruby wrapper warns and drops it.
1716
+ "data_bar" => &["type", "criteria", "value", "format", "color"],
1717
+ _ => &["type", "criteria", "value", "format", "colors"],
1718
+ };
1719
+ check_keys(ruby, options, allowed, &format!("conditional_format :{kind}"))?;
1720
+ let format = opt::<&Format>(ruby, options, "format")?;
1721
+ let value = raw_opt(ruby, options, "value");
1722
+ let criteria = raw_opt(ruby, options, "criteria");
1723
+ // A whole number in `range`: other numbers raise ArgumentError with
1724
+ // `hint`, non-numbers TypeError.
1725
+ let whole = |v: Value, name: &str, range: std::ops::RangeInclusive<f64>, hint: &str| {
1726
+ let n = f64::try_convert(v)?;
1727
+ if n.fract() == 0.0 && range.contains(&n) {
1728
+ Ok(n as u16)
1729
+ } else {
1730
+ Err(Error::new(
1731
+ ruby.exception_arg_error(),
1732
+ format!("invalid {name} {}: {hint}", v.inspect()),
1733
+ ))
1734
+ }
1735
+ };
1736
+ // A color for the options added after 1.0, which raise TypeError for a
1737
+ // non-color type (Format keeps its released ArgumentError).
1738
+ let new_color = |c: Value| {
1739
+ if Integer::from_value(c).is_none() && RString::from_value(c).is_none() {
1740
+ return Err(Error::new(
1741
+ ruby.exception_type_error(),
1742
+ format!("invalid color {}: use \"#RRGGBB\" or 0xRRGGBB", c.inspect()),
1743
+ ));
1744
+ }
1745
+ color(ruby, c)
1746
+ };
1747
+ // true or false (nil when not given); other values raise TypeError.
1748
+ let flag = |name: &str| {
1749
+ let v = raw_opt(ruby, options, name);
1750
+ if v.to_bool() && !v.is_kind_of(ruby.class_true_class()) {
1751
+ return Err(Error::new(
1752
+ ruby.exception_type_error(),
1753
+ format!("invalid {name} {}: use true or false", v.inspect()),
1754
+ ));
1755
+ }
1756
+ Ok(v.to_bool())
1757
+ };
1699
1758
  // Built before taking the lock: building converts Ruby values.
1700
1759
  type AddRule = Box<dyn FnOnce(&mut rust_xlsxwriter::Worksheet) -> Result<(), XlsxError>>;
1701
1760
  fn rule<T: ConditionalFormat + Send + Sync + 'static>(
@@ -1705,14 +1764,21 @@ impl Worksheet {
1705
1764
  Box::new(move |ws| ws.add_conditional_format(fr, fc, lr, lc, &cf).map(|_| ()))
1706
1765
  }
1707
1766
  let range = (fr, fc, lr, lc);
1767
+ // The rule for a conditional format that takes format:.
1768
+ macro_rules! formatted {
1769
+ ($cf:expr) => {
1770
+ rule(
1771
+ range,
1772
+ match format {
1773
+ Some(f) => $cf.set_format(&f.0.format),
1774
+ None => $cf,
1775
+ },
1776
+ )
1777
+ };
1778
+ }
1708
1779
  let add_rule = match kind {
1709
1780
  "cell" => {
1710
- let mut cf =
1711
- ConditionalFormatCell::new().set_rule(cell_rule(ruby, criteria, value)?);
1712
- if let Some(f) = format {
1713
- cf = cf.set_format(&f.0.format);
1714
- }
1715
- rule(range, cf)
1781
+ formatted!(ConditionalFormatCell::new().set_rule(cell_rule(ruby, criteria, value)?))
1716
1782
  }
1717
1783
  "text" => {
1718
1784
  let text = String::try_convert(value)?;
@@ -1732,31 +1798,204 @@ impl Worksheet {
1732
1798
  2 => ConditionalFormatTextRule::BeginsWith(text),
1733
1799
  _ => ConditionalFormatTextRule::EndsWith(text),
1734
1800
  };
1735
- let mut cf = ConditionalFormatText::new().set_rule(text_rule);
1736
- if let Some(f) = format {
1737
- cf = cf.set_format(&f.0.format);
1738
- }
1739
- rule(range, cf)
1801
+ formatted!(ConditionalFormatText::new().set_rule(text_rule))
1802
+ }
1803
+ "formula" => formatted!(
1804
+ ConditionalFormatFormula::new().set_rule(String::try_convert(value)?.as_str())
1805
+ ),
1806
+ "top" | "bottom" => {
1807
+ let percent = raw_opt(ruby, options, "percent").to_bool();
1808
+ let max = if percent { 100.0 } else { 1000.0 };
1809
+ let n = if value.is_nil() {
1810
+ 10
1811
+ } else {
1812
+ whole(value, &format!("{kind} value"), 1.0..=max, &format!("use 1..{max}"))?
1813
+ };
1814
+ let top_rule = match (kind, percent) {
1815
+ ("top", false) => ConditionalFormatTopRule::Top(n),
1816
+ ("top", true) => ConditionalFormatTopRule::TopPercent(n),
1817
+ (_, false) => ConditionalFormatTopRule::Bottom(n),
1818
+ (_, true) => ConditionalFormatTopRule::BottomPercent(n),
1819
+ };
1820
+ formatted!(ConditionalFormatTop::new().set_rule(top_rule))
1821
+ }
1822
+ "average" => {
1823
+ use ConditionalFormatAverageRule as A;
1824
+ // The rule without std_dev, and its 1, 2 and 3 std_dev variants.
1825
+ let above = (
1826
+ A::AboveAverage,
1827
+ Some([
1828
+ A::OneStandardDeviationAbove,
1829
+ A::TwoStandardDeviationsAbove,
1830
+ A::ThreeStandardDeviationsAbove,
1831
+ ]),
1832
+ );
1833
+ let (plain, deviations) = if criteria.is_nil() {
1834
+ above
1835
+ } else {
1836
+ choice(
1837
+ ruby,
1838
+ "average criteria",
1839
+ criteria,
1840
+ &[
1841
+ ("above", above),
1842
+ (
1843
+ "below",
1844
+ (
1845
+ A::BelowAverage,
1846
+ Some([
1847
+ A::OneStandardDeviationBelow,
1848
+ A::TwoStandardDeviationsBelow,
1849
+ A::ThreeStandardDeviationsBelow,
1850
+ ]),
1851
+ ),
1852
+ ),
1853
+ ("above_or_equal", (A::EqualOrAboveAverage, None)),
1854
+ ("below_or_equal", (A::EqualOrBelowAverage, None)),
1855
+ ],
1856
+ )?
1857
+ };
1858
+ let std_dev = raw_opt(ruby, options, "std_dev");
1859
+ let hint = "use 1, 2 or 3, with criteria :above or :below";
1860
+ let average_rule = match (std_dev.is_nil(), deviations) {
1861
+ (true, _) => plain,
1862
+ (false, Some([one, two, three])) => {
1863
+ match whole(std_dev, "std_dev", 1.0..=3.0, hint)? {
1864
+ 1 => one,
1865
+ 2 => two,
1866
+ _ => three,
1867
+ }
1868
+ }
1869
+ (false, None) => {
1870
+ return Err(Error::new(
1871
+ ruby.exception_arg_error(),
1872
+ format!("invalid std_dev {}: {hint}", std_dev.inspect()),
1873
+ ))
1874
+ }
1875
+ };
1876
+ formatted!(ConditionalFormatAverage::new().set_rule(average_rule))
1877
+ }
1878
+ "duplicate" => formatted!(ConditionalFormatDuplicate::new()),
1879
+ "unique" => formatted!(ConditionalFormatDuplicate::new().invert()),
1880
+ "blank" => formatted!(ConditionalFormatBlank::new()),
1881
+ "not_blank" => formatted!(ConditionalFormatBlank::new().invert()),
1882
+ "error" => formatted!(ConditionalFormatError::new()),
1883
+ "not_error" => formatted!(ConditionalFormatError::new().invert()),
1884
+ "date" => {
1885
+ use ConditionalFormatDateRule as D;
1886
+ let period = choice(
1887
+ ruby,
1888
+ "date criteria",
1889
+ criteria,
1890
+ &[
1891
+ ("yesterday", D::Yesterday),
1892
+ ("today", D::Today),
1893
+ ("tomorrow", D::Tomorrow),
1894
+ ("last_7_days", D::Last7Days),
1895
+ ("last_week", D::LastWeek),
1896
+ ("this_week", D::ThisWeek),
1897
+ ("next_week", D::NextWeek),
1898
+ ("last_month", D::LastMonth),
1899
+ ("this_month", D::ThisMonth),
1900
+ ("next_month", D::NextMonth),
1901
+ ],
1902
+ )?;
1903
+ formatted!(ConditionalFormatDate::new().set_rule(period))
1740
1904
  }
1741
- "formula" => {
1742
- let mut cf =
1743
- ConditionalFormatFormula::new().set_rule(String::try_convert(value)?.as_str());
1744
- if let Some(f) = format {
1745
- cf = cf.set_format(&f.0.format);
1905
+ "data_bar" => {
1906
+ let bar = ConditionalFormatDataBar::new();
1907
+ match raw_opt(ruby, options, "color") {
1908
+ c if c.is_nil() => rule(range, bar),
1909
+ c => rule(range, bar.set_fill_color(new_color(c)?)),
1746
1910
  }
1747
- rule(range, cf)
1748
1911
  }
1749
- "data_bar" => rule(range, ConditionalFormatDataBar::new()),
1750
- _ => match opt::<u8>(ruby, options, "colors")?.unwrap_or(3) {
1751
- 2 => rule(range, ConditionalFormat2ColorScale::new()),
1752
- 3 => rule(range, ConditionalFormat3ColorScale::new()),
1753
- n => {
1754
- return Err(Error::new(
1912
+ "icon_set" => {
1913
+ use ConditionalFormatIconType as I;
1914
+ let icons = raw_opt(ruby, options, "icons");
1915
+ let icon_type = if icons.is_nil() {
1916
+ I::ThreeTrafficLights
1917
+ } else {
1918
+ choice(
1919
+ ruby,
1920
+ "icons",
1921
+ icons,
1922
+ &[
1923
+ ("three_arrows", I::ThreeArrows),
1924
+ ("three_arrows_gray", I::ThreeArrowsGray),
1925
+ ("three_flags", I::ThreeFlags),
1926
+ ("three_traffic_lights", I::ThreeTrafficLights),
1927
+ ("three_traffic_lights_with_rim", I::ThreeTrafficLightsWithRim),
1928
+ ("three_signs", I::ThreeSigns),
1929
+ ("three_symbols_circled", I::ThreeSymbolsCircled),
1930
+ ("three_symbols", I::ThreeSymbols),
1931
+ ("three_stars", I::ThreeStars),
1932
+ ("three_triangles", I::ThreeTriangles),
1933
+ ("four_arrows", I::FourArrows),
1934
+ ("four_arrows_gray", I::FourArrowsGray),
1935
+ ("four_red_to_black", I::FourRedToBlack),
1936
+ ("four_histograms", I::FourHistograms),
1937
+ ("four_traffic_lights", I::FourTrafficLights),
1938
+ ("five_arrows", I::FiveArrows),
1939
+ ("five_arrows_gray", I::FiveArrowsGray),
1940
+ ("five_histograms", I::FiveHistograms),
1941
+ ("five_quadrants", I::FiveQuadrants),
1942
+ ("five_boxes", I::FiveBoxes),
1943
+ ],
1944
+ )?
1945
+ };
1946
+ rule(
1947
+ range,
1948
+ ConditionalFormatIconSet::new()
1949
+ .set_icon_type(icon_type)
1950
+ .reverse_icons(flag("reverse")?)
1951
+ .show_icons_only(flag("icons_only")?),
1952
+ )
1953
+ }
1954
+ _ => {
1955
+ // colors: is how many (2 or 3), or the colors from minimum to maximum.
1956
+ let colors = raw_opt(ruby, options, "colors");
1957
+ let list = match RArray::from_value(colors) {
1958
+ Some(items) => {
1959
+ let mut list = Vec::with_capacity(items.len());
1960
+ each_entry(items, |_, c| {
1961
+ list.push(new_color(c)?);
1962
+ Ok(())
1963
+ })?;
1964
+ Some(list)
1965
+ }
1966
+ None => None,
1967
+ };
1968
+ let invalid = || {
1969
+ Error::new(
1755
1970
  ruby.exception_arg_error(),
1756
- format!("invalid colors {n}: use 2 or 3"),
1757
- ))
1971
+ format!(
1972
+ "invalid colors {}: use 2 or 3, or an Array of 2 or 3 colors",
1973
+ colors.inspect()
1974
+ ),
1975
+ )
1976
+ };
1977
+ match list.as_deref() {
1978
+ Some(&[min, max]) => rule(
1979
+ range,
1980
+ ConditionalFormat2ColorScale::new()
1981
+ .set_minimum_color(min)
1982
+ .set_maximum_color(max),
1983
+ ),
1984
+ Some(&[min, mid, max]) => rule(
1985
+ range,
1986
+ ConditionalFormat3ColorScale::new()
1987
+ .set_minimum_color(min)
1988
+ .set_midpoint_color(mid)
1989
+ .set_maximum_color(max),
1990
+ ),
1991
+ Some(_) => return Err(invalid()),
1992
+ None => match opt::<u8>(ruby, options, "colors")?.unwrap_or(3) {
1993
+ 2 => rule(range, ConditionalFormat2ColorScale::new()),
1994
+ 3 => rule(range, ConditionalFormat3ColorScale::new()),
1995
+ _ => return Err(invalid()),
1996
+ },
1758
1997
  }
1759
- },
1998
+ }
1760
1999
  };
1761
2000
  rb_self.with_ws(|ws| add_rule(ws).map_err(xerr))?;
1762
2001
  Ok(rb_self)
@@ -1911,6 +2150,54 @@ impl Worksheet {
1911
2150
  Ok(rb_self)
1912
2151
  }
1913
2152
 
2153
+ fn embed_image(
2154
+ ruby: &Ruby,
2155
+ rb_self: Obj<Self>,
2156
+ row: u32,
2157
+ col: u16,
2158
+ bytes: RString,
2159
+ alt_text: Option<String>,
2160
+ format: Option<&Format>,
2161
+ ) -> Result<Obj<Self>, Error> {
2162
+ // rust_xlsxwriter numbers embedded images only when saving, and panics
2163
+ // if it writes the cell to disk before that.
2164
+ if rb_self.flushes_rows {
2165
+ return Err(Error::new(
2166
+ ruby.get_inner(&ERROR),
2167
+ "embed_image is not supported in constant_memory / low_memory mode",
2168
+ ));
2169
+ }
2170
+ // SAFETY: the bytes are copied into the Image before any Ruby code runs.
2171
+ let mut image = Image::new_from_buffer(unsafe { bytes.as_slice() }).map_err(xerr)?;
2172
+ if let Some(alt_text) = alt_text {
2173
+ image = image.set_alt_text(alt_text);
2174
+ }
2175
+ let tables = rb_self.table_formats.borrow();
2176
+ let rows = rb_self.formatted_rows.borrow();
2177
+ let columns = rb_self.formatted_columns.borrow();
2178
+ // As in write(): an image is never a date, so with no format of its
2179
+ // own it takes its table column's, if any.
2180
+ let format = Self::format_for(
2181
+ &tables,
2182
+ &rows,
2183
+ &columns,
2184
+ row,
2185
+ col,
2186
+ format.map(|f| &*f.0),
2187
+ &CellValue::Empty,
2188
+ );
2189
+ rb_self.with_ws(|ws| {
2190
+ match format {
2191
+ Some(f) => ws.embed_image_with_format(row, col, &image, f),
2192
+ None => ws.embed_image(row, col, &image),
2193
+ }
2194
+ .map(|_| ())
2195
+ .map_err(xerr)
2196
+ })?;
2197
+ rb_self.advance(row);
2198
+ Ok(rb_self)
2199
+ }
2200
+
1914
2201
  fn insert_chart(
1915
2202
  ruby: &Ruby,
1916
2203
  rb_self: Obj<Self>,
@@ -2287,6 +2574,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
2287
2574
  ws.define_method("name", method!(Worksheet::name, 0))?;
2288
2575
  ws.define_method("_write_comment", method!(Worksheet::write_comment, 4))?;
2289
2576
  ws.define_method("_insert_image", method!(Worksheet::insert_image, 4))?;
2577
+ ws.define_method("_embed_image", method!(Worksheet::embed_image, 5))?;
2290
2578
  ws.define_method("_insert_chart", method!(Worksheet::insert_chart, 3))?;
2291
2579
  ws.define_method("_add_table", method!(Worksheet::add_table, 5))?;
2292
2580
  ws.define_method(
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FastXlsx
4
4
  # The gem version.
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
data/lib/fast_xlsx.rb CHANGED
@@ -269,6 +269,13 @@ module FastXlsx
269
269
  end
270
270
  private :_write_ref
271
271
 
272
+ # [row, col, image bytes] from insert_image's and embed_image's (*cell, source).
273
+ def image_args(args)
274
+ row, col, (source, *) = CellRange.cell(args, 1..1)
275
+ [row, col, source.respond_to?(:read) ? source.read : File.binread(source)]
276
+ end
277
+ private :image_args
278
+
272
279
  # Writes one cell: ws["B2"] = 42 or ws[1, 1] = 42. Use {#write} to give
273
280
  # it a format.
274
281
  # @overload []=(ref, value)
@@ -330,14 +337,30 @@ module FastXlsx
330
337
  # Highlights cells in the range by rule; see the README for each type's
331
338
  # options.
332
339
  # @param range a cell range (see {Worksheet})
333
- # @param type [Symbol] :cell, :text, :formula, :data_bar or :color_scale
334
- # @option options [Symbol] :criteria e.g. :>, :between, :contains
335
- # @option options [Numeric, String, Array] :value
340
+ # @param type [Symbol] :cell, :text, :formula, :top, :bottom, :average,
341
+ # :duplicate, :unique, :blank, :not_blank, :error, :not_error, :date,
342
+ # :data_bar, :color_scale or :icon_set
343
+ # @option options [Symbol] :criteria e.g. :>, :between, :contains,
344
+ # :below (:average), :last_7_days (:date)
345
+ # @option options [Numeric, String, Array] :value (:top and :bottom: how many, default 10)
336
346
  # @option options [Format, Hash] :format
337
- # @option options [Integer] :colors 2 or 3 (:color_scale)
347
+ # @option options [Boolean] :percent value is a percentage (:top, :bottom)
348
+ # @option options [Integer] :std_dev 1, 2 or 3 standard deviations (:average)
349
+ # @option options [Integer, Array] :colors 2 or 3, or the 2 or 3 colors
350
+ # ("#RRGGBB" or 0xRRGGBB) from minimum to maximum (:color_scale)
351
+ # @option options [String, Integer] :color bar fill and border color,
352
+ # "#RRGGBB" or 0xRRGGBB (:data_bar)
353
+ # @option options [Symbol] :icons e.g. :three_arrows, :five_boxes; default
354
+ # :three_traffic_lights (:icon_set)
355
+ # @option options [Boolean] :reverse reverse the order of the icons (:icon_set)
356
+ # @option options [Boolean] :icons_only hide the cell values (:icon_set)
338
357
  # @return [self]
339
358
  def conditional_format(*range, type:, **options)
340
359
  options[:format] = Format._coerce(options[:format]) if options.key?(:format)
360
+ if type == :data_bar && options.key?(:colors)
361
+ warn "fast_xlsx: colors: is ignored on :data_bar and will raise in 2.0; use color:", uplevel: 1
362
+ options.delete(:colors)
363
+ end
341
364
  _conditional_format(*CellRange.split(range).first, { type: type, **options })
342
365
  end
343
366
 
@@ -370,9 +393,20 @@ module FastXlsx
370
393
  # @return [self]
371
394
  # @raise [FastXlsx::Error] for data that isn't a supported image
372
395
  def insert_image(*args, **)
373
- row, col, (source, *) = CellRange.cell(args, 1..1)
374
- bytes = source.respond_to?(:read) ? source.read : File.binread(source)
375
- _insert_image(row, col, bytes, { ** })
396
+ _insert_image(*image_args(args), { ** })
397
+ end
398
+
399
+ # Embeds a PNG, JPEG, GIF or BMP image in the cell, where it fits the cell
400
+ # and moves and sizes with it (Excel 365's "Place in Cell"; older Excel
401
+ # shows #VALUE!). Not available in memory: :constant or :low mode.
402
+ # @overload embed_image(*cell, source, alt_text: nil, format: nil)
403
+ # @param source [String, #read] a file path or an IO
404
+ # @param alt_text [String, nil]
405
+ # @param format [Format, Hash, nil] the cell's format, e.g. to center the image
406
+ # @return [self]
407
+ # @raise [FastXlsx::Error] for data that isn't a supported image, or in those modes
408
+ def embed_image(*args, alt_text: nil, format: nil)
409
+ _embed_image(*image_args(args), alt_text, Format._coerce(format))
376
410
  end
377
411
 
378
412
  # Inserts a chart with its top-left corner in the cell.
data/sig/fast_xlsx.rbs CHANGED
@@ -124,9 +124,18 @@ module FastXlsx
124
124
  | (indexes rows, indexes cols, cell value, ?format? format) -> self
125
125
  | (*cell_range | cell args, format: format?) -> self
126
126
  def conditional_format: (*cell_range range,
127
- type: :cell | :text | :formula | :data_bar | :color_scale,
127
+ type: :cell | :text | :formula | :top | :bottom | :average | :duplicate | :unique
128
+ | :blank | :not_blank | :error | :not_error | :date | :data_bar | :color_scale
129
+ | :icon_set,
128
130
  ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
129
- ?format: format, ?colors: 2 | 3) -> self
131
+ ?format: format, ?percent: bool, ?std_dev: 1 | 2 | 3,
132
+ ?colors: 2 | 3 | Array[Format::color], ?color: Format::color,
133
+ ?icons: icon_type, ?reverse: bool, ?icons_only: bool) -> self
134
+ type icon_type = :three_arrows | :three_arrows_gray | :three_flags | :three_traffic_lights
135
+ | :three_traffic_lights_with_rim | :three_signs | :three_symbols_circled | :three_symbols
136
+ | :three_stars | :three_triangles | :four_arrows | :four_arrows_gray | :four_red_to_black
137
+ | :four_histograms | :four_traffic_lights | :five_arrows | :five_arrows_gray
138
+ | :five_histograms | :five_quadrants | :five_boxes
130
139
  def data_validation: (*cell_range range,
131
140
  type: :list | :whole_number | :decimal | :text_length,
132
141
  ?criteria: Symbol, ?value: Numeric | String | Array[Numeric | String],
@@ -138,6 +147,8 @@ module FastXlsx
138
147
  ?x_offset: Integer, ?y_offset: Integer, ?alt_text: String) -> self
139
148
  | (String cell, String | _Reader source, ?scale: Numeric, ?width: Numeric, ?height: Numeric,
140
149
  ?x_offset: Integer, ?y_offset: Integer, ?alt_text: String) -> self
150
+ def embed_image: (Integer row, Integer col, String | _Reader source, ?alt_text: String?, ?format: format?) -> self
151
+ | (String cell, String | _Reader source, ?alt_text: String?, ?format: format?) -> self
141
152
  type chart_series = { values: String, ?categories: String, ?name: String }
142
153
 
143
154
  def insert_chart: (Integer row, Integer col, type: Symbol, series: Array[chart_series],
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_xlsx
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac