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 +4 -4
- data/CHANGELOG.md +13 -0
- data/Cargo.lock +4 -4
- data/README.md +17 -3
- data/ext/fast_xlsx/Cargo.toml +1 -1
- data/ext/fast_xlsx/src/lib.rs +324 -36
- data/lib/fast_xlsx/version.rb +1 -1
- data/lib/fast_xlsx.rb +41 -7
- data/sig/fast_xlsx.rbs +13 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5f9a163e801092064c80b6b921d5ca3c75987420652654809e0fea86d957be4e
|
|
4
|
+
data.tar.gz: e96a6a4b451d600c982291d1a391787f767f4c4d7bf4126f309e8608de7dba76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
210
|
+
version = "0.9.3"
|
|
211
211
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
212
|
-
checksum = "
|
|
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.
|
|
356
|
+
version = "0.101.0"
|
|
357
357
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
358
|
-
checksum = "
|
|
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,
|
|
319
|
-
ws.conditional_format(0,
|
|
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`.
|
data/ext/fast_xlsx/Cargo.toml
CHANGED
|
@@ -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.
|
|
17
|
+
rust_xlsxwriter = { version = "0.101.0", features = ["constant_memory"] }
|
|
18
18
|
|
|
19
19
|
[build-dependencies]
|
|
20
20
|
rb-sys-env = "0.2.2"
|
data/ext/fast_xlsx/src/lib.rs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
-
"
|
|
1742
|
-
let
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
"
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
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!(
|
|
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(
|
data/lib/fast_xlsx/version.rb
CHANGED
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, :
|
|
334
|
-
#
|
|
335
|
-
#
|
|
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 [
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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 | :
|
|
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, ?
|
|
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],
|