Skip to content

Commit 78dfc0a

Browse files
authored
Merge pull request #38 from roteiro-gis/ian/dev
Add GRIB1 predefined bitmap writer support
2 parents 49ee08e + b470c0d commit 78dfc0a

3 files changed

Lines changed: 130 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- add GRIB2 Mercator grid template 3.10 and Albers equal-area grid template
66
3.31 reader metadata parsing, scan-order normalization, projected coordinate
77
offsets, flat data decode, and writer roundtrip support
8+
- add GRIB1 writer predefined bitmap references
89

910
## 0.5.0 - 2026-05-18
1011

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,13 @@ GribWriter::new(&mut bytes).write_grib2_message([field])?;
180180
- Feature-gated writer GRIB2 JPEG2000 template 5.40 and PNG template 5.41 packed data encode
181181
- Writer GRIB2 bitmap section generation from explicit masks or `NaN` values
182182
- Writer single-message multi-field GRIB2 output with reused grid sections
183-
- Writer GRIB1 regular lat/lon fields with simple packing and optional bitmap section
183+
- Writer GRIB1 regular lat/lon fields with simple packing and optional explicit
184+
or predefined bitmap section
184185

185186
## Not Yet Supported
186187

187188
- Remaining GRIB2 grid templates beyond 3.0, 3.10, 3.20, 3.30, and 3.31
188189
- Writer GRIB2 row-by-row complex packing
189-
- Writer GRIB1 predefined bitmap references
190190

191191
Unsupported cases fail explicitly with typed errors.
192192
Calendar-dependent forecast units such as months and years are exposed through

grib-writer/src/lib.rs

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub struct Grib1FieldBuilder {
6363
grid: Option<GridDefinition>,
6464
packing: Option<PackingStrategy>,
6565
values: Option<Vec<f64>>,
66-
bitmap: Option<Vec<bool>>,
66+
bitmap: Option<Grib1BitmapDefinition>,
6767
value_order: ValueOrder,
6868
}
6969

@@ -96,7 +96,22 @@ impl Grib1FieldBuilder {
9696
}
9797

9898
pub fn bitmap(mut self, bitmap: &[bool]) -> Self {
99-
self.bitmap = Some(bitmap.to_vec());
99+
self.bitmap = Some(Grib1BitmapDefinition {
100+
present: bitmap.to_vec(),
101+
table_reference: None,
102+
});
103+
self
104+
}
105+
106+
/// Use a GRIB1 predefined bitmap table reference.
107+
///
108+
/// The bitmap is used to pack only present values. The written message
109+
/// contains the nonzero table reference, not the bitmap payload itself.
110+
pub fn predefined_bitmap(mut self, table_reference: u16, bitmap: &[bool]) -> Self {
111+
self.bitmap = Some(Grib1BitmapDefinition {
112+
present: bitmap.to_vec(),
113+
table_reference: Some(table_reference),
114+
});
100115
self
101116
}
102117

@@ -130,22 +145,34 @@ impl Grib1FieldBuilder {
130145
});
131146
}
132147
if let Some(bitmap) = &bitmap {
133-
if bitmap.len() != expected {
148+
if bitmap.present.len() != expected {
134149
return Err(Error::DataLengthMismatch {
135150
expected,
136-
actual: bitmap.len(),
151+
actual: bitmap.present.len(),
137152
});
138153
}
154+
if bitmap.table_reference == Some(0) {
155+
return Err(Error::Other(
156+
"GRIB1 predefined bitmap table reference must be nonzero".into(),
157+
));
158+
}
139159
}
140160

141161
if self.value_order == ValueOrder::LogicalRowMajor {
142-
reorder_field_to_grib_scan_order(&grid, &mut values, bitmap.as_deref_mut())?;
162+
reorder_field_to_grib_scan_order(
163+
&grid,
164+
&mut values,
165+
bitmap.as_mut().map(|bitmap| bitmap.present.as_mut_slice()),
166+
)?;
143167
}
144168

145-
let packed = match packing {
169+
let bitmap_mask = bitmap.as_ref().map(|bitmap| bitmap.present.as_slice());
170+
let predefined_bitmap_reference = bitmap.as_ref().and_then(|bitmap| bitmap.table_reference);
171+
172+
let mut packed = match packing {
146173
PackingStrategy::SimpleAuto { decimal_scale } => {
147174
product.decimal_scale = decimal_scale;
148-
pack_simple_auto(&values, bitmap.as_deref(), decimal_scale)?
175+
pack_simple_auto(&values, bitmap_mask, decimal_scale)?
149176
}
150177
PackingStrategy::ComplexAuto { .. } => {
151178
return Err(Error::Other(
@@ -163,23 +190,35 @@ impl Grib1FieldBuilder {
163190
));
164191
}
165192
};
193+
if predefined_bitmap_reference.is_some() {
194+
packed.bitmap_payload = None;
195+
}
166196
product.has_grid_definition = true;
167-
product.has_bitmap = packed.bitmap_payload.is_some();
197+
product.has_bitmap =
198+
packed.bitmap_payload.is_some() || predefined_bitmap_reference.is_some();
168199

169200
Ok(Grib1Field {
170201
product,
171202
grid,
172203
packed,
204+
predefined_bitmap_reference,
173205
})
174206
}
175207
}
176208

209+
#[derive(Debug, Clone)]
210+
struct Grib1BitmapDefinition {
211+
present: Vec<bool>,
212+
table_reference: Option<u16>,
213+
}
214+
177215
/// A validated, packed GRIB1 field ready for message serialization.
178216
#[derive(Debug, Clone)]
179217
pub struct Grib1Field {
180218
product: Grib1ProductDefinition,
181219
grid: GridDefinition,
182220
packed: PackedField,
221+
predefined_bitmap_reference: Option<u16>,
183222
}
184223

185224
impl Grib1Field {
@@ -375,7 +414,9 @@ impl<'a, W: Write> GribWriter<'a, W> {
375414
let mut body = Vec::new();
376415
write_grib1_product_section(&mut body, &field.product)?;
377416
write_grib1_grid_section(&mut body, &field.grid)?;
378-
if let Some(bitmap) = &field.packed.bitmap_payload {
417+
if let Some(table_reference) = field.predefined_bitmap_reference {
418+
write_grib1_predefined_bitmap_section(&mut body, table_reference)?;
419+
} else if let Some(bitmap) = &field.packed.bitmap_payload {
379420
write_grib1_bitmap_section(&mut body, bitmap, field.grid.num_points())?;
380421
}
381422
write_grib1_data_section(&mut body, &field.packed, 0)?;
@@ -1553,6 +1594,18 @@ fn write_grib1_bitmap_section(
15531594
Ok(())
15541595
}
15551596

1597+
fn write_grib1_predefined_bitmap_section(out: &mut Vec<u8>, table_reference: u16) -> Result<()> {
1598+
if table_reference == 0 {
1599+
return Err(Error::Other(
1600+
"GRIB1 predefined bitmap table reference must be nonzero".into(),
1601+
));
1602+
}
1603+
write_u24_be(out, 6)?;
1604+
write_u8_be(out, 0)?;
1605+
write_u16_be(out, table_reference)?;
1606+
Ok(())
1607+
}
1608+
15561609
fn write_grib1_data_section(out: &mut Vec<u8>, packed: &PackedField, flags: u8) -> Result<()> {
15571610
validate_grib1_binary_data_flags(flags)?;
15581611
let DataRepresentation::SimplePacking(params) = &packed.representation else {
@@ -2269,7 +2322,7 @@ mod tests {
22692322
PolarStereographicGrid, ProductDefinition, ProductDefinitionTemplate,
22702323
};
22712324
use grib_reader::sections::scan_sections;
2272-
use grib_reader::GribFile;
2325+
use grib_reader::{GribFile, OpenOptions, PredefinedBitmap};
22732326
use serde::Deserialize;
22742327

22752328
fn identification() -> Identification {
@@ -2589,6 +2642,70 @@ mod tests {
25892642
assert_eq!(decoded[3], 8.0);
25902643
}
25912644

2645+
#[test]
2646+
fn writes_grib1_predefined_bitmap_reference() {
2647+
let bitmap = [true, false, true, false];
2648+
let field = Grib1FieldBuilder::new()
2649+
.product(grib1_product())
2650+
.grid(grid())
2651+
.packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
2652+
.values(&[9.0, 999.0, 7.0, 999.0])
2653+
.predefined_bitmap(300, &bitmap)
2654+
.build()
2655+
.unwrap();
2656+
let bytes = write_grib1_message(field);
2657+
2658+
let bitmap_offset = 8 + 28 + 32;
2659+
assert_eq!(
2660+
&bytes[bitmap_offset..bitmap_offset + 6],
2661+
&[0, 0, 6, 0, 1, 44]
2662+
);
2663+
2664+
let err = match GribFile::from_bytes(bytes.clone()) {
2665+
Ok(_) => panic!("expected unsupported predefined bitmap"),
2666+
Err(err) => err,
2667+
};
2668+
assert!(matches!(
2669+
err,
2670+
grib_core::Error::UnsupportedBitmapIndicator(300)
2671+
));
2672+
2673+
let bitmap_payload = [0b1010_0000];
2674+
let predefined = [PredefinedBitmap {
2675+
center_id: 7,
2676+
subcenter_id: Some(0),
2677+
table_reference: 300,
2678+
bitmap: &bitmap_payload,
2679+
}];
2680+
let file = GribFile::from_bytes_with_grib1_predefined_bitmaps(
2681+
bytes,
2682+
OpenOptions::default(),
2683+
&predefined,
2684+
)
2685+
.unwrap();
2686+
let decoded = file.message(0).unwrap().read_flat_data_as_f64().unwrap();
2687+
assert_eq!(decoded[0], 9.0);
2688+
assert!(decoded[1].is_nan());
2689+
assert_eq!(decoded[2], 7.0);
2690+
assert!(decoded[3].is_nan());
2691+
}
2692+
2693+
#[test]
2694+
fn rejects_zero_grib1_predefined_bitmap_reference() {
2695+
let err = Grib1FieldBuilder::new()
2696+
.product(grib1_product())
2697+
.grid(grid())
2698+
.packing(PackingStrategy::SimpleAuto { decimal_scale: 0 })
2699+
.values(&[1.0, 999.0, 3.0, 999.0])
2700+
.predefined_bitmap(0, &[true, false, true, false])
2701+
.build()
2702+
.unwrap_err();
2703+
2704+
assert!(
2705+
matches!(err, grib_core::Error::Other(message) if message.contains("must be nonzero"))
2706+
);
2707+
}
2708+
25922709
#[test]
25932710
fn writes_grib1_ibm_float_reference_value() {
25942711
let bytes = write_grib1_message(grib1_simple_field(&[10.0, 11.0, 12.0, 13.0]));

0 commit comments

Comments
 (0)