From d7358dfafab2b1ad7b47b5d883ad10920d8353d1 Mon Sep 17 00:00:00 2001 From: "Simeon H.K. Fitch" Date: Tue, 27 Sep 2022 17:09:53 -0400 Subject: [PATCH 1/8] Creating editable color tables. --- fixtures/labels.tif | Bin 0 -> 31835 bytes src/raster/rasterband.rs | 180 +++++++++++++++++++++++++++++++++------ src/raster/tests.rs | 38 ++++++++- 3 files changed, 189 insertions(+), 29 deletions(-) create mode 100644 fixtures/labels.tif diff --git a/fixtures/labels.tif b/fixtures/labels.tif new file mode 100644 index 0000000000000000000000000000000000000000..2b318c0327c7179fd867edbc9fac4d671f583d39 GIT binary patch literal 31835 zcmeI3ziSjh6vt;aXE-DXkr*umjfaA10zwKUCpl4UJP6p>9w=xl0Sm#O0l`ABPnpKb zQt(d^YyyJ#4+u8ro4LFFHE;IE>~3}kz5~g=dGp@e&wjE=svD2RrVyepR;3UP5t z``^Jk9WT?Avmd_d+y#p79K8_%onNB(`O#Y)_ik)&Q%WuQS%|?mA@2PY;wjC!axMB= zd$c?JeR|_?y0<$#UfcXD4`w~~@1v_Pf61F0A4hw)j$gc@(TgRuvs3EfDUv;9kgk2A z6>{)NJ%o_2pRH)SgX=r@?~WdfpX^T_PxdFbFYjz6_F{?!126ysFaQHE00S@p126ys zFaQHE00S@p126ysFaQHE00S@p126ysFaQHE00S@p126ysFaQHE00S@p126ysFaQHE z00S@p12E7*1H#A8{-_T((dwYi45S&eXW%htf5_9&-jK84UcBZkdB!h8XO38Kf0cv0 z(Km~ITN=G%UBkU4XTkBB*Y#eob6DqWrCU-;x4!(Y=E&uIk*I{H}Vz z6UZ$n&wpO7f}>VBuC|Vx>D+lLkM2A!uhAgSYp|c6F!?lvZU@UhVbSy3s#JvZRfbHb z9C@{gg}N1ep+b96OXPN-;wL70@!obT{UtZw^ma`azq{eQcx%g*-jW;Ojs53${|OPJ z+3W%M0x<=@NJrOHLF?OyjG~Apk=C2&tQ(Pq&`igksuSc4A*IZ}K)TUl?3tmRL6l|h6Bu``= zHyMPMBgY$2Tdm+U&jM&oS0{I$z;{|vQwMx^vFyxxvD)%gDbVz`{J?mJLXYnoiPOmQ zeZ_m70C-54dV(3$- literal 0 HcmV?d00001 diff --git a/src/raster/rasterband.rs b/src/raster/rasterband.rs index 642a0274b..84f689a77 100644 --- a/src/raster/rasterband.rs +++ b/src/raster/rasterband.rs @@ -3,13 +3,10 @@ use crate::gdal_major_object::MajorObject; use crate::metadata::Metadata; use crate::raster::{GDALDataType, GdalType}; use crate::utils::{_last_cpl_err, _last_null_pointer_err, _string}; -use gdal_sys::{ - self, CPLErr, GDALColorEntry, GDALColorInterp, GDALColorTableH, GDALComputeRasterMinMax, - GDALGetRasterStatistics, GDALMajorObjectH, GDALPaletteInterp, GDALRIOResampleAlg, GDALRWFlag, - GDALRasterBandH, GDALRasterIOExtraArg, -}; +use gdal_sys::{self, CPLErr, GDALColorEntry, GDALColorInterp, GDALColorTableH, GDALComputeRasterMinMax, GDALGetRasterStatistics, GDALMajorObjectH, GDALPaletteInterp, GDALRIOResampleAlg, GDALRWFlag, GDALRasterBandH, GDALRasterIOExtraArg, GDALCreateColorTable, GDALSetColorEntry, GDALSetRasterColorTable}; use libc::c_int; use std::ffi::CString; +use std::fmt::{Debug, Formatter}; use std::marker::PhantomData; #[cfg(feature = "ndarray")] @@ -485,6 +482,11 @@ impl<'a> RasterBand<'a> { Some(ColorTable::from_c_color_table(c_color_table)) } + /// Set the color table for this band. + pub fn set_color_table(&mut self, colors: ColorTable) { + unsafe { GDALSetRasterColorTable(self.c_rasterband, colors.c_color_table) }; + } + /// Returns the scale of this band if set. pub fn scale(&self) -> Option { let mut pb_success = 1; @@ -811,15 +813,21 @@ impl ColorInterpretation { } } +/// Types of color interpretations for a [`ColorTable`]. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum PaletteInterpretation { + /// Grayscale Gray, + /// Red, Green, Blue and Alpha Rgba, + /// Cyan, Magenta, Yellow and Black Cmyk, + /// Hue, Lightness and Saturation Hls, } impl PaletteInterpretation { + /// Instantiates Self from the C API int value of [`GDALPaletteInterp`]. fn from_c_int(palette_interpretation: GDALPaletteInterp::Type) -> Self { match palette_interpretation { GDALPaletteInterp::GPI_Gray => Self::Gray, @@ -829,6 +837,16 @@ impl PaletteInterpretation { _ => unreachable!("GDAL has implemented a new type of `GDALPaletteInterp`"), } } + + /// Returns the C API int value of this palette interpretation. + pub fn c_int(&self) -> GDALPaletteInterp::Type { + match self { + Self::Gray => GDALPaletteInterp::GPI_Gray, + Self::Rgba => GDALPaletteInterp::GPI_RGB, + Self::Cmyk => GDALPaletteInterp::GPI_CMYK, + Self::Hls => GDALPaletteInterp::GPI_HLS + } + } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -859,7 +877,7 @@ pub struct HlsEntry { pub s: i16, } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq)] pub enum ColorEntry { Gray(GrayEntry), Rgba(RgbaEntry), @@ -867,6 +885,96 @@ pub enum ColorEntry { Hls(HlsEntry), } +impl ColorEntry { + /// Instantiate a greyscale color entry + pub fn grey(g: i16) -> Self { + Self::Gray(GrayEntry { g }) + } + + /// Instantiate an red, green, blue, alpha color entry + pub fn rgba(r: i16, g: i16, b: i16, a: i16) -> Self { + Self::Rgba(RgbaEntry { r, g, b, a}) + } + + /// Instantiate a cyan, magenta, yellow, black color entry + pub fn cmyk(c: i16, m: i16, y: i16, k: i16) -> Self { + Self::Cmyk(CmykEntry { c, m, y, k }) + } + + /// Instantiate a hue, lightness, saturation color entry + pub fn hls(h: i16, l: i16, s: i16) -> Self { + Self::Hls(HlsEntry { h, l, s }) + } + + fn from(e: GDALColorEntry, interp: PaletteInterpretation) -> ColorEntry { + match interp { + PaletteInterpretation::Gray => ColorEntry::Gray(GrayEntry { + g: e.c1 + }), + PaletteInterpretation::Rgba => ColorEntry::Rgba(RgbaEntry { + r: e.c1, + g: e.c2, + b: e.c3, + a: e.c4, + }), + PaletteInterpretation::Cmyk => ColorEntry::Cmyk(CmykEntry { + c: e.c1, + m: e.c2, + y: e.c3, + k: e.c4, + }), + PaletteInterpretation::Hls => ColorEntry::Hls(HlsEntry { + h: e.c1, + l: e.c2, + s: e.c3, + }), + } + } +} + +impl From for GDALColorEntry { + fn from(e: ColorEntry) -> Self { + match e { + ColorEntry::Gray(e) => GDALColorEntry { + c1: e.g, + c2: 0, + c3: 0, + c4: 0 + }, + ColorEntry::Rgba(e) => GDALColorEntry { + c1: e.r, + c2: e.g, + c3: e.b, + c4: e.a + }, + ColorEntry::Cmyk(e) => GDALColorEntry { + c1: e.c, + c2: e.m, + c3: e.y, + c4: e.k + }, + ColorEntry::Hls(e) => GDALColorEntry { + c1: e.h, + c2: e.l, + c3: e.s, + c4: 0 + } + } + } +} + +// For more compact debug output, skip enum wrapper. +impl Debug for ColorEntry { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ColorEntry::Gray(e) => e.fmt(f), + ColorEntry::Rgba(e) => e.fmt(f), + ColorEntry::Cmyk(e) => e.fmt(f), + ColorEntry::Hls(e) => e.fmt(f) + } + } +} + /// Color table for raster bands that use the PaletteIndex color interpretation. /// /// This object carries the lifetime of the raster band that @@ -879,6 +987,16 @@ pub struct ColorTable<'a> { } impl<'a> ColorTable<'a> { + /// Instantiate a new color table with the given palette interpretation. + pub fn new(interp: PaletteInterpretation) -> Self { + let c_color_table = unsafe { GDALCreateColorTable(interp.c_int()) }; + Self { + palette_interpretation: interp, + c_color_table, + phantom_raster_band: PhantomData + } + } + fn from_c_color_table(c_color_table: GDALColorTableH) -> Self { let interp_index = unsafe { gdal_sys::GDALGetPaletteInterpretation(c_color_table) }; ColorTable { @@ -907,26 +1025,7 @@ impl<'a> ColorTable<'a> { } *c_color_entry }; - match self.palette_interpretation { - PaletteInterpretation::Gray => Some(ColorEntry::Gray(GrayEntry { g: color_entry.c1 })), - PaletteInterpretation::Rgba => Some(ColorEntry::Rgba(RgbaEntry { - r: color_entry.c1, - g: color_entry.c2, - b: color_entry.c3, - a: color_entry.c4, - })), - PaletteInterpretation::Cmyk => Some(ColorEntry::Cmyk(CmykEntry { - c: color_entry.c1, - m: color_entry.c2, - y: color_entry.c3, - k: color_entry.c4, - })), - PaletteInterpretation::Hls => Some(ColorEntry::Hls(HlsEntry { - h: color_entry.c1, - l: color_entry.c2, - s: color_entry.c3, - })), - } + Some(ColorEntry::from(color_entry, self.palette_interpretation)) } /// Get a color entry as RGB. @@ -950,4 +1049,33 @@ impl<'a> ColorTable<'a> { a: color_entry.c4, }) } + + /// Set entry in the RasterBand color table. + /// + /// The passed in entry must match the color interpretation of `self`. + /// + /// The table is grown as needed to hold the supplied index. + pub fn set_color_entry(&mut self, index: u16, entry: ColorEntry) { + unsafe { GDALSetColorEntry(self.c_color_table, index as c_int, &entry.into()) } + } +} + +impl Default for ColorTable<'_> { + fn default() -> Self { + Self::new(PaletteInterpretation::Rgba) + } } + +impl Debug for ColorTable<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let entries = (0..self.entry_count()) + .filter_map(|i| self.entry(i)) + .collect::>(); + + + f.debug_struct("ColorTable") + .field("palette_interpretation", &self.palette_interpretation) + .field("entries", &entries) + .finish() + } +} \ No newline at end of file diff --git a/src/raster/tests.rs b/src/raster/tests.rs index ba67a2a4c..be3eefa03 100644 --- a/src/raster/tests.rs +++ b/src/raster/tests.rs @@ -1,9 +1,7 @@ use crate::dataset::Dataset; use crate::metadata::Metadata; use crate::raster::rasterband::ResampleAlg; -use crate::raster::{ - ByteBuffer, ColorInterpretation, RasterCreationOption, StatisticsAll, StatisticsMinMax, -}; +use crate::raster::{ByteBuffer, ColorEntry, ColorInterpretation, ColorTable, RasterCreationOption, StatisticsAll, StatisticsMinMax}; use crate::test_utils::TempFixture; use crate::vsi::unlink_mem_file; use crate::Driver; @@ -12,6 +10,7 @@ use std::path::Path; #[cfg(feature = "ndarray")] use ndarray::arr2; +use crate::errors::GdalError; macro_rules! fixture { ($name:expr) => { @@ -807,6 +806,39 @@ fn test_color_table() { } } +#[test] +fn test_create_color_table() -> crate::errors::Result<()>{ + let fixture = TempFixture::fixture("labels.tif"); + // Open, modify, then close the base file. + { + let dataset = Dataset::open(&fixture)?; + assert_eq!(dataset.raster_count(), 1); + let mut band = dataset.rasterband(1)?; + assert_eq!(band.band_type(), GDALDataType::GDT_Byte); + assert!(band.color_table().is_none()); + + let mut ct = ColorTable::default(); + ct.set_color_entry(2, ColorEntry::rgba(255, 0, 0, 255)); + ct.set_color_entry(5, ColorEntry::rgba(0, 255, 0, 255)); + ct.set_color_entry(7, ColorEntry::rgba(0, 0, 255, 255)); + + assert_eq!(ct.entry_count(), 8); + assert_eq!(ct.entry(0), Some(ColorEntry::rgba(0, 0, 0, 0))); + assert_eq!(ct.entry(2), Some(ColorEntry::rgba(255, 0, 0, 255))); + assert_eq!(ct.entry(8), None); + + band.set_color_table(ct); + + } + + // Reopen to confirm the changes. + let dataset = Dataset::open(&fixture)?; + let band = dataset.rasterband(1)?; + let ct = band.color_table().ok_or_else(|| GdalError::BadArgument("missing color table".into()))?; + + Ok(()) +} + #[test] fn test_raster_stats() { let fixture = TempFixture::fixture("tinymarble.tif"); From 99809a2822f1ca58b6edb9d5716313a8173b9a6a Mon Sep 17 00:00:00 2001 From: "Simeon H.K. Fitch" Date: Wed, 28 Sep 2022 13:33:01 -0400 Subject: [PATCH 2/8] Color tabld test and example. --- src/raster/rasterband.rs | 33 +++++++++++++++++++++++++++++++++ src/raster/tests.rs | 4 ++++ 2 files changed, 37 insertions(+) diff --git a/src/raster/rasterband.rs b/src/raster/rasterband.rs index 84f689a77..a27424ff7 100644 --- a/src/raster/rasterband.rs +++ b/src/raster/rasterband.rs @@ -980,6 +980,39 @@ impl Debug for ColorEntry { /// This object carries the lifetime of the raster band that /// contains it. This is necessary to prevent the raster band /// from being dropped before the color table. +/// +/// # Example +/// +/// +/// ```rust, no_run +/// use gdal::{Dataset, Driver}; +/// use gdal::raster::{ColorEntry, ColorTable, PaletteInterpretation}; +/// # fn main() -> gdal::errors::Result<()> { +/// +/// // Open source multinomial classification raster +/// let ds = Dataset::open("fixtures/labels.tif")?; +/// +/// // Create in-memory copy to mutate +/// let mem_driver = Driver::get_by_name("MEM")?; +/// let ds = ds.create_copy(&mem_driver, "", &[])?; +/// let mut band = ds.rasterband(1)?; +/// assert!(band.color_table().is_none()); +/// +/// // Create a new color table for 3 classes + no-data +/// let mut ct = ColorTable::new(PaletteInterpretation::Rgba); +/// ct.set_color_entry(0, ColorEntry::rgba(255, 255, 0, 255)); +/// ct.set_color_entry(1, ColorEntry::rgba(0, 255, 255, 255)); +/// ct.set_color_entry(2, ColorEntry::rgba(255, 0, 255, 255)); +/// ct.set_color_entry(255, ColorEntry::rgba(0, 0, 0, 0)); +/// band.set_color_table(ct); +/// +/// // Render a PNG +/// let png_driver = Driver::get_by_name("PNG")?; +/// ds.create_copy(&png_driver, "/tmp/labels.png", &[])?; +/// +/// # Ok(()) +/// # } +/// ``` pub struct ColorTable<'a> { palette_interpretation: PaletteInterpretation, c_color_table: GDALColorTableH, diff --git a/src/raster/tests.rs b/src/raster/tests.rs index be3eefa03..0b0872fa1 100644 --- a/src/raster/tests.rs +++ b/src/raster/tests.rs @@ -835,6 +835,10 @@ fn test_create_color_table() -> crate::errors::Result<()>{ let dataset = Dataset::open(&fixture)?; let band = dataset.rasterband(1)?; let ct = band.color_table().ok_or_else(|| GdalError::BadArgument("missing color table".into()))?; + assert_eq!(ct.entry_count(), 8); + assert_eq!(ct.entry(0), Some(ColorEntry::rgba(0, 0, 0, 0))); + assert_eq!(ct.entry(2), Some(ColorEntry::rgba(255, 0, 0, 255))); + assert_eq!(ct.entry(8), None); Ok(()) } From a70e2b2883f4a526ae132e36f14d0f8ec8badc4e Mon Sep 17 00:00:00 2001 From: "Simeon H.K. Fitch" Date: Wed, 28 Sep 2022 14:58:51 -0400 Subject: [PATCH 3/8] Added color ramp creation. --- CHANGES.md | 5 ++ src/raster/rasterband.rs | 139 ++++++++++++++++++++++++++++++--------- src/raster/tests.rs | 33 +++++++--- 3 files changed, 137 insertions(+), 40 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5eed282f9..d68d6cf1e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,11 @@ - +- Added ability to set color table for bands with palette color interpretation. + Added ability to create a color ramp (interpolated) color table. + + - <> + ## 0.13 - Add prebuild bindings for GDAL 3.5 diff --git a/src/raster/rasterband.rs b/src/raster/rasterband.rs index a27424ff7..ba0a95725 100644 --- a/src/raster/rasterband.rs +++ b/src/raster/rasterband.rs @@ -3,7 +3,12 @@ use crate::gdal_major_object::MajorObject; use crate::metadata::Metadata; use crate::raster::{GDALDataType, GdalType}; use crate::utils::{_last_cpl_err, _last_null_pointer_err, _string}; -use gdal_sys::{self, CPLErr, GDALColorEntry, GDALColorInterp, GDALColorTableH, GDALComputeRasterMinMax, GDALGetRasterStatistics, GDALMajorObjectH, GDALPaletteInterp, GDALRIOResampleAlg, GDALRWFlag, GDALRasterBandH, GDALRasterIOExtraArg, GDALCreateColorTable, GDALSetColorEntry, GDALSetRasterColorTable}; +use gdal_sys::{ + self, CPLErr, GDALColorEntry, GDALColorInterp, GDALColorTableH, GDALComputeRasterMinMax, + GDALCreateColorRamp, GDALCreateColorTable, GDALDestroyColorTable, GDALGetPaletteInterpretation, + GDALGetRasterStatistics, GDALMajorObjectH, GDALPaletteInterp, GDALRIOResampleAlg, GDALRWFlag, + GDALRasterBandH, GDALRasterIOExtraArg, GDALSetColorEntry, GDALSetRasterColorTable, +}; use libc::c_int; use std::ffi::CString; use std::fmt::{Debug, Formatter}; @@ -483,7 +488,9 @@ impl<'a> RasterBand<'a> { } /// Set the color table for this band. - pub fn set_color_table(&mut self, colors: ColorTable) { + /// + /// See [`ColorTable`] for usage example. + pub fn set_color_table(&mut self, colors: &ColorTable) { unsafe { GDALSetRasterColorTable(self.c_rasterband, colors.c_color_table) }; } @@ -844,7 +851,7 @@ impl PaletteInterpretation { Self::Gray => GDALPaletteInterp::GPI_Gray, Self::Rgba => GDALPaletteInterp::GPI_RGB, Self::Cmyk => GDALPaletteInterp::GPI_CMYK, - Self::Hls => GDALPaletteInterp::GPI_HLS + Self::Hls => GDALPaletteInterp::GPI_HLS, } } } @@ -893,7 +900,7 @@ impl ColorEntry { /// Instantiate an red, green, blue, alpha color entry pub fn rgba(r: i16, g: i16, b: i16, a: i16) -> Self { - Self::Rgba(RgbaEntry { r, g, b, a}) + Self::Rgba(RgbaEntry { r, g, b, a }) } /// Instantiate a cyan, magenta, yellow, black color entry @@ -906,11 +913,20 @@ impl ColorEntry { Self::Hls(HlsEntry { h, l, s }) } + /// Get the ['PaletteInterpretation'] describing `self`. + pub fn palette_interpretation(&self) -> PaletteInterpretation { + match self { + ColorEntry::Gray(_) => PaletteInterpretation::Gray, + ColorEntry::Rgba(_) => PaletteInterpretation::Rgba, + ColorEntry::Cmyk(_) => PaletteInterpretation::Cmyk, + ColorEntry::Hls(_) => PaletteInterpretation::Hls, + } + } + + /// Create from a C [`GDALColorEntry`]. fn from(e: GDALColorEntry, interp: PaletteInterpretation) -> ColorEntry { match interp { - PaletteInterpretation::Gray => ColorEntry::Gray(GrayEntry { - g: e.c1 - }), + PaletteInterpretation::Gray => ColorEntry::Gray(GrayEntry { g: e.c1 }), PaletteInterpretation::Rgba => ColorEntry::Rgba(RgbaEntry { r: e.c1, g: e.c2, @@ -932,33 +948,33 @@ impl ColorEntry { } } -impl From for GDALColorEntry { - fn from(e: ColorEntry) -> Self { +impl From<&ColorEntry> for GDALColorEntry { + fn from(e: &ColorEntry) -> Self { match e { ColorEntry::Gray(e) => GDALColorEntry { c1: e.g, c2: 0, c3: 0, - c4: 0 + c4: 0, }, ColorEntry::Rgba(e) => GDALColorEntry { c1: e.r, c2: e.g, c3: e.b, - c4: e.a + c4: e.a, }, ColorEntry::Cmyk(e) => GDALColorEntry { c1: e.c, c2: e.m, c3: e.y, - c4: e.k + c4: e.k, }, ColorEntry::Hls(e) => GDALColorEntry { c1: e.h, c2: e.l, c3: e.s, - c4: 0 - } + c4: 0, + }, } } } @@ -970,12 +986,12 @@ impl Debug for ColorEntry { ColorEntry::Gray(e) => e.fmt(f), ColorEntry::Rgba(e) => e.fmt(f), ColorEntry::Cmyk(e) => e.fmt(f), - ColorEntry::Hls(e) => e.fmt(f) + ColorEntry::Hls(e) => e.fmt(f), } } } -/// Color table for raster bands that use the PaletteIndex color interpretation. +/// Color table for raster bands that use [`ColorInterpretation::PaletteIndex`] color interpretation. /// /// This object carries the lifetime of the raster band that /// contains it. This is necessary to prevent the raster band @@ -983,7 +999,6 @@ impl Debug for ColorEntry { /// /// # Example /// -/// /// ```rust, no_run /// use gdal::{Dataset, Driver}; /// use gdal::raster::{ColorEntry, ColorTable, PaletteInterpretation}; @@ -1000,11 +1015,11 @@ impl Debug for ColorEntry { /// /// // Create a new color table for 3 classes + no-data /// let mut ct = ColorTable::new(PaletteInterpretation::Rgba); -/// ct.set_color_entry(0, ColorEntry::rgba(255, 255, 0, 255)); -/// ct.set_color_entry(1, ColorEntry::rgba(0, 255, 255, 255)); -/// ct.set_color_entry(2, ColorEntry::rgba(255, 0, 255, 255)); -/// ct.set_color_entry(255, ColorEntry::rgba(0, 0, 0, 0)); -/// band.set_color_table(ct); +/// ct.set_color_entry(0, &ColorEntry::rgba(255, 255, 0, 255)); +/// ct.set_color_entry(1, &ColorEntry::rgba(0, 255, 255, 255)); +/// ct.set_color_entry(2, &ColorEntry::rgba(255, 0, 255, 255)); +/// ct.set_color_entry(255, &ColorEntry::rgba(0, 0, 0, 0)); +/// band.set_color_table(&ct); /// /// // Render a PNG /// let png_driver = Driver::get_by_name("PNG")?; @@ -1016,25 +1031,77 @@ impl Debug for ColorEntry { pub struct ColorTable<'a> { palette_interpretation: PaletteInterpretation, c_color_table: GDALColorTableH, + /// If `true`, Rust is responsible for deallocating color table pointed to by + /// `c_color_table`, which is the case when instantiated directly, as opposed to + /// when read via [`RasterBand::color_table`]. + rust_owned: bool, phantom_raster_band: PhantomData<&'a RasterBand<'a>>, } impl<'a> ColorTable<'a> { /// Instantiate a new color table with the given palette interpretation. pub fn new(interp: PaletteInterpretation) -> Self { - let c_color_table = unsafe { GDALCreateColorTable(interp.c_int()) }; + let c_color_table = unsafe { GDALCreateColorTable(interp.c_int()) }; Self { palette_interpretation: interp, c_color_table, - phantom_raster_band: PhantomData + rust_owned: true, + phantom_raster_band: PhantomData, } } + /// Constructs a color ramp from one color entry to another. + /// + /// `start_index` and `end_index` must be `0..=255`. + /// + /// Returns `None` if `start_color` and `end_color` do not have the same [`PaletteInterpretation`]. + /// + /// # Example + /// + /// ```rust, no_run + /// use gdal::{Dataset, Driver}; + /// use gdal::raster::{ColorEntry, ColorTable, PaletteInterpretation}; + /// # fn main() -> gdal::errors::Result<()> { + /// // Create a 16 step blue to white color table. + /// let ct = ColorTable::color_ramp( + /// 0, &ColorEntry::rgba(0, 0, 255, 255), + /// 15, &ColorEntry::rgba(255, 255, 255, 255) + /// )?; + /// println!("{ct:?}"); + /// # Ok(()) + /// # } + pub fn color_ramp( + start_index: u8, + start_color: &ColorEntry, + end_index: u8, + end_color: &ColorEntry, + ) -> Result> { + if start_color.palette_interpretation() != end_color.palette_interpretation() { + Err(GdalError::BadArgument( + "start_color and end_color must have the same palette_interpretation".into(), + )) + } else { + let ct = ColorTable::new(start_color.palette_interpretation()); + unsafe { + GDALCreateColorRamp( + ct.c_color_table, + start_index as c_int, + &start_color.into(), + end_index as c_int, + &end_color.into(), + ); + } + Ok(ct) + } + } + + /// Wrap C color table fn from_c_color_table(c_color_table: GDALColorTableH) -> Self { - let interp_index = unsafe { gdal_sys::GDALGetPaletteInterpretation(c_color_table) }; + let interp_index = unsafe { GDALGetPaletteInterpretation(c_color_table) }; ColorTable { palette_interpretation: PaletteInterpretation::from_c_int(interp_index), c_color_table, + rust_owned: false, phantom_raster_band: PhantomData, } } @@ -1062,6 +1129,8 @@ impl<'a> ColorTable<'a> { } /// Get a color entry as RGB. + /// + /// Returns `None` if `palette_interpretation` is not `Rgba`. pub fn entry_as_rgb(&self, index: usize) -> Option { let mut color_entry = GDALColorEntry { c1: 0, @@ -1085,11 +1154,20 @@ impl<'a> ColorTable<'a> { /// Set entry in the RasterBand color table. /// - /// The passed in entry must match the color interpretation of `self`. + /// The `entry` variant type must match `palette_interpretation`. /// - /// The table is grown as needed to hold the supplied index. - pub fn set_color_entry(&mut self, index: u16, entry: ColorEntry) { - unsafe { GDALSetColorEntry(self.c_color_table, index as c_int, &entry.into()) } + /// The table is grown as needed to hold the supplied index, filling in gaps with + /// the default [`GDALColorEntry`] value. + pub fn set_color_entry(&mut self, index: u16, entry: &ColorEntry) { + unsafe { GDALSetColorEntry(self.c_color_table, index as c_int, &entry.into()) } + } +} + +impl Drop for ColorTable<'_> { + fn drop(&mut self) { + if self.rust_owned { + unsafe { GDALDestroyColorTable(self.c_color_table) } + } } } @@ -1105,10 +1183,9 @@ impl Debug for ColorTable<'_> { .filter_map(|i| self.entry(i)) .collect::>(); - f.debug_struct("ColorTable") .field("palette_interpretation", &self.palette_interpretation) .field("entries", &entries) .finish() } -} \ No newline at end of file +} diff --git a/src/raster/tests.rs b/src/raster/tests.rs index 0b0872fa1..492f3d725 100644 --- a/src/raster/tests.rs +++ b/src/raster/tests.rs @@ -1,16 +1,19 @@ use crate::dataset::Dataset; use crate::metadata::Metadata; use crate::raster::rasterband::ResampleAlg; -use crate::raster::{ByteBuffer, ColorEntry, ColorInterpretation, ColorTable, RasterCreationOption, StatisticsAll, StatisticsMinMax}; +use crate::raster::{ + ByteBuffer, ColorEntry, ColorInterpretation, ColorTable, RasterCreationOption, StatisticsAll, + StatisticsMinMax, +}; use crate::test_utils::TempFixture; use crate::vsi::unlink_mem_file; use crate::Driver; use gdal_sys::GDALDataType; use std::path::Path; +use crate::errors::GdalError; #[cfg(feature = "ndarray")] use ndarray::arr2; -use crate::errors::GdalError; macro_rules! fixture { ($name:expr) => { @@ -807,7 +810,7 @@ fn test_color_table() { } #[test] -fn test_create_color_table() -> crate::errors::Result<()>{ +fn test_create_color_table() -> crate::errors::Result<()> { let fixture = TempFixture::fixture("labels.tif"); // Open, modify, then close the base file. { @@ -818,23 +821,24 @@ fn test_create_color_table() -> crate::errors::Result<()>{ assert!(band.color_table().is_none()); let mut ct = ColorTable::default(); - ct.set_color_entry(2, ColorEntry::rgba(255, 0, 0, 255)); - ct.set_color_entry(5, ColorEntry::rgba(0, 255, 0, 255)); - ct.set_color_entry(7, ColorEntry::rgba(0, 0, 255, 255)); + ct.set_color_entry(2, &ColorEntry::rgba(255, 0, 0, 255)); + ct.set_color_entry(5, &ColorEntry::rgba(0, 255, 0, 255)); + ct.set_color_entry(7, &ColorEntry::rgba(0, 0, 255, 255)); assert_eq!(ct.entry_count(), 8); assert_eq!(ct.entry(0), Some(ColorEntry::rgba(0, 0, 0, 0))); assert_eq!(ct.entry(2), Some(ColorEntry::rgba(255, 0, 0, 255))); assert_eq!(ct.entry(8), None); - band.set_color_table(ct); - + band.set_color_table(&ct); } // Reopen to confirm the changes. let dataset = Dataset::open(&fixture)?; let band = dataset.rasterband(1)?; - let ct = band.color_table().ok_or_else(|| GdalError::BadArgument("missing color table".into()))?; + let ct = band + .color_table() + .ok_or_else(|| GdalError::BadArgument("missing color table".into()))?; assert_eq!(ct.entry_count(), 8); assert_eq!(ct.entry(0), Some(ColorEntry::rgba(0, 0, 0, 0))); assert_eq!(ct.entry(2), Some(ColorEntry::rgba(255, 0, 0, 255))); @@ -843,6 +847,17 @@ fn test_create_color_table() -> crate::errors::Result<()>{ Ok(()) } +#[test] +fn test_color_ramp() -> crate::errors::Result<()> { + let ct = ColorTable::color_ramp(0, &ColorEntry::grey(0), 99, &ColorEntry::grey(99))?; + assert_eq!(ct.entry(0), Some(ColorEntry::grey(0))); + assert_eq!(ct.entry(57), Some(ColorEntry::grey(57))); + assert_eq!(ct.entry(99), Some(ColorEntry::grey(99))); + assert_eq!(ct.entry(100), None); + + Ok(()) +} + #[test] fn test_raster_stats() { let fixture = TempFixture::fixture("tinymarble.tif"); From cb1a8143730aca52be65a76d79fc99aaad5a0e94 Mon Sep 17 00:00:00 2001 From: "Simeon H.K. Fitch" Date: Wed, 28 Sep 2022 15:06:25 -0400 Subject: [PATCH 4/8] Documentation tweaks. --- src/raster/rasterband.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/raster/rasterband.rs b/src/raster/rasterband.rs index ba0a95725..eb729af91 100644 --- a/src/raster/rasterband.rs +++ b/src/raster/rasterband.rs @@ -856,11 +856,13 @@ impl PaletteInterpretation { } } +/// Grayscale [`ColorTable`] entry. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct GrayEntry { pub g: i16, } +/// Red, green, blue, alpha [`ColorTable`] entry. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct RgbaEntry { pub r: i16, @@ -869,6 +871,7 @@ pub struct RgbaEntry { pub a: i16, } +/// Cyan, magenta, yellow, black [`ColorTable`] entry. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct CmykEntry { pub c: i16, @@ -877,6 +880,7 @@ pub struct CmykEntry { pub k: i16, } +/// Hue, lightness, saturation [`ColorTable`] entry. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct HlsEntry { pub h: i16, @@ -884,6 +888,7 @@ pub struct HlsEntry { pub s: i16, } +/// Options for defining [`ColorInterpretation::PaletteIndex`] entries in a [`ColorTable`]. #[derive(Copy, Clone, PartialEq, Eq)] pub enum ColorEntry { Gray(GrayEntry), @@ -993,9 +998,6 @@ impl Debug for ColorEntry { /// Color table for raster bands that use [`ColorInterpretation::PaletteIndex`] color interpretation. /// -/// This object carries the lifetime of the raster band that -/// contains it. This is necessary to prevent the raster band -/// from being dropped before the color table. /// /// # Example /// From df79fbb8047f1860b91cf932ad79bcd46f6f949b Mon Sep 17 00:00:00 2001 From: "Simeon H.K. Fitch" Date: Wed, 28 Sep 2022 15:17:27 -0400 Subject: [PATCH 5/8] Linux test failure debugging. --- .github/workflows/ci.yml | 3 +++ CHANGES.md | 2 +- src/dataset.rs | 9 ++++++++- src/raster/tests.rs | 26 +++++++++++--------------- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a90fb904..f7f042874 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: +env: + RUST_BACKTRACE: 1 + jobs: gdal_35: name: "ci gdal-35" diff --git a/CHANGES.md b/CHANGES.md index d68d6cf1e..1364cb663 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,7 +30,7 @@ - Added ability to set color table for bands with palette color interpretation. Added ability to create a color ramp (interpolated) color table. - - <> + - ## 0.13 diff --git a/src/dataset.rs b/src/dataset.rs index ce1f9cc40..a38f98290 100644 --- a/src/dataset.rs +++ b/src/dataset.rs @@ -19,10 +19,10 @@ use crate::{ Driver, Metadata, }; -use gdal_sys::OGRGeometryH; use gdal_sys::{ self, CPLErr, GDALAccess, GDALDatasetH, GDALMajorObjectH, OGRErr, OGRLayerH, OGRwkbGeometryType, }; +use gdal_sys::{GDALFlushCache, OGRGeometryH}; use libc::{c_double, c_int, c_uint}; #[cfg(all(major_ge_3, minor_ge_1))] @@ -400,6 +400,13 @@ impl Dataset { Ok(Dataset { c_dataset }) } + /// Flush all write cached data to disk. + /// + /// See [`GDALFlushCache`]. + pub fn flush_cache(&self) { + unsafe { GDALFlushCache(self.c_dataset) } + } + /// Creates a new Dataset by wrapping a C pointer /// /// # Safety diff --git a/src/raster/tests.rs b/src/raster/tests.rs index 492f3d725..7b84dae82 100644 --- a/src/raster/tests.rs +++ b/src/raster/tests.rs @@ -11,7 +11,6 @@ use crate::Driver; use gdal_sys::GDALDataType; use std::path::Path; -use crate::errors::GdalError; #[cfg(feature = "ndarray")] use ndarray::arr2; @@ -810,13 +809,13 @@ fn test_color_table() { } #[test] -fn test_create_color_table() -> crate::errors::Result<()> { +fn test_create_color_table() { let fixture = TempFixture::fixture("labels.tif"); // Open, modify, then close the base file. { - let dataset = Dataset::open(&fixture)?; + let dataset = Dataset::open(&fixture).unwrap(); assert_eq!(dataset.raster_count(), 1); - let mut band = dataset.rasterband(1)?; + let mut band = dataset.rasterband(1).unwrap(); assert_eq!(band.band_type(), GDALDataType::GDT_Byte); assert!(band.color_table().is_none()); @@ -831,31 +830,28 @@ fn test_create_color_table() -> crate::errors::Result<()> { assert_eq!(ct.entry(8), None); band.set_color_table(&ct); + dataset.flush_cache(); + drop(dataset); } // Reopen to confirm the changes. - let dataset = Dataset::open(&fixture)?; - let band = dataset.rasterband(1)?; - let ct = band - .color_table() - .ok_or_else(|| GdalError::BadArgument("missing color table".into()))?; + let dataset = Dataset::open(&fixture).unwrap(); + let band = dataset.rasterband(1).unwrap(); + let ct = band.color_table().expect("saved color table"); + assert_eq!(ct.entry_count(), 8); assert_eq!(ct.entry(0), Some(ColorEntry::rgba(0, 0, 0, 0))); assert_eq!(ct.entry(2), Some(ColorEntry::rgba(255, 0, 0, 255))); assert_eq!(ct.entry(8), None); - - Ok(()) } #[test] -fn test_color_ramp() -> crate::errors::Result<()> { - let ct = ColorTable::color_ramp(0, &ColorEntry::grey(0), 99, &ColorEntry::grey(99))?; +fn test_color_ramp() { + let ct = ColorTable::color_ramp(0, &ColorEntry::grey(0), 99, &ColorEntry::grey(99)).unwrap(); assert_eq!(ct.entry(0), Some(ColorEntry::grey(0))); assert_eq!(ct.entry(57), Some(ColorEntry::grey(57))); assert_eq!(ct.entry(99), Some(ColorEntry::grey(99))); assert_eq!(ct.entry(100), None); - - Ok(()) } #[test] From 39d9d02d60dbbb04e302a7f3f0c4485e29c5ebc9 Mon Sep 17 00:00:00 2001 From: "Simeon H.K. Fitch" Date: Thu, 29 Sep 2022 11:24:30 -0400 Subject: [PATCH 6/8] Rework color table tests to not rely on editing and reopening an existing file, which seems to have race-condition issues on Linux. --- src/raster/tests.rs | 31 +++++++++++++++++++++---------- src/test_utils.rs | 17 ++++++++++++----- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/raster/tests.rs b/src/raster/tests.rs index 7b84dae82..789c4a0d7 100644 --- a/src/raster/tests.rs +++ b/src/raster/tests.rs @@ -810,15 +810,25 @@ fn test_color_table() { #[test] fn test_create_color_table() { - let fixture = TempFixture::fixture("labels.tif"); + let outfile = TempFixture::empty("color_labels.tif"); // Open, modify, then close the base file. { - let dataset = Dataset::open(&fixture).unwrap(); + let dataset = Dataset::open(fixture!("labels.tif")).unwrap(); + // Confirm we have a band without a color table. assert_eq!(dataset.raster_count(), 1); - let mut band = dataset.rasterband(1).unwrap(); + let band = dataset.rasterband(1).unwrap(); assert_eq!(band.band_type(), GDALDataType::GDT_Byte); assert!(band.color_table().is_none()); + // Create a new file to put color table in + let dataset = dataset + .create_copy(&dataset.driver(), &outfile, &[]) + .unwrap(); + dataset + .rasterband(1) + .unwrap() + .set_no_data_value(None) + .unwrap(); let mut ct = ColorTable::default(); ct.set_color_entry(2, &ColorEntry::rgba(255, 0, 0, 255)); ct.set_color_entry(5, &ColorEntry::rgba(0, 255, 0, 255)); @@ -829,20 +839,21 @@ fn test_create_color_table() { assert_eq!(ct.entry(2), Some(ColorEntry::rgba(255, 0, 0, 255))); assert_eq!(ct.entry(8), None); - band.set_color_table(&ct); - dataset.flush_cache(); - drop(dataset); + dataset.rasterband(1).unwrap().set_color_table(&ct); } // Reopen to confirm the changes. - let dataset = Dataset::open(&fixture).unwrap(); + let dataset = Dataset::open(&outfile).unwrap(); let band = dataset.rasterband(1).unwrap(); let ct = band.color_table().expect("saved color table"); - assert_eq!(ct.entry_count(), 8); - assert_eq!(ct.entry(0), Some(ColorEntry::rgba(0, 0, 0, 0))); + // Note: the GeoTIFF driver alters the palette, creating black entries to fill up all indexes + // up to 255. Other drivers may do things differently. + assert_eq!(ct.entry(0), Some(ColorEntry::rgba(0, 0, 0, 255))); assert_eq!(ct.entry(2), Some(ColorEntry::rgba(255, 0, 0, 255))); - assert_eq!(ct.entry(8), None); + assert_eq!(ct.entry(5), Some(ColorEntry::rgba(0, 255, 0, 255))); + assert_eq!(ct.entry(7), Some(ColorEntry::rgba(0, 0, 255, 255))); + assert_eq!(ct.entry(8), Some(ColorEntry::rgba(0, 0, 0, 255))); } #[test] diff --git a/src/test_utils.rs b/src/test_utils.rs index a4b120d65..a728529fc 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -12,13 +12,20 @@ impl TempFixture { /// /// This can potentially be removed when is resolved. pub fn fixture(name: &str) -> Self { - let path = std::path::Path::new("fixtures").join(name); + let staging = Self::empty(name); + let source = Path::new("fixtures").join(name); + std::fs::copy(&source, &staging.temp_path).unwrap(); + staging + } + /// Creates a temporary directory and path to a non-existent file with given `name`. + /// Useful for writing results to during testing + /// + /// Returns the struct `TempFixture` that contains the temp dir (for clean-up on `drop`) + /// as well as the empty file path. + pub fn empty(name: &str) -> Self { let _temp_dir = tempfile::tempdir().unwrap(); - let temp_path = _temp_dir.path().join(path.file_name().unwrap()); - - std::fs::copy(&path, &temp_path).unwrap(); - + let temp_path = _temp_dir.path().join(name); Self { _temp_dir, temp_path, From b3c5aa8356c878281b6fce15a48ef5863dfaaba3 Mon Sep 17 00:00:00 2001 From: "Simeon H.K. Fitch" Date: Tue, 4 Oct 2022 13:45:06 -0400 Subject: [PATCH 7/8] Update src/dataset.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Laurențiu Nicola --- src/dataset.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dataset.rs b/src/dataset.rs index a38f98290..3c1234437 100644 --- a/src/dataset.rs +++ b/src/dataset.rs @@ -403,7 +403,7 @@ impl Dataset { /// Flush all write cached data to disk. /// /// See [`GDALFlushCache`]. - pub fn flush_cache(&self) { + pub fn flush_cache(&mut self) { unsafe { GDALFlushCache(self.c_dataset) } } From 9e95a62c681d001429c87ea84cb95035eff61179 Mon Sep 17 00:00:00 2001 From: "Simeon H.K. Fitch" Date: Tue, 4 Oct 2022 15:21:07 -0400 Subject: [PATCH 8/8] Update src/raster/rasterband.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Laurențiu Nicola --- src/raster/rasterband.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/raster/rasterband.rs b/src/raster/rasterband.rs index eb729af91..acf656975 100644 --- a/src/raster/rasterband.rs +++ b/src/raster/rasterband.rs @@ -834,7 +834,7 @@ pub enum PaletteInterpretation { } impl PaletteInterpretation { - /// Instantiates Self from the C API int value of [`GDALPaletteInterp`]. + /// Creates a Rust [`PaletteInterpretation`] from a C API [`GDALPaletteInterp`] value. fn from_c_int(palette_interpretation: GDALPaletteInterp::Type) -> Self { match palette_interpretation { GDALPaletteInterp::GPI_Gray => Self::Gray,