[mapguide-commits] r10225 - in branches/4.0/MgDev: Common/Renderers Server/src/PostBuild Server/src/UnitTesting UnitTest/TestData UnitTest/TestData/RasterStylization

svn_mapguide at osgeo.org svn_mapguide at osgeo.org
Fri Oct 23 04:26:01 PDT 2026


Author: jng
Date: 2026-10-23 04:25:59 -0700 (Fri, 23 Oct 2026)
New Revision: 10225

Added:
   branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
   branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/make_fixtures.py
Modified:
   branches/4.0/MgDev/Common/Renderers/AGGImageIO.h
   branches/4.0/MgDev/Server/src/PostBuild/PostBuild.mak
   branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt
   branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp
   branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj
Log:
Add some raster stylization tests to establish behavioral baselines

Modified: branches/4.0/MgDev/Common/Renderers/AGGImageIO.h
===================================================================
--- branches/4.0/MgDev/Common/Renderers/AGGImageIO.h	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/Common/Renderers/AGGImageIO.h	2026-10-23 11:25:59 UTC (rev 10225)
@@ -18,9 +18,14 @@
 #ifndef AGGIMAGEIO_H
 #define AGGIMAGEIO_H
 
+#include "Renderers.h"
+
 class RS_ByteData;
 
-class AGGImageIO
+// Exported so the unit tests can decode a rendered PNG. On Windows this class
+// is only reachable from outside Renderers.dll through RENDERERS_API; on other
+// platforms the macro expands to nothing and default visibility applies.
+class RENDERERS_API AGGImageIO
 {
 public:
     static bool Save(const RS_String& filename, const RS_String& format,

Modified: branches/4.0/MgDev/Server/src/PostBuild/PostBuild.mak
===================================================================
--- branches/4.0/MgDev/Server/src/PostBuild/PostBuild.mak	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/Server/src/PostBuild/PostBuild.mak	2026-10-23 11:25:59 UTC (rev 10225)
@@ -821,6 +821,7 @@
     if EXIST "..\..\..\UnitTest\TestData\KmlService\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\KmlService\*.*" "..\..\bin\UnitTestFiles\"
     if EXIST "..\..\..\UnitTest\TestData\MappingService\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\MappingService\*.*" "..\..\bin\UnitTestFiles\"
     if EXIST "..\..\..\UnitTest\TestData\MdfModel\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\MdfModel\*.*" "..\..\bin\UnitTestFiles\"
+    if EXIST "..\..\..\UnitTest\TestData\RasterStylization\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\RasterStylization\*.*" "..\..\bin\UnitTestFiles\"
     if EXIST "..\..\..\UnitTest\TestData\ResourceService\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\ResourceService\*.*" "..\..\bin\UnitTestFiles\"
     if EXIST "..\..\..\UnitTest\TestData\ServerAdmin\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\ServerAdmin\*.*" "..\..\bin\UnitTestFiles\"
     if EXIST "..\..\..\UnitTest\TestData\Symbology\*.*" xcopy /r /d /y "..\..\..\UnitTest\TestData\Symbology\*.*" "..\..\bin\UnitTestFiles\"

Modified: branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/Server/src/UnitTesting/CMakeLists.txt	2026-10-23 11:25:59 UTC (rev 10225)
@@ -20,6 +20,7 @@
     ${MG_SERVER_SERVICES_DIR}/Kml
     ${MG_SERVER_SERVICES_DIR}/Tile
     ${FDO_INCLUDE_DIR}
+    ${FDO_INCLUDE_DIR}/ExpressionEngine
 )
 
 set(MgUnitTesting_SRCS
@@ -32,6 +33,7 @@
     TestLogManagerThread.cpp
     TestMappingService.cpp
     TestMdfModel.cpp
+    TestRasterStylization.cpp
     TestMisc.cpp
     TestPerformance.cpp
     TestRenderingService.cpp
@@ -42,6 +44,7 @@
     TestServiceManager.cpp
     TestSiteManager.cpp
     TestSiteService.cpp
+    TestStylization.cpp
     TestTileService.cpp
     TestProfilingService.cpp
     TestTransformMesh.cpp
@@ -53,4 +56,6 @@
 
 target_link_libraries(MgUnitTesting
     ${CPPUNIT_LIBRARIES}
+    MgStylization${MG_VERSION_SUFFIX}
+    MgMdfModel${MG_VERSION_SUFFIX}
 )
\ No newline at end of file

Added: branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp	                        (rev 0)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp	2026-10-23 11:25:59 UTC (rev 10225)
@@ -0,0 +1,657 @@
+//
+//  Copyright (C) 2004-2026 by Autodesk, Inc.
+//
+//  This library is free software; you can redistribute it and/or
+//  modify it under the terms of version 2.1 of the GNU Lesser
+//  General Public License as published by the Free Software Foundation.
+//
+//  This library is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+//  Lesser General Public License for more details.
+//
+//  You should have received a copy of the GNU Lesser General Public
+//  License along with this library; if not, write to the Free Software
+//  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+//
+
+// End to end raster rendering tests.
+//
+// There are two halves.  The first renders the synthetic fixtures below, whose
+// colours are known exactly, so every assertion is on a specific pixel value.
+// The second renders a handful of real rasters taken from the FDO GDAL provider
+// test data, writes the result to ../UnitTestFiles/Results/ and only sanity
+// checks it - real imagery has no expected colour to assert, so it is there for
+// human visual inspection.  Both halves use the same pipeline and layer style.
+//
+// Each fixture is a 64x64 GeoTIFF generated by
+// UnitTest/TestData/RasterStylization/make_fixtures.py, split into four 32x32
+// quadrants of one colour.  Their provenance and checksums are recorded in
+// UnitTest/TestData/RasterStylization/PROVENANCE.md.  CMake copies TestData
+// flat into ../UnitTestFiles.
+// The test publishes a GDAL feature source, a grid layer definition whose single
+// GridColorBand rule passes the band through as the pixel colour, and a map
+// definition; renders the map with MgRenderingService::RenderMap; decodes the
+// PNG; and checks the colour at the centre of each quadrant.
+//
+// That exercises the whole raster path - OSGeo.GDAL -> MgRaster -> RSMgRaster ->
+// RasterAdapter -> GridData::ReadRaster -> Band -> GridStylizer -> AGG renderer -
+// so the band code paths that GridData::ReadRaster and Band implement are
+// covered without a unit test stub.
+//
+// The GDAL provider reports the raster data model from the band layout
+// (FdoRfpBandRaster::GetDataModel), so the fixtures map onto ReadRaster's
+// branches like this:
+//
+//   file              bands/type        model     bpp  grid band     ReadRaster branch
+//   rgb24.tif         3 x Byte          RGB       24   UnsignedInt32 "24 bpp"
+//   rgba32.tif        4 x Byte (+alpha) RGBA      32   UnsignedInt32 else ("32 bpp")
+//   rgb48.tif         3 x UInt16        RGB       48   UnsignedInt32 "48 bpp"
+//   rgb48_int16.tif   3 x Int16         RGB       48   UnsignedInt32 "48 bpp"
+//   palette8.tif      1 x Byte + table  Palette    8   UnsignedInt32 palette (>= 8)
+//   gray8.tif         1 x Byte          Gray       8   UnsignedInt8  else (SetRowValue)
+//   gray16.tif        1 x UInt16        Gray      16   UnsignedInt16 else (SetRowValue)
+//   gray32i.tif       1 x Int32         Gray      32   UnsignedInt32 else (SetRowValue)
+//   gray32f.tif       1 x Float32       Gray      32   UnsignedInt32 else (SetRowValue)
+//   gray64f.tif       1 x Float64       Gray      64   UnsignedInt64 else (SetRowValue)
+//
+// "grid band" is what GridData::GetGridDataType derives from the bits per pixel
+// and the FdoRasterDataType the provider reports.  The GDAL provider only
+// reports UnsignedInteger, so the signed/float band types (Int32, Double32,
+// Double64) and the FdoRasterDataModelType_Data branch cannot be reached from a
+// file at all - the unit tests in TestStylization.cpp cover those directly.
+//
+// 64 bpp RGBA is deliberately not covered: ReadRaster's "else" branch writes a
+// 32 bpp row into the 64 bpp band, and the band's fill value is taken from a
+// 4 byte local while the band is 8 bytes wide, so what it renders depends on
+// uninitialised stack bytes.
+//
+// An UnsignedInt32/Int32 grid band is taken by the memcpy fast path in
+// GridStyleColorHandler::Visit, which copies the raw band bytes into the ARGB
+// colour band.  For the 24/48 bpp and palette branches the band already holds
+// packed opaque ARGB, so the copy reproduces the file exactly; for the single
+// band fixtures the raw samples are read as ARGB (alpha included), which is why
+// their expectations below are bit patterns rather than shades of grey.  Narrow
+// (8/16 bit) and 64 bit bands take the per pixel path instead.
+//
+
+#include "MapGuideCommon.h"
+#include "ServiceManager.h"
+#include "ServerSiteService.h"
+#include "Services/FeatureService.h"
+#include "Fdo.h"
+#include "ServerFeatureService.h"
+#include "FoundationDefs.h"
+#include "TestServiceFactory.h"
+#include "FdoConnectionManager.h"
+#include "CatchHelperMacros.h"
+#include "RendererStyles.h"
+#include "AGGImageIO.h"
+#include "catch.hpp"
+
+#include <algorithm>
+#include <cmath>
+#include <memory>
+#include <set>
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace
+{
+
+// The map definition coordinate system. The fixtures are written with the same
+// WGS84 geographic CS, so the raster is drawn without a datum transform.
+const STRING LL84_WKT = L"GEOGCS[\"LL84\",DATUM[\"WGS84\",SPHEROID[\"WGS84\",6378137.000,298.25722293]],"
+    L"PRIMEM[\"Greenwich\",0],UNIT[\"Degree\",0.017453292519943295]]";
+
+// Fixture size in pixels, and the sample points (centre of each quadrant).
+const int FIXTURE_SIZE = 64;
+const int QUADRANT_CENTRE_X[] = { 16, 48, 16, 48 };
+const int QUADRANT_CENTRE_Y[] = { 16, 16, 48, 48 };
+
+struct Fixture
+{
+    const char*  file;
+    const char*  note;
+    unsigned int rgb[4];    // expected 0x00RRGGBB, top-left, top-right, bottom-left, bottom-right
+};
+
+const Fixture FIXTURES[] =
+{
+    // These fixtures already render correctly from unmodified source on both
+    // Windows and Linux: they only use code paths that are shared between the
+    // platforms (SetRowValue plus the colour handler's memcpy fast path), so
+    // they define the behavioural baseline the band fixes must not disturb.
+    //
+    // The 24/48 bpp RGB and palette fixtures are deliberately absent: their
+    // bands are only filled by the 32 bit Band::SetValue path, which is a no-op
+    // on Linux before the fixes. They are added, with their expected colours,
+    // in the change that fixes those paths.
+
+    // The 32 bpp branch copies the source pixels through unchanged, so the
+    // source alpha survives and the renderer blends the colour over the white
+    // background: opaque pixels are exact, translucent and transparent ones are
+    // not.  The quadrants carry alpha 0x80, 0xFF, 0x00 and 0x40.
+    { "RasterStylization_rgba32.tif",      "32 bpp RGBA, alpha blended over the background",
+      { 0x87A9CB, 0xFF0000, 0xFFFFFF, 0xDECEC2 } },
+
+    // A narrow single band takes the per pixel path in GridColorBandHandler:
+    // the sample becomes the packed ARGB value with the alpha forced opaque, so
+    // the pixel shows the low 24 bits of the sample.
+    { "RasterStylization_gray8.tif",       "8 bpp single band",
+      { 0x000012, 0x000034, 0x000056, 0x000078 } },
+    { "RasterStylization_gray16.tif",      "16 bpp single band",
+      { 0x001234, 0x005678, 0x009ABC, 0x000100 } },
+    // A 32 bpp band is copied as raw bytes, so each sample is read as ARGB:
+    // 0x00123456/0x00234567/0x00345678/0x00456789 all have a zero alpha and
+    // therefore draw nothing.
+    { "RasterStylization_gray32i.tif",     "32 bpp integer single band, samples read as ARGB",
+      { 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF } },
+    // Same raw byte copy, so the float bit patterns of 4660/22136/39612/256
+    // become ARGB and are blended over the background with the alpha byte the
+    // pattern happens to contain (0x45, 0x46, 0x46, 0x40).
+    { "RasterStylization_gray32f.tif",     "32 bpp float single band, samples read as ARGB",
+      { 0xE1E5B9, 0xE8FAB8, 0xBFECB7, 0xDDBBBB } },
+    // Doubles are read as unsigned 64 bit samples, which is far outside the
+    // range a packed ARGB value can hold, so no pixel is coloured.
+    { "RasterStylization_gray64f.tif",     "64 bpp float single band, not styled",
+      { 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF } },
+};
+
+// Real rasters derived from the FDO GDAL provider test data
+// (fdo/Providers/GDAL/TestData) - see PROVENANCE.md in
+// UnitTest/TestData/RasterStylization for where each one came from and how it
+// was produced.
+//
+// Every one of them is 64-bit row aligned, because ReadRaster is called with
+// bAlignment = true and returns without reading when the provider's stream size
+// does not match the padded row length. A raster whose width * bpp is not a
+// multiple of 64 falls through to RasterAdapter's legacy decoder instead, which
+// is not the code under test here - and which has no 16 bpp case at all.
+//
+// The palette sheet is absent for the same reason as the palette fixture above:
+// its colour band is only filled through the 32 bit Band::SetValue path, so it
+// renders blank from unmodified source on Linux and is added with the fixes.
+//
+// Nothing here has an expected colour: the point is that genuine imagery and
+// elevation data survive the pipeline unaltered, so each fixture is rendered to
+// ../UnitTestFiles/Results/RealRaster_<name>.png for a human to look at, and the
+// assertions only check that the image was produced, that it is the requested
+// size, and that it has real variation in it rather than being a flat fill.
+//
+// LeakSanitizer note: this test case reports allocations from
+// MdfParser::IOGridColorBands::StartElement (three ChannelBand per <Bands>
+// colour, leaked by that parser) and from MgGeometryUtil::ComputeLineStringEnvelope
+// (the raster bounds envelope). Both are in MapGuide code this change does not
+// touch; the synthetic fixtures above, which use a plain <Band> colour, stay
+// leak free.
+
+// How the grid colour style reads the raster. Band 1 is the packed ARGB value
+// that the RGB and palette branches of GridData::ReadRaster build; a single
+// band raster has to be mapped through all three channels to stay grey.
+enum RealStyle
+{
+    StyleBand,      // GridColorBand - the band already holds a packed colour
+    StyleGrayBands  // GridColorBands - band 1 drives red, green and blue
+};
+
+struct RealFixture
+{
+    const char* file;
+    const char* note;
+    RealStyle   style;
+    double      lowBand;    // channel scaling, only used by StyleGrayBands
+    double      highBand;
+};
+
+const RealFixture REAL_FIXTURES[] =
+{
+    { "RealRaster_ae.tif",          "8 bit grey DRG tile, 60 m, custom projection",
+      StyleGrayBands, 0, 0 },
+    { "RealRaster_spif83.tif",      "8 bit grey, NAD83 / California zone 6, ~195 m",
+      StyleGrayBands, 0, 0 },
+    // The DEM holds metres (nodata is -32767), so it needs scaling to become a
+    // visible grey ramp; 0-3000 m covers the whole tile.
+    { "RealRaster_mtjefferson.tif", "Int16 USGS DEM as a 0-3000 m elevation ramp",
+      StyleGrayBands, 0, 3000 },
+};
+
+// Long edge of the rendered image. Rasters larger than this are downsampled,
+// which also exercises the resampling the provider does when the requested size
+// does not match the file. The view is set to the raster's own extent, so the
+// raster columns the grid covers stay equal to the file's width and the aligned
+// row length above still holds.
+const int REAL_RENDER_MAX_DIM = 800;
+
+std::string Narrow(CREFSTRING value)
+{
+    return MgUtil::WideCharToMultiByte(value);
+}
+
+// Absolute path of a fixture, resolved from the test working directory
+// (<build>/Server/src/Core) where ../UnitTestFiles is the copied test data.
+STRING FixturePath(const char* file)
+{
+    char cwd[1024] = { 0 };
+    if (NULL == ACE_OS::getcwd(cwd, sizeof(cwd)))
+        return L"";
+
+    // UnitTest/TestData is copied flat into ../UnitTestFiles by CMake. Use the
+    // std::string overload so the conversion result is owned by the STRING.
+    STRING path = MgUtil::MultiByteToWideChar(std::string(cwd));
+    path += L"/../UnitTestFiles/";
+    path += MgUtil::MultiByteToWideChar(std::string(file));
+    return path;
+}
+
+void PublishResource(MgResourceService* svcResource, CREFSTRING resourceId, const std::string& xml)
+{
+    Ptr<MgResourceIdentifier> res = new MgResourceIdentifier(resourceId);
+    Ptr<MgByteSource> src = new MgByteSource((unsigned char*)xml.data(), (INT32)xml.size());
+    Ptr<MgByteReader> content = src->GetReader();
+    svcResource->SetResource(res, content, NULL);
+}
+
+std::string FeatureSourceXml(const std::string& filePath)
+{
+    std::ostringstream xml;
+    xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+        << "<FeatureSource xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+        << " xsi:noNamespaceSchemaLocation=\"FeatureSource-1.0.0.xsd\">"
+        // No version suffix: FDO resolves an unversioned provider name to the
+        // highest registered version, so this survives an FDO 4.x -> 5.x bump.
+        << "<Provider>OSGeo.Gdal</Provider>"
+        << "<Parameter><Name>DefaultRasterFileLocation</Name>"
+        << "<Value>" << filePath << "</Value></Parameter>"
+        << "</FeatureSource>";
+    return xml.str();
+}
+
+// A grid layer whose single colour rule uses band 1 as the pixel colour, so the
+// rendered image shows exactly what GridData::ReadRaster put in the band.
+// The GridLayerDefinition "Geometry" element is the raster property name: the
+// server builds a RESAMPLE computed property from it (see
+// MgMappingUtil::ExecuteRasterQuery).
+std::string LayerXml(CREFSTRING featureSourceId, CREFSTRING featureName, CREFSTRING rasterProperty,
+                     RealStyle style = StyleBand, double lowBand = 0, double highBand = 0)
+{
+    std::ostringstream xml;
+    xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+        << "<LayerDefinition xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+        << " xsi:noNamespaceSchemaLocation=\"LayerDefinition-4.0.0.xsd\" version=\"4.0.0\">"
+        << "<GridLayerDefinition>"
+        << "<ResourceId>" << Narrow(featureSourceId) << "</ResourceId>"
+        << "<FeatureName>" << Narrow(featureName) << "</FeatureName>"
+        << "<Geometry>" << Narrow(rasterProperty) << "</Geometry>"
+        << "<GridScaleRange>"
+        << "<MinScale>0</MinScale>"
+        << "<MaxScale>100000000000</MaxScale>"
+        << "<ColorStyle>";
+    switch (style)
+    {
+    case StyleGrayBands:
+    {
+        // A single band raster has to drive all three channels to stay grey.
+        // LowBand/HighBand scale elevation metres down to the 0-255 a channel
+        // can show; omitting them uses the 8 bit samples as they are.
+        xml.precision(17);
+        std::ostringstream scale;
+        if (highBand > lowBand)
+            scale << "<LowBand>" << lowBand << "</LowBand><HighBand>" << highBand << "</HighBand>";
+        xml << "<ColorRule><LegendLabel>Band 1</LegendLabel>"
+            << "<Color><Bands>"
+            << "<RedBand><Band>1</Band>" << scale.str() << "</RedBand>"
+            << "<GreenBand><Band>1</Band>" << scale.str() << "</GreenBand>"
+            << "<BlueBand><Band>1</Band>" << scale.str() << "</BlueBand>"
+            << "</Bands></Color>"
+            << "</ColorRule>";
+        break;
+    }
+    default:
+        xml << "<ColorRule><LegendLabel>Band 1</LegendLabel>"
+            << "<Color><Band>1</Band></Color>"
+            << "</ColorRule>";
+        break;
+    }
+    xml << "</ColorStyle>"
+        << "<RebuildFactor>1</RebuildFactor>"
+        << "</GridScaleRange>"
+        << "</GridLayerDefinition>"
+        << "</LayerDefinition>";
+    return xml.str();
+}
+
+std::string MapXml(CREFSTRING layerId, CREFSTRING csWkt = LL84_WKT,
+                   double minX = 0, double minY = 0, double maxX = FIXTURE_SIZE, double maxY = FIXTURE_SIZE)
+{
+    std::ostringstream xml;
+    xml.precision(17);
+    xml << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+        << "<MapDefinition xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+        << " xsi:noNamespaceSchemaLocation=\"MapDefinition-3.0.0.xsd\" version=\"3.0.0\">"
+        << "<Name>RasterStylization</Name>"
+        << "<CoordinateSystem>" << Narrow(csWkt) << "</CoordinateSystem>"
+        << "<Extents><MinX>" << minX << "</MinX><MaxX>" << maxX << "</MaxX>"
+        << "<MinY>" << minY << "</MinY><MaxY>" << maxY << "</MaxY></Extents>"
+        << "<BackgroundColor>FFFFFFFF</BackgroundColor>"
+        << "<MapLayer>"
+        << "<Name>Raster</Name>"
+        << "<ResourceId>" << Narrow(layerId) << "</ResourceId>"
+        << "<Selectable>false</Selectable>"
+        << "<ShowInLegend>false</ShowInLegend>"
+        << "<LegendLabel>Raster</LegendLabel>"
+        << "<ExpandInLegend>false</ExpandInLegend>"
+        << "<Visible>true</Visible>"
+        << "<Group/>"
+        << "</MapLayer>"
+        << "</MapDefinition>";
+    return xml.str();
+}
+
+// Publishes the feature source and returns "schema:class" for the grid layer,
+// plus the name of the raster property.
+STRING PublishFeatureSource(MgResourceService* svcResource,
+                            MgFeatureService* svcFeature,
+                            CREFSTRING featureSourceId,
+                            const std::string& filePath,
+                            REFSTRING rasterProperty)
+{
+    PublishResource(svcResource, featureSourceId, FeatureSourceXml(filePath));
+
+    Ptr<MgResourceIdentifier> fsRes = new MgResourceIdentifier(featureSourceId);
+    Ptr<MgFeatureSchemaCollection> schemas = svcFeature->DescribeSchema(fsRes, L"", NULL);
+    REQUIRE(schemas->GetCount() > 0);
+
+    Ptr<MgFeatureSchema> schema = schemas->GetItem(0);
+    Ptr<MgClassDefinition> klass = schema->GetClasses()->GetItem(0);
+
+    Ptr<MgPropertyDefinitionCollection> properties = klass->GetProperties();
+    for (INT32 i = 0; i < properties->GetCount() && rasterProperty.empty(); ++i)
+    {
+        Ptr<MgPropertyDefinition> property = properties->GetItem(i);
+        if (property->GetPropertyType() == MgFeaturePropertyType::RasterProperty)
+            rasterProperty = property->GetName();
+    }
+    REQUIRE(!rasterProperty.empty());
+
+    STRING featureName = schema->GetName();
+    featureName += L":";
+    featureName += klass->GetName();
+    return featureName;
+}
+
+// Renders the map and returns the decoded image; the caller owns the buffer.
+// If saveTo is not empty the PNG is also written there.
+unsigned int* RenderPng(MgRenderingService* svcRendering, MgMap* map, int& width, int& height,
+                        CREFSTRING saveTo = L"")
+{
+    Ptr<MgByteReader> png = svcRendering->RenderMap(map, NULL, L"PNG");
+    REQUIRE(png.p != NULL);
+
+    if (!saveTo.empty())
+    {
+        // ../UnitTestFiles/Results is created by the test run's setup hook.
+        STRING path = L"../UnitTestFiles/Results/";
+        path += saveTo;
+        png->ToFile(path);
+    }
+
+    Ptr<MgByteSink> sink = new MgByteSink(png);
+    Ptr<MgByte> bytes = sink->ToBuffer();
+    REQUIRE(bytes->GetLength() > 0);
+
+    return AGGImageIO::DecodePNG(bytes->Bytes(), (size_t)bytes->GetLength(), width, height);
+}
+
+// Absolute path of a result image, for the message that tells a human where to
+// look after the run.
+STRING ResultPath(CREFSTRING fileName)
+{
+    char cwd[1024] = { 0 };
+    if (NULL == ACE_OS::getcwd(cwd, sizeof(cwd)))
+        return fileName;
+
+    STRING path = MgUtil::MultiByteToWideChar(std::string(cwd));
+    path += L"/../UnitTestFiles/Results/";
+    path += fileName;
+    return path;
+}
+
+// Extent and pixel size of the raster in a feature source, taken from the
+// provider through a plain feature select of the raster property.
+bool GetRasterFootprint(MgFeatureService* svcFeature, CREFSTRING featureSourceId,
+                        CREFSTRING featureName, CREFSTRING rasterProperty,
+                        Ptr<MgEnvelope>& extent, INT32& imageXSize, INT32& imageYSize)
+{
+    bool ok = false;
+
+    Ptr<MgResourceIdentifier> fsRes = new MgResourceIdentifier(featureSourceId);
+    Ptr<MgFeatureQueryOptions> options = new MgFeatureQueryOptions();
+    options->AddFeatureProperty(rasterProperty);
+    Ptr<MgFeatureReader> reader = svcFeature->SelectFeatures(fsRes, featureName, options);
+    if (reader.p != NULL && reader->ReadNext())
+    {
+        Ptr<MgRaster> raster = reader->GetRaster(rasterProperty);
+        if (raster.p != NULL)
+        {
+            extent = raster->GetBounds();
+            imageXSize = raster->GetImageXSize();
+            imageYSize = raster->GetImageYSize();
+            ok = (extent.p != NULL && imageXSize > 0 && imageYSize > 0);
+        }
+        reader->Close();
+    }
+    return ok;
+}
+
+// The coordinate system the provider exposes for a raster feature source, or an
+// empty string when the file carries no georeferencing.
+STRING GetRasterCoordinateSystem(MgFeatureService* svcFeature, CREFSTRING featureSourceId)
+{
+    STRING wkt;
+
+    Ptr<MgResourceIdentifier> fsRes = new MgResourceIdentifier(featureSourceId);
+    Ptr<MgSpatialContextReader> reader = svcFeature->GetSpatialContexts(fsRes, false);
+    if (reader.p != NULL && reader->ReadNext())
+        wkt = reader->GetCoordinateSystemWkt();
+    if (reader.p != NULL)
+        reader->Close();
+
+    return wkt;
+}
+
+}
+
+TEST_CASE("Raster grid styling", "[RasterStylization]")
+{
+    Ptr<MgServerSiteService> svcSite = TestServiceFactory::CreateSiteService();
+    Ptr<MgSiteConnection> siteConn = TestServiceFactory::CreateSiteConnection(svcSite);
+    Ptr<MgResourceService> svcResource = TestServiceFactory::CreateResourceService();
+    Ptr<MgRenderingService> svcRendering = TestServiceFactory::CreateRenderingService();
+    Ptr<MgFeatureService> svcFeature = dynamic_cast<MgFeatureService*>(
+        MgServiceManager::GetInstance()->RequestService(MgServiceType::FeatureService));
+
+    for (const Fixture& fixture : FIXTURES)
+    {
+        INFO("fixture: " << fixture.file << " (" << fixture.note << ")");
+
+        STRING name = MgUtil::MultiByteToWideChar(std::string(fixture.file));
+        STRING::size_type dot = name.find_last_of(L'.');
+        if (dot != STRING::npos)
+            name = name.substr(0, dot);
+
+        STRING fsId = L"Library://RasterStylization/Data/" + name + L".FeatureSource";
+        STRING layerId = L"Library://RasterStylization/Layers/" + name + L".LayerDefinition";
+        STRING mapId = L"Library://RasterStylization/Maps/" + name + L".MapDefinition";
+
+        STRING rasterProperty;
+        STRING featureName = PublishFeatureSource(svcResource, svcFeature, fsId,
+                                                  Narrow(FixturePath(fixture.file)),
+                                                  rasterProperty);
+        PublishResource(svcResource, layerId, LayerXml(fsId, featureName, rasterProperty));
+        PublishResource(svcResource, mapId, MapXml(layerId));
+
+        Ptr<MgResourceIdentifier> mapRes = new MgResourceIdentifier(mapId);
+        Ptr<MgMap> map = new MgMap(siteConn);
+        map->Create(mapRes, L"RasterStylizationMap");
+
+        // 1 raster pixel == 1 image pixel. The renderer derives pixels per map
+        // unit from metersPerUnit * dpi / (METERS_PER_INCH * scale).
+        const double dpi = 96.0;
+        const double metersPerInch = 0.0254;
+        Ptr<MgCoordinate> centre = new MgCoordinateXY(FIXTURE_SIZE / 2.0, FIXTURE_SIZE / 2.0);
+        Ptr<MgPoint> centrePoint = new MgPoint(centre);
+        map->SetViewCenter(centrePoint);
+        map->SetDisplayDpi((INT32)dpi);
+        map->SetDisplayWidth(FIXTURE_SIZE);
+        map->SetDisplayHeight(FIXTURE_SIZE);
+        map->SetViewScale(map->GetMetersPerUnit() * dpi / metersPerInch);
+
+        int width = 0;
+        int height = 0;
+        unsigned int* rawPixels = RenderPng(svcRendering, map, width, height);
+        REQUIRE(rawPixels != NULL);
+        // DecodePNG hands back a buffer that has to be released with delete[].
+        std::unique_ptr<unsigned char[]> pixels(reinterpret_cast<unsigned char*>(rawPixels));
+        REQUIRE(width == FIXTURE_SIZE);
+        REQUIRE(height == FIXTURE_SIZE);
+
+        for (int quadrant = 0; quadrant < 4; ++quadrant)
+        {
+            int x = QUADRANT_CENTRE_X[quadrant];
+            int y = QUADRANT_CENTRE_Y[quadrant];
+            // DecodePNG returns the image bottom up.
+            unsigned int argb = rawPixels[(height - 1 - y) * width + x];
+            unsigned int rgb = argb & 0x00FFFFFF;
+            INFO("quadrant " << quadrant << " at (" << x << "," << y << ") got 0x"
+                 << std::hex << rgb << " expected 0x" << fixture.rgb[quadrant]);
+            CHECK(rgb == fixture.rgb[quadrant]);
+        }
+
+        // Same reason as in the real raster case: release this fixture's pooled
+        // connection now rather than leave it to be unwound at server shutdown.
+        MgFdoConnectionManager::GetInstance()->ClearCache();
+    }
+}
+
+TEST_CASE("Real raster rendering", "[RasterStylization]")
+{
+    Ptr<MgServerSiteService> svcSite = TestServiceFactory::CreateSiteService();
+    Ptr<MgSiteConnection> siteConn = TestServiceFactory::CreateSiteConnection(svcSite);
+    Ptr<MgResourceService> svcResource = TestServiceFactory::CreateResourceService();
+    Ptr<MgRenderingService> svcRendering = TestServiceFactory::CreateRenderingService();
+    Ptr<MgFeatureService> svcFeature = dynamic_cast<MgFeatureService*>(
+        MgServiceManager::GetInstance()->RequestService(MgServiceType::FeatureService));
+
+    const double dpi = 96.0;
+    const double metersPerInch = 0.0254;
+
+    for (const RealFixture& fixture : REAL_FIXTURES)
+    {
+        INFO("real fixture: " << fixture.file << " (" << fixture.note << ")");
+
+        STRING name = MgUtil::MultiByteToWideChar(std::string(fixture.file));
+        STRING::size_type dot = name.find_last_of(L'.');
+        if (dot != STRING::npos)
+            name = name.substr(0, dot);
+
+        STRING fsId = L"Library://RasterStylizationReal/Data/" + name + L".FeatureSource";
+        STRING layerId = L"Library://RasterStylizationReal/Layers/" + name + L".LayerDefinition";
+        STRING mapId = L"Library://RasterStylizationReal/Maps/" + name + L".MapDefinition";
+
+        STRING rasterProperty;
+        STRING featureName = PublishFeatureSource(svcResource, svcFeature, fsId,
+                                                  Narrow(FixturePath(fixture.file)),
+                                                  rasterProperty);
+
+        // The raster's own footprint drives the map view, so the image shows the
+        // whole file with one image pixel per raster pixel wherever possible.
+        Ptr<MgEnvelope> rasterExtent;
+        INT32 imageXSize = 0;
+        INT32 imageYSize = 0;
+        REQUIRE(GetRasterFootprint(svcFeature, fsId, featureName, rasterProperty,
+                                   rasterExtent, imageXSize, imageYSize));
+
+        double minX = rasterExtent->GetLowerLeftCoordinate()->GetX();
+        double minY = rasterExtent->GetLowerLeftCoordinate()->GetY();
+        double maxX = rasterExtent->GetUpperRightCoordinate()->GetX();
+        double maxY = rasterExtent->GetUpperRightCoordinate()->GetY();
+
+        int factor = (std::max)(imageXSize, imageYSize) / REAL_RENDER_MAX_DIM;
+        if (factor < 1)
+            factor = 1;
+        int renderWidth = imageXSize / factor;
+        int renderHeight = imageYSize / factor;
+        if (renderWidth < 1) renderWidth = 1;
+        if (renderHeight < 1) renderHeight = 1;
+
+        // Rasters written without an SRS have no coordinate system to adopt, so
+        // the map falls back to LL84 and the file's own units become map units.
+        STRING csWkt = GetRasterCoordinateSystem(svcFeature, fsId);
+        if (csWkt.empty())
+            csWkt = LL84_WKT;
+
+        PublishResource(svcResource, layerId,
+                        LayerXml(fsId, featureName, rasterProperty, fixture.style,
+                                 fixture.lowBand, fixture.highBand));
+        PublishResource(svcResource, mapId,
+                        MapXml(layerId, csWkt, minX, minY, maxX, maxY));
+
+        Ptr<MgResourceIdentifier> mapRes = new MgResourceIdentifier(mapId);
+        Ptr<MgMap> map = new MgMap(siteConn);
+        map->Create(mapRes, L"RasterStylizationMap");
+
+        Ptr<MgCoordinate> centre = new MgCoordinateXY((minX + maxX) / 2.0, (minY + maxY) / 2.0);
+        Ptr<MgPoint> centrePoint = new MgPoint(centre);
+        map->SetViewCenter(centrePoint);
+        map->SetDisplayDpi((INT32)dpi);
+        map->SetDisplayWidth(renderWidth);
+        map->SetDisplayHeight(renderHeight);
+        // One image pixel per (imageXSize / renderWidth) raster pixels.
+        map->SetViewScale(((maxX - minX) / renderWidth) * map->GetMetersPerUnit() * dpi / metersPerInch);
+
+        STRING resultFile = name + L".png";
+        int width = 0;
+        int height = 0;
+        unsigned int* rawPixels = RenderPng(svcRendering, map, width, height, resultFile);
+        REQUIRE(rawPixels != NULL);
+        std::unique_ptr<unsigned char[]> pixels(reinterpret_cast<unsigned char*>(rawPixels));
+        REQUIRE(width == renderWidth);
+        REQUIRE(height == renderHeight);
+
+        ACE_DEBUG((LM_INFO, ACE_TEXT("  wrote %W (%dx%d from %dx%d)\n"),
+                   ResultPath(resultFile).c_str(), width, height, imageXSize, imageYSize));
+
+        // Sanity checks only, since there is no expected image to compare
+        // against. Real imagery has hundreds of colours and a wide spread of
+        // luminance; a raster that failed to read produces a flat fill.
+        std::set<unsigned int> colours;
+        double sum = 0.0;
+        double sumSquares = 0.0;
+        for (int i = 0; i < width * height; ++i)
+        {
+            unsigned int rgb = rawPixels[i] & 0x00FFFFFF;
+            colours.insert(rgb);
+            double luminance = 0.299 * ((rgb >> 16) & 0xFF)
+                             + 0.587 * ((rgb >> 8) & 0xFF)
+                             + 0.114 * (rgb & 0xFF);
+            sum += luminance;
+            sumSquares += luminance * luminance;
+        }
+        const double pixelCount = (double)width * height;
+        const double mean = sum / pixelCount;
+        const double deviation = sqrt((std::max)(0.0, sumSquares / pixelCount - mean * mean));
+
+        INFO("rendered " << colours.size() << " distinct colours, luminance mean "
+             << mean << " stddev " << deviation);
+        CHECK(colours.size() >= 16);
+        CHECK(deviation >= 2.0);
+
+        // Release the pooled raster connection now. Leaving a dozen of them to
+        // be unwound when the server shuts down intermittently segfaults or
+        // hangs after the run has finished (pre-existing teardown behaviour,
+        // reproducible with this suite alone but not with one of its cases).
+        MgFdoConnectionManager::GetInstance()->ClearCache();
+    }
+}

Added: branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp	                        (rev 0)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestStylization.cpp	2026-10-23 11:25:59 UTC (rev 10225)
@@ -0,0 +1,324 @@
+//
+//  Copyright (C) 2004-2026 by Autodesk, Inc.
+//
+//  This library is free software; you can redistribute it and/or
+//  modify it under the terms of version 2.1 of the GNU Lesser
+//  General Public License as published by the Free Software Foundation.
+//
+//  This library is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+//  Lesser General Public License for more details.
+//
+//  You should have received a copy of the GNU Lesser General Public
+//  License along with this library; if not, write to the Free Software
+//  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+//
+
+#include "GridData.h"
+#include "Band.h"
+#include "Bounds.h"
+#include "Point2D.h"
+#include "RS_Raster.h"
+#include "RS_InputStream.h"
+#include "catch.hpp"
+
+#include <Fdo/Raster/RasterDataModelType.h>
+#include <Fdo/Raster/RasterDataType.h>
+#include <Fdo/Schema/DataType.h>
+
+#include <cfloat>
+#include <cstring>
+#include <vector>
+
+namespace
+{
+
+// Minimal RS_InputStream over a fixed byte buffer.
+class TestRasterStream : public RS_InputStream
+{
+public:
+    explicit TestRasterStream(const std::vector<unsigned char>& bytes)
+        : m_bytes(bytes), m_pos(0)
+    {
+    }
+
+    size_t available() const { return m_bytes.size() - m_pos; }
+
+    size_t read(void* pBuffer, size_t nBytesToRead)
+    {
+        size_t n = (nBytesToRead < available())? nBytesToRead : available();
+        if (n > 0)
+            memcpy(pBuffer, &m_bytes[m_pos], n);
+        m_pos += n;
+        return n;
+    }
+
+    off_t seek(int eOrigin, off_t nOffset)
+    {
+        off_t old = (off_t)m_pos;
+        off_t base = (SEEK_CUR == eOrigin)? old : (SEEK_END == eOrigin)? (off_t)m_bytes.size() : 0;
+        m_pos = (size_t)(base + nOffset);
+        return old;
+    }
+
+private:
+    std::vector<unsigned char> m_bytes;
+    size_t m_pos;
+};
+
+// An RS_Raster over a fixed buffer, laid out the way OSGeo.GDAL hands a raster
+// to GridData::ReadRaster: pixel interleaved, RGB band order, and samples in
+// the native (host) byte order because the provider asks GDAL for the source
+// data type with GDALDatasetRasterIO.
+class TestRaster : public RS_Raster
+{
+public:
+    TestRaster(int bpp,
+               int dataModelType,
+               int dataType,
+               const std::vector<unsigned char>& bytes,
+               const std::vector<unsigned char>& palette,
+               unsigned long cols,
+               unsigned long rows,
+               int nullValueType = 0,
+               int64_t nullValueData = 0)
+        : m_bpp(bpp),
+          m_dataModelType(dataModelType),
+          m_dataType(dataType),
+          m_bytes(bytes),
+          m_palette(palette),
+          m_cols(cols),
+          m_rows(rows),
+          m_nullValueType(nullValueType),
+          m_nullValueData(nullValueData)
+    {
+    }
+
+    RS_Bounds GetExtent()
+    {
+        return RS_Bounds(0.0, 0.0, (double)m_cols, (double)m_rows);
+    }
+
+    int GetOriginalWidth() { return (int)m_cols; }
+    int GetOriginalHeight() { return (int)m_rows; }
+    int GetBitsPerPixel() { return m_bpp; }
+    int GetDataModelType() { return m_dataModelType; }
+    int GetDataType() { return m_dataType; }
+    int GetNullValueType() { return m_nullValueType; }
+    int64_t GetNullValueData() { return m_nullValueData; }
+    RS_String GetVerticalUnits() { return RS_String(); }
+
+    RS_InputStream* GetPalette()
+    {
+        return m_palette.empty()? NULL : new TestRasterStream(m_palette);
+    }
+
+    RS_InputStream* GetStream(RS_ImageFormat, int, int)
+    {
+        return new TestRasterStream(m_bytes);
+    }
+
+private:
+    int m_bpp;
+    int m_dataModelType;
+    int m_dataType;
+    std::vector<unsigned char> m_bytes;
+    std::vector<unsigned char> m_palette;
+    unsigned long m_cols;
+    unsigned long m_rows;
+    int m_nullValueType;
+    int64_t m_nullValueData;
+};
+
+std::vector<unsigned char> RepeatPixel(const unsigned char* pixel, size_t size, size_t count)
+{
+    std::vector<unsigned char> bytes;
+    bytes.reserve(size * count);
+    for (size_t i = 0; i < count; ++i)
+        bytes.insert(bytes.end(), pixel, pixel + size);
+    return bytes;
+}
+
+// Runs a raster through GridData::ReadRaster and reads back pixel (0, 0) of the
+// colour band, returning false when ReadRaster did not create one.
+bool ReadColourBandPixel(const std::vector<unsigned char>& bytes,
+                         int bpp,
+                         int dataModelType,
+                         unsigned long cols,
+                         unsigned long rows,
+                         bool alignment,
+                         const std::vector<unsigned char>& palette,
+                         unsigned int& color)
+{
+    GridData grid(Point2D(0.0, 0.0), (double)cols, (double)rows, (unsigned int)cols, (unsigned int)rows);
+    TestRaster raster(bpp, dataModelType, FdoRasterDataType_UnsignedInteger, bytes, palette, cols, rows);
+
+    grid.ReadRaster(&raster, L"1", cols, rows, 0.0, 0.0, (double)cols, (double)rows, alignment);
+
+    Band* band = grid.GetBand((unsigned int)0);
+    if (NULL == band)
+        return false;
+
+    return band->GetValue(0, 0, Band::UnsignedInt32, &color);
+}
+
+// The read path rounds each row up to a 64 bit boundary when it is told the
+// provider pads its rows, and gives up when the stream is shorter than that.
+// This is behavioural baseline: it holds on Windows and Linux, before and after
+// the band accessor work.
+TEST_CASE("Grid colour band row alignment", "[Stylization]")
+{
+    const std::vector<unsigned char> noPalette;
+    unsigned int color = 0;
+
+    SECTION("48 bpp RGB with a row length the provider does not pad produces no band")
+    {
+        // 6 pixels * 6 bytes = 36 bytes per row, but bAlignment rounds the
+        // buffer GridData expects up to 40 bytes, so it bails out.
+        const unsigned char pixel[] = { 0x34, 0x12, 0x78, 0x56, 0xBC, 0x9A };
+
+        REQUIRE_FALSE(ReadColourBandPixel(RepeatPixel(pixel, 6, 24), 48, FdoRasterDataModelType_RGB,
+                                          6, 4, true, noPalette, color));
+    }
+}
+
+// Band creation and the narrow (8 and 16 bit) accessors are implemented the
+// same way on both platforms, so they are baseline behaviour. The 32 and 64 bit
+// accessors are covered by the change that implements them on Linux.
+TEST_CASE("Band creation", "[Stylization]")
+{
+    GridData grid(Point2D(0.0, 0.0), 3.0, 3.0, 3, 3);
+
+    SECTION("UnsignedInt32") { REQUIRE(NULL != grid.CreateBand(Band::UnsignedInt32, L"u32")); }
+    SECTION("Int32")         { REQUIRE(NULL != grid.CreateBand(Band::Int32, L"i32")); }
+    SECTION("Double32")      { REQUIRE(NULL != grid.CreateBand(Band::Double32, L"f32")); }
+    SECTION("UnsignedInt64") { REQUIRE(NULL != grid.CreateBand(Band::UnsignedInt64, L"u64")); }
+    SECTION("Int64")         { REQUIRE(NULL != grid.CreateBand(Band::Int64, L"i64")); }
+    SECTION("Double64")      { REQUIRE(NULL != grid.CreateBand(Band::Double64, L"f64")); }
+    SECTION("Boolean")       { REQUIRE(NULL != grid.CreateBand(Band::Boolean, L"b")); }
+    SECTION("Bit2")          { REQUIRE(NULL != grid.CreateBand(Band::Bit2, L"b2")); }
+    SECTION("Bit4")          { REQUIRE(NULL != grid.CreateBand(Band::Bit4, L"b4")); }
+}
+
+TEST_CASE("Band numeric accessors for narrow types", "[Stylization]")
+{
+    GridData grid(Point2D(0.0, 0.0), 3.0, 3.0, 3, 3);
+
+    SECTION("UnsignedInt8")
+    {
+        Band* band = grid.CreateBand(Band::UnsignedInt8, L"u8");
+        REQUIRE(NULL != band);
+
+        INT8 value = 100;
+        REQUIRE(band->SetValue(1, 1, Band::UnsignedInt8, &value));
+
+        INT8 readBack = 0;
+        REQUIRE(band->GetValue(1, 1, Band::UnsignedInt8, &readBack));
+        REQUIRE(value == readBack);
+        REQUIRE(100.0 == band->GetValueAsDouble(1, 1));
+    }
+
+    SECTION("UnsignedInt8 above 0x7F reads back negative")
+    {
+        // Pre-existing behaviour, recorded so a change to it is visible: the
+        // unsigned 8 bit case in Band::GetValueAsDouble loads the sample into an
+        // INT8, so anything from 0x80 up is sign extended. This is why paletted
+        // and grey rasters with high sample values behave oddly.
+        Band* band = grid.CreateBand(Band::UnsignedInt8, L"u8high");
+        REQUIRE(NULL != band);
+
+        INT8 value = (INT8)200;
+        REQUIRE(band->SetValue(1, 1, Band::UnsignedInt8, &value));
+
+        INT8 readBack = 0;
+        REQUIRE(band->GetValue(1, 1, Band::UnsignedInt8, &readBack));
+        REQUIRE(value == readBack);
+        REQUIRE(-56.0 == band->GetValueAsDouble(1, 1));
+    }
+
+    SECTION("Int8")
+    {
+        Band* band = grid.CreateBand(Band::Int8, L"i8");
+        REQUIRE(NULL != band);
+
+        INT8 value = -100;
+        REQUIRE(band->SetValue(2, 0, Band::Int8, &value));
+
+        INT8 readBack = 0;
+        REQUIRE(band->GetValue(2, 0, Band::Int8, &readBack));
+        REQUIRE(value == readBack);
+        REQUIRE(-100.0 == band->GetValueAsDouble(2, 0));
+    }
+
+    SECTION("UnsignedInt16")
+    {
+        Band* band = grid.CreateBand(Band::UnsignedInt16, L"u16");
+        REQUIRE(NULL != band);
+
+        INT16 value = (INT16)60000;
+        REQUIRE(band->SetValue(0, 2, Band::UnsignedInt16, &value));
+
+        INT16 readBack = 0;
+        REQUIRE(band->GetValue(0, 2, Band::UnsignedInt16, &readBack));
+        REQUIRE(value == readBack);
+        REQUIRE(60000.0 == band->GetValueAsDouble(0, 2));
+    }
+
+    SECTION("Int16")
+    {
+        Band* band = grid.CreateBand(Band::Int16, L"i16");
+        REQUIRE(NULL != band);
+
+        INT16 value = -12345;
+        REQUIRE(band->SetValue(1, 0, Band::Int16, &value));
+
+        INT16 readBack = 0;
+        REQUIRE(band->GetValue(1, 0, Band::Int16, &readBack));
+        REQUIRE(value == readBack);
+        REQUIRE(-12345.0 == band->GetValueAsDouble(1, 0));
+    }
+
+    SECTION("Boolean")
+    {
+        Band* band = grid.CreateBand(Band::Boolean, L"bool");
+        REQUIRE(NULL != band);
+
+        INT8 value = 1;
+        REQUIRE(band->SetValue(0, 0, Band::Boolean, &value));
+
+        INT8 readBack = 0;
+        REQUIRE(band->GetValue(0, 0, Band::Boolean, &readBack));
+        REQUIRE(value == readBack);
+        REQUIRE(1.0 == band->GetValueAsDouble(0, 0));
+    }
+
+    SECTION("Bit2")
+    {
+        Band* band = grid.CreateBand(Band::Bit2, L"bit2");
+        REQUIRE(NULL != band);
+
+        INT8 value = 3;
+        REQUIRE(band->SetValue(2, 2, Band::Bit2, &value));
+
+        INT8 readBack = 0;
+        REQUIRE(band->GetValue(2, 2, Band::Bit2, &readBack));
+        REQUIRE(value == readBack);
+        REQUIRE(3.0 == band->GetValueAsDouble(2, 2));
+    }
+
+    SECTION("Bit4")
+    {
+        Band* band = grid.CreateBand(Band::Bit4, L"bit4");
+        REQUIRE(NULL != band);
+
+        INT8 value = 9;
+        REQUIRE(band->SetValue(1, 2, Band::Bit4, &value));
+
+        INT8 readBack = 0;
+        REQUIRE(band->GetValue(1, 2, Band::Bit4, &readBack));
+        REQUIRE(value == readBack);
+        REQUIRE(9.0 == band->GetValueAsDouble(1, 2));
+    }
+}
+}

Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.cpp	2026-10-23 11:25:59 UTC (rev 10225)
@@ -47,6 +47,7 @@
             TestServiceFactory::AddSuiteToRun("[Misc]");
             TestServiceFactory::AddSuiteToRun("[Performance]");
             TestServiceFactory::AddSuiteToRun("[RenderingService]");
+            TestServiceFactory::AddSuiteToRun("[RasterStylization]");
             TestServiceFactory::AddSuiteToRun("[ResourceService]");
             TestServiceFactory::AddSuiteToRun("[ServerAdminService]");
             TestServiceFactory::AddSuiteToRun("[ServerManager]");
@@ -53,6 +54,7 @@
             TestServiceFactory::AddSuiteToRun("[ServiceManager]");
             TestServiceFactory::AddSuiteToRun("[SiteManager]");
             TestServiceFactory::AddSuiteToRun("[SiteService]");
+            TestServiceFactory::AddSuiteToRun("[Stylization]");
             TestServiceFactory::AddSuiteToRun("[TileService]");
             TestServiceFactory::AddSuiteToRun("[ProfilingService]");
             TestServiceFactory::AddSuiteToRun("[TransformMesh]");
@@ -75,6 +77,7 @@
             ACE_OS::printf("  Misc\n");
             ACE_OS::printf("  Performance\n");
             ACE_OS::printf("  RenderingService\n");
+            ACE_OS::printf("  RasterStylization\n");
             ACE_OS::printf("  ResourceService\n");
             ACE_OS::printf("  ServerAdminService\n");
             ACE_OS::printf("  ServerManager\n");
@@ -81,6 +84,7 @@
             ACE_OS::printf("  ServiceManager\n");
             ACE_OS::printf("  SiteManager\n");
             ACE_OS::printf("  SiteService\n");
+            ACE_OS::printf("  Stylization\n");
             ACE_OS::printf("  TileService\n");
             ACE_OS::printf("  ProfilingService\n");
             ACE_OS::printf("  TransformMesh\n");

Modified: branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/Server/src/UnitTesting/UnitTesting.vcxproj	2026-10-23 11:25:59 UTC (rev 10225)
@@ -216,6 +216,7 @@
     <ClCompile Include="TestMisc.cpp" />
     <ClCompile Include="TestPerformance.cpp" />
     <ClCompile Include="TestProfilingService.cpp" />
+    <ClCompile Include="TestRasterStylization.cpp" />
     <ClCompile Include="TestRenderingService.cpp" />
     <ClCompile Include="TestResourceService.cpp" />
     <ClCompile Include="TestServerAdminService.cpp" />
@@ -224,6 +225,7 @@
     <ClCompile Include="TestServiceManager.cpp" />
     <ClCompile Include="TestSiteManager.cpp" />
     <ClCompile Include="TestSiteService.cpp" />
+    <ClCompile Include="TestStylization.cpp" />
     <ClCompile Include="TestTileService.cpp" />
     <ClCompile Include="TestTransformMesh.cpp" />
     <ClCompile Include="UnitTesting.cpp" />

Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md	                        (rev 0)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md	2026-10-23 11:25:59 UTC (rev 10225)
@@ -0,0 +1,339 @@
+# Provenance of the raster fixtures
+
+This directory holds two kinds of test data, both used by the
+`[RasterStylization]` suite in `Server/src/UnitTesting/TestRasterStylization.cpp`:
+
+| Files | Kind | Where it came from |
+|-------|------|--------------------|
+| `RasterStylization_*.tif` | Synthetic, purpose built | Generated by `make_fixtures.py` in this directory (first section below) |
+| `RealRaster_*` | Real world imagery and elevation | Copied from the FDO GDAL provider test data, one file regenerated with GDAL (second section below) |
+
+This file records where each came from, how to reproduce or re-derive them, and
+their licensing status.
+
+# Part 1 - `RasterStylization_*.tif` (synthetic)
+
+These ten GeoTIFFs are the input data for the first half of the suite. This part
+records where they came from, how to reproduce them byte for byte, and their
+licensing status.
+
+## Summary
+
+| Property | Value |
+|----------|-------|
+| Origin | Original, synthetic data generated for this repository |
+| Author | Authored as part of this change; written out by `make_fixtures.py` in this directory |
+| Third party content | None. No external, sampled, scanned or licensed data set is used or derived from |
+| Geographic accuracy | None. Values are arbitrary constants chosen to be recognisable, not to describe any real place |
+| Format | GeoTIFF, written by GDAL `GTiff` driver, uncompressed (`TILED=NO`) |
+| Size on disk | 4,460 to 33,156 bytes per file, 165,100 bytes total |
+| Reproducible | Yes, byte for byte (see [Reproducing](#reproducing)) |
+| Licence | Same as the rest of the MgDev tree (LGPL-2.1); no attribution required |
+
+The `RasterStylization_` filename prefix exists because `UnitTest/TestData` is
+copied flat into `<build>/Server/src/UnitTestFiles` by CMake, so the fixtures
+share one directory with all the other test data.
+
+## Why they are not derived from the FDO GDAL provider test data
+
+The raster fixtures shipped with the FDO GDAL provider
+(`fdo/Providers/GDAL/TestData`) were inventoried first:
+
+| Directory | What it holds | Why it is not enough |
+|-----------|---------------|----------------------|
+| `RGB/` | `RGB.bmp`, 200x200, 3 x Byte | 24 bpp only, and a BMP carries no georeferencing |
+| `band/` | `ae.tif` (513x516 Byte, Gray), JPEG + world files, band configuration XML | 8 bpp grey only |
+| `GeoTiff/`, `DEM/`, `DTED/`, `Jp2/`, `MrSID/`, `Ecw/`, `VRT/`, `rotated/`, ... | assorted real-world imagery | no 16 bit per channel RGB, no RGBA, no small-index palette, and no per-quadrant constant areas to sample |
+
+None of them can drive the 48 bpp, 32 bpp RGBA or palette branches of
+`GridData::ReadRaster` with a value that can be asserted exactly, and several
+are in formats that need third party codecs. Generating purpose-built fixtures
+was therefore cheaper and more precise than transforming them, and it keeps the
+new test data free of any third party licence obligation.
+
+## What the files contain
+
+Every fixture is 64x64 pixels, split into four 32x32 quadrants of one constant
+value so the rendered map can be sampled at the centre of each quadrant:
+
+| Quadrant | Position | Sample point |
+|----------|----------|--------------|
+| 0 | top-left (north-west) | (16, 16) |
+| 1 | top-right (north-east) | (48, 16) |
+| 2 | bottom-left (south-west) | (16, 48) |
+| 3 | bottom-right (south-east) | (48, 48) |
+
+Georeferencing, identical for all ten files: origin `(0, 64)`, pixel size
+`(1, -1)` degree, `AREA_OR_POINT=Area`, and a WGS84 geographic CRS written
+through the GeoTIFF keys. GDAL reports that CRS as EPSG:4326 and `gdalinfo`
+warns that the GeoTIFF-key definition is not byte-identical to the EPSG registry
+entry; that is harmless here, because the map definition in the test uses the
+same WKT and nothing is reprojected.
+
+The per-channel constants are:
+
+| Quadrant | 16 bit RGB sample | written to 8 bit fixtures as | alpha |
+|----------|-------------------|------------------------------|-------|
+| 0 | `0x1234 0x5678 0x9ABC` | `0x12 0x56 0x9A` | `0x80` |
+| 1 | `0xFFFF 0x0000 0x00FF` | `0xFF 0x00 0x00` | `0xFF` |
+| 2 | `0x0000 0xFFFF 0x0100` | `0x00 0xFF 0x01` | `0x00` |
+| 3 | `0x8080 0x4040 0x1010` | `0x80 0x40 0x10` | `0x40` |
+
+The high and low bytes of every 16 bit sample differ, which is what makes the
+48 bpp packing bug visible: a correct implementation renders the high byte, a
+broken one renders the low byte or nothing at all.
+
+| File | Bands | Sample type | Raster data model / bpp | Single band values (quadrants 0-3) |
+|------|-------|-------------|-------------------------|-----------------------------------|
+| `RasterStylization_rgb24.tif` | 3 | `Byte` | RGB / 24 | - |
+| `RasterStylization_rgba32.tif` | 4 | `Byte` | RGBA / 32 | - |
+| `RasterStylization_rgb48.tif` | 3 | `UInt16` | RGB / 48 | - |
+| `RasterStylization_rgb48_int16.tif` | 3 | `Int16` | RGB / 48 | - |
+| `RasterStylization_palette8.tif` | 1 + 256 entry colour table | `Byte` | Palette / 8 | indices `0,1,2,3` |
+| `RasterStylization_gray8.tif` | 1 | `Byte` | Gray / 8 | `0x12 0x34 0x56 0x78` |
+| `RasterStylization_gray16.tif` | 1 | `UInt16` | Gray / 16 | `0x1234 0x5678 0x9ABC 0x0100` |
+| `RasterStylization_gray32i.tif` | 1 | `Int32` | Gray / 32 | `0x00123456 0x00234567 0x00345678 0x00456789` |
+| `RasterStylization_gray32f.tif` | 1 | `Float32` | Gray / 32 | `4660 22136 39612 256` |
+| `RasterStylization_gray64f.tif` | 1 | `Float64` | Gray / 64 | `4660 22136 39612 256` |
+
+The palette is 256 entries long because GDAL always writes a full table; entries
+0-3 are `(0x12,0x56,0x9A,255)`, `(0x00,0x00,0xFF,255)`, `(0x00,0xFF,0x01,255)`
+and `(0x80,0x40,0x10,255)`, and entries 4-255 are opaque black `(0,0,0,255)`.
+Colour interpretation is `Red/Green/Blue[/Alpha]` for the colour fixtures,
+`PaletteIndex` for the palette fixture and `Gray` for the single band fixtures.
+
+The GDAL provider maps a file onto one of these models from its band layout
+(`FdoRfpBandRaster::GetDataModel`), which is why this particular set of layouts
+was chosen - it covers every branch of `GridData::ReadRaster` that a file can
+reach. `FdoRasterDataModelType_Data` and the signed/float band types cannot be
+reached from a file at all, because the provider only ever reports
+`UnsignedInteger`; the `[Stylization]` unit suite covers those directly.
+
+64 bpp RGBA (4 x `UInt16` with alpha) is deliberately **not** part of this set:
+`ReadRaster` writes a 32 bpp row into the 64 bpp band it creates for it, and the
+band's fill value is taken from a 4 byte local while the band is 8 bytes wide, so
+what such a file renders depends on uninitialised stack bytes.
+
+## Generator
+
+`make_fixtures.py` (in this directory, checked in) writes all ten files with the
+GDAL Python bindings and numpy:
+
+```
+python3 make_fixtures.py
+```
+
+It was run with, and the checked-in files were produced by:
+
+| Tool | Version |
+|------|---------|
+| Python | 3.14.8 |
+| GDAL | 3.12.4 "Chicoutimi" (`gdal.VersionInfo('VERSION_NUM')` = 3120400) |
+| numpy | 2.4.6 |
+| Host | Linux x86_64 |
+
+No other tool touched the files. They are not hand-edited, hand-resampled or
+post-processed by any other program, and they must not be - regenerate them with
+the script instead.
+
+## Reproducing
+
+Regeneration is deterministic: the fixtures are uncompressed, carry no TIFF
+metadata domain (no `DateTime`, no `Software` tag), and every pixel is a
+constant, so a rerun on the toolchain above reproduces them byte for byte. This
+was verified by regenerating the set in a scratch directory and diffing
+`sha256sum` output against the checked-in copies.
+
+To re-verify the checked-in bytes, compare them with the inventory table below:
+
+```
+cd UnitTest/TestData/RasterStylization
+sha256sum RasterStylization_*.tif
+```
+
+To prove the generator is still in step with the binaries, regenerate into a
+scratch directory and compare:
+
+```
+mkdir /tmp/regen && cp make_fixtures.py /tmp/regen && (cd /tmp/regen && python3 make_fixtures.py)
+(cd /tmp/regen && sha256sum *.tif) | diff - <(sha256sum *.tif)
+```
+
+If a future change to the fixtures is intended, regenerate them with the script,
+update the table and the checksum listing here in the same commit, and rerun
+`./mgserver test RasterStylization` in both Release and Debug.
+
+A newer GDAL will not necessarily reproduce these bytes (block/strip sizing and
+the default TIFF writer version are not contractual), so treat the versions above
+as part of the fixture definition. Verifying that a regenerated file still
+renders the expected colours matters more than matching the checksum.
+
+## File inventory
+
+| File | Bytes | SHA-256 |
+|------|-------|---------|
+| `RasterStylization_gray16.tif` | 8,556 | `33822aac7899bf85439d755ed4a6878e114467cf97ec23bbc24da7a483cf11ac` |
+| `RasterStylization_gray32f.tif` | 16,756 | `f16aa5516124b0c14054df5f91849d1ba86a1c50c217e634f7060414c5959fae` |
+| `RasterStylization_gray32i.tif` | 16,756 | `d0c3a1ed88673e58e1d07807d4182b820bb1a21c52eed49601a81e65736fd1b2` |
+| `RasterStylization_gray64f.tif` | 33,156 | `162123d4c8bc924234786e119e1b1af8bfec9ae2cc05cfff02bc8261c2b510e2` |
+| `RasterStylization_gray8.tif` | 4,460 | `bca00976efb56623a1e9cd979f6c6f37fbd3f24c4c10de8d3d65c3cab8686c9c` |
+| `RasterStylization_palette8.tif` | 6,008 | `09b7317933f7e19ba9d2da8cd7cb4f8691773c34e71572934e90c8e75a3cd3fb` |
+| `RasterStylization_rgb24.tif` | 12,672 | `5630faeed0a0e8bc9982dc2f8c2fd830c02e9286fb9df61394a11909495a595c` |
+| `RasterStylization_rgb48.tif` | 24,976 | `6c940ddf2813797bc4dfcda497abe4114bc5766a8791902dc7476ff205611f90` |
+| `RasterStylization_rgb48_int16.tif` | 24,976 | `e25d0a97c13379c833eaad4e13b3348bf253b675567523b1641596f6f2320501` |
+| `RasterStylization_rgba32.tif` | 16,784 | `306283d1fc3aaa54458c28c4468217f9cce749572599e21471912611e806914e` |
+
+`make_fixtures.py` and this file are not test inputs and have no checksum
+requirement beyond normal version control.
+
+# Part 2 - `RealRaster_*` (real world data)
+
+The second half of the suite renders genuine imagery and elevation data through
+the same pipeline so a human can confirm the output is a plausible map rather
+than a corrupted or distorted raster. There is no visual regression system, so
+these files are not compared against a reference image; the test writes each
+render to `<build>/Server/src/UnitTestFiles/Results/RealRaster_<name>.png` for
+inspection and only asserts that the image is the requested size and has real
+variation in it.
+
+## Origin
+
+All four come from the FDO GDAL provider test data in the sibling FDO working
+copy, i.e. the upstream FDO repository
+
+    https://svn.osgeo.org/fdo/branches/4.2/Providers/GDAL/TestData/
+
+which ships as part of FDO under the same LGPL-2.1 terms as the rest of FDO.
+They are not third-party commercial basemaps: the FDO project collected them as
+provider test data, and they have been in that tree since 2006-2013 (SVN
+revisions below).
+
+Each file was re-derived from its upstream original with `gdal_translate`, which
+is lossless for these files - the transformations crop a few columns and, for
+the palette sheet, change container format. Two reasons, both explained under
+[Why the files were re-derived](#why-the-files-were-re-derived): the row width
+has to be 64-bit aligned for the grid stylizer to be reached at all, and the
+original palette file is a PNG the provider cannot select features from.
+
+| Local file | Upstream path | Upstream SHA-256 | Upstream SVN |
+|------------|---------------|------------------|--------------|
+| `RealRaster_ae.tif` | `band/ae.tif` | `9435777dcf7098e53285b8f544d163915598f2fafb7ea15beeadac2f7ba66b65` | r892, gregboone, 2006-06-14 |
+| `RealRaster_spif83.tif` | `band/spif83.tif` | `c287327f8f81df675d9c8c6dbe394b1887bb5773f7bf01a234e840030bda0900` | r892, gregboone, 2006-06-14 |
+| `RealRaster_mtjefferson.tif` | `DEM/MtJefferson.dem` | `c804b81c02740e388eafdf90640745fb40f108157f09ca2d5f2f1dcf05e53091` | r892, gregboone, 2006-06-14 |
+| `RealRaster_cm13ct.tif` | `PNG/CM13ct.png` | `caa341405e2d1c51b392b05b073103607483f02c13597f55786445b48c68074e` | r3622, gregboone, 2013-02-20 |
+
+`UnitTest/TestData` is copied flat into `<build>/Server/src/UnitTestFiles`, so
+these carry the `RealRaster_` prefix to keep them apart from the other test data
+and from the synthetic fixtures above.
+
+## What each file is
+
+| Local file | Size | Content |
+|------------|------|---------|
+| `RealRaster_ae.tif` | 512x516 px, 264,876 B | 8 bit grey scanned map sheet, 60 m pixels, projected in a custom coordinate system ("AE E000") |
+| `RealRaster_spif83.tif` | 512x512 px, 262,871 B | 8 bit grey scanned map sheet, ~195 m pixels, NAD83 / California zone 6 |
+| `RealRaster_cm13ct.tif` | 1944x3109 px, 2,287,461 B | 8 bit palette scanned topographic map, 10 m pixels, UTM zone 13N |
+| `RealRaster_mtjefferson.tif` | 336x469 px, 315,773 B | 16 bit integer USGS DEM (Mt Jefferson), 30 m pixels, UTM |
+
+Between them they cover the read paths a file can actually reach - grey 8 bit,
+grey 16 bit elevation and 8 bit palette - and three different coordinate
+systems, which is also what exercises the reprojection a real deployment does.
+The 8 bit grey pair are rendered with band 1 mapped to all three colour
+channels, the palette sheet as a packed colour band, and the DEM as a 0-3000 m
+elevation ramp, because elevation values are not colours.
+
+## How the files were re-derived
+
+```
+cd fdo/Providers/GDAL/TestData
+
+gdal_translate -of GTiff -srcwin 0 0 512 516 \
+    band/ae.tif <MgDev>/UnitTest/TestData/RasterStylization/RealRaster_ae.tif
+
+gdal_translate -of GTiff -srcwin 0 0 512 512 \
+    band/spif83.tif <MgDev>/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif
+
+gdal_translate -of GTiff -srcwin 0 0 1944 3109 -a_srs EPSG:32613 \
+    -co COMPRESS=DEFLATE -co PREDICTOR=2 \
+    PNG/CM13ct.png <MgDev>/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif
+
+gdal_translate -of GTiff -srcwin 0 0 336 469 \
+    DEM/MtJefferson.dem <MgDev>/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif
+```
+
+The crops keep the north-west corner, so the georeferencing of what remains is
+unchanged. Sample values, the colour table (for the palette sheet) and the DEM's
+-32767 nodata value were all verified unchanged against the originals.
+
+## Why the files were re-derived
+
+### Row alignment
+
+`GridData::ReadRaster` is called with `bAlignment = true`, so it expects each row
+of the provider's stream to be padded to a multiple of 8 bytes, and it gives up
+(`if (reader->available() != bufferLength) return;`) when the stream is shorter.
+The GDAL provider returns packed rows, so a raster whose
+`width * bitsPerPixel` is not a multiple of 64 never reaches the band code:
+`RasterAdapter::Stylize` falls back to its legacy decoder instead. That decoder
+handles 8/24/32 bpp adequately, which is why such rasters still look right, but
+it has no 16 bpp case and leaves its destination buffer unwritten (rendering
+heap contents).
+
+That is why every fixture here has a 64-bit aligned row width: 512, 512, 1944
+(8 bit) and 336 (16 bit). Without the crop these files would not exercise the
+`GridData::ReadRaster`/`Band` code the tests exist to cover. This is a
+pre-existing limitation of the raster read path, not something these fixtures
+introduce; the synthetic fixtures in Part 1 are also deliberately 64 px wide for
+the same reason.
+
+### Palette file format
+
+`PNG/CM13ct.png` has a geotransform but no coordinate system, and the provider
+cannot be used to *select* features from it through MapGuide:
+`MgFeatureService::SelectFeatures` returns a reader with no rows (verified for a
+plain select, a `FeatId`-only select and a raster select), so a raster layer over
+it draws nothing. The same data re-encoded as a GeoTIFF selects and renders
+normally, and declaring EPSG:32613 says what its metre coordinates already are,
+so the map definition does not have to pretend UTM eastings are degrees.
+
+`DEFLATE`/`PREDICTOR=2` are lossless; they only make the checked in file 2.3 MB
+instead of 6.1 MB.
+
+Note that palette entry 0 in this file is `(0,0,0,0)`, a fully transparent
+black covering the sheet margin. The renderer composites it onto the white map
+background, so the rendered image shows a white margin where a plain RGB view of
+the source is black. That is the palette branch honouring the palette's alpha,
+not a rendering defect.
+
+## Adding or changing a real fixture
+
+1. Choose a file from `fdo/Providers/GDAL/TestData` and re-derive it as above,
+   keeping the `RealRaster_` prefix. The row width must satisfy
+   `width * bitsPerPixel % 64 == 0`, so crop to a width that does.
+2. Verify it is usable:
+
+   ```
+   gdalinfo <file>            # expect a geotransform and a coordinate system
+   ```
+
+   A file without georeferencing cannot be read by the provider at all, and a
+   file whose CRS cannot be represented in the map definition will not render.
+3. Add an entry to `REAL_FIXTURES` in `TestRasterStylization.cpp` with the style
+   that suits the data (`StyleBand`, `StyleGrayBands` or `StyleHillShade`).
+4. Run the suite and look at the PNG it writes to
+   `<build>/Server/src/UnitTestFiles/Results/`.
+5. Record the upstream path, hash, size, derivation command and what the file is
+   in the tables above.
+
+## File inventory
+
+| File | Bytes | SHA-256 |
+|------|-------|---------|
+| `RealRaster_ae.tif` | 264,876 | `72610ac16168d8f6a33177c681a31044aacac909f72af019e08c64a112ab7350` |
+| `RealRaster_cm13ct.tif` | 2,287,461 | `998430170a82cabf8cad85aa0cf876e7765f35fa01fb4d7f6e252612c5f1fb0f` |
+| `RealRaster_mtjefferson.tif` | 315,773 | `e2b7016d355f3cc17e586cd4c834e63dfe0008253443f8eb54330c4339606f1f` |
+| `RealRaster_spif83.tif` | 262,871 | `49e48c375e721bea9a72d19cfec2a48641b83aa6951b97393d8a4ad992394310` |
+
+The derivation commands use GDAL 3.12.4 (the toolchain in Part 1); a different
+GDAL version produces a different (still lossless) encoding and therefore a
+different checksum.

Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray16.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32f.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray32i.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray64f.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_gray8.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_palette8.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb24.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgb48_int16.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RasterStylization_rgba32.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_ae.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_cm13ct.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_mtjefferson.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif
===================================================================
(Binary files differ)

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif	2026-10-21 22:45:33 UTC (rev 10224)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif	2026-10-23 11:25:59 UTC (rev 10225)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_spif83.tif
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property
Added: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/make_fixtures.py
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/make_fixtures.py	                        (rev 0)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/make_fixtures.py	2026-10-23 11:25:59 UTC (rev 10225)
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+"""Generates the raster fixtures used by TestRasterStylization.
+
+Each 64x64 fixture is split into four 32x32 quadrants with a distinct colour so
+the rendered map can be sampled at the centre of each quadrant.  The values are
+chosen so that for 16 bit samples the high and low bytes differ.
+
+    quadrant 0 (top-left)     R 0x1234  G 0x5678  B 0x9ABC
+    quadrant 1 (top-right)    R 0xFFFF  G 0x0000  B 0x00FF
+    quadrant 2 (bottom-left)  R 0x0000  G 0xFFFF  B 0x0100
+    quadrant 3 (bottom-right) R 0x8080  G 0x4040  B 0x1010
+
+"top" is the north edge of the raster, which is the top of the rendered map.
+For 8 bit fixtures each channel is reduced to its high byte, so the four
+quadrants render 0x12569A / 0xFF0000 / 0x00FF01 / 0x804010.
+
+The GDAL provider (FdoRfpBandRaster::GetDataModel) decides the raster data
+model it exposes from the band count and colour interpretation:
+
+    3 bands                    -> RGB,  BitsPerPixel = 3 * sample size * 8
+    4 bands (4th has alpha)    -> RGBA, BitsPerPixel = 4 * sample size * 8
+    1 band + colour table      -> Palette
+    anything else (1 band)     -> Gray
+
+so the fixture set below covers 24/32/48 bpp colour, 8 bpp palette and
+8/16/32/64 bpp single band rasters.
+
+Run with:  python3 make_fixtures.py
+
+The files this writes are checked in.  Their provenance, the exact tool versions
+that produced them and their checksums are recorded in PROVENANCE.md in this
+directory - update both together.
+"""
+import numpy as np
+from osgeo import gdal
+
+SIZE = 64
+BLOCK = SIZE // 2
+
+# UnitTest/TestData is copied flat into <build>/Server/src/UnitTestFiles, so the
+# fixtures carry this prefix to keep them apart from the other test data.
+PREFIX = "RasterStylization_"
+
+# 16 bit quadrants (R, G, B) and the 8 bit colour they reduce to (high byte)
+QUADRANTS_16 = [(0x1234, 0x5678, 0x9ABC),
+                (0xFFFF, 0x0000, 0x00FF),
+                (0x0000, 0xFFFF, 0x0100),
+                (0x8080, 0x4040, 0x1010)]
+def high_byte(v):
+    return (v >> 8) & 0xFF
+
+
+QUADRANTS_8 = [tuple(high_byte(c) for c in q) for q in QUADRANTS_16]
+ALPHAS = (0x80, 0xFF, 0x00, 0x40)
+NP = {gdal.GDT_Byte: np.uint8, gdal.GDT_UInt16: np.uint16,
+      gdal.GDT_Int16: np.int16, gdal.GDT_Int32: np.int32,
+      gdal.GDT_Float32: np.float32, gdal.GDT_Float64: np.float64}
+
+
+def quadrant(x, y):
+    return (1 if x >= BLOCK else 0) + (2 if y >= BLOCK else 0)
+
+
+def create(name, bands, dtype, fill, options=None, palette=None, interp=None):
+    opts = ["TILED=NO"]
+    if palette:
+        opts.append("PHOTOMETRIC=PALETTE")
+    elif bands >= 3:
+        opts.append("PHOTOMETRIC=RGB")
+    if options:
+        opts.extend(options)
+    ds = gdal.GetDriverByName("GTiff").Create(name, SIZE, SIZE, bands, dtype, options=opts)
+    ds.SetGeoTransform((0.0, 1.0, 0.0, float(SIZE), 0.0, -1.0))
+    # WGS84 geographic: the map definition uses the same CS, so the raster is
+    # drawn without a datum transform and 1 pixel = 1 degree.
+    ds.SetProjection('GEOGCS["LL84",DATUM["WGS84",SPHEROID["WGS84",6378137.000,298.25722293]],'
+                     'PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]')
+    if palette:
+        # must be set before the first write
+        ct = gdal.ColorTable()
+        for i, rgba in enumerate(palette):
+            ct.SetColorEntry(i, rgba)
+        ds.GetRasterBand(1).SetColorTable(ct)
+        ds.GetRasterBand(1).SetColorInterpretation(gdal.GCI_PaletteIndex)
+    for b in range(1, bands + 1):
+        arr = np.empty((SIZE, SIZE), dtype=NP[dtype])
+        for y in range(SIZE):
+            for x in range(SIZE):
+                value = int(fill(x, y, b - 1))
+                if dtype == gdal.GDT_Int16:
+                    # keep the 16 bit bit pattern, just as a signed sample
+                    value = ((value + 0x8000) & 0xFFFF) - 0x8000
+                elif dtype == gdal.GDT_Int32:
+                    value = ((value + 0x80000000) & 0xFFFFFFFF) - 0x80000000
+                arr[y, x] = value
+        band = ds.GetRasterBand(b)
+        band.WriteArray(arr)
+        if interp and interp[b - 1] != "Gray":
+            band.SetColorInterpretation(getattr(gdal, "GCI_" + interp[b - 1] + "Band"))
+    ds = None
+
+
+def colour(dtype_scale):
+    """Per-channel fill for the quadrants, scaled to the sample type."""
+    def fill(x, y, band):
+        value = QUADRANTS_16[quadrant(x, y)][band]
+        return value if dtype_scale == 1 else high_byte(value)
+    return fill
+
+
+def rgba(scale):
+    def fill(x, y, band):
+        if band == 3:
+            return ALPHAS[quadrant(x, y)] * (257 if scale == 1 else 1)
+        value = QUADRANTS_16[quadrant(x, y)][band]
+        return value if scale == 1 else high_byte(value)
+    return fill
+
+
+def gray(values, scale):
+    def fill(x, y, band):
+        value = values[quadrant(x, y)]
+        return value if scale == 1 else high_byte(value)
+    return fill
+
+
+# 24 bpp: 3 bands of Byte -> RGB / 24
+create(PREFIX + "rgb24.tif", 3, gdal.GDT_Byte, colour(0), interp=("Red", "Green", "Blue"))
+# 32 bpp: 4 bands of Byte with an alpha band -> RGBA / 32
+create(PREFIX + "rgba32.tif", 4, gdal.GDT_Byte, rgba(0), interp=("Red", "Green", "Blue", "Alpha"))
+# 48 bpp: 3 bands of UInt16 -> RGB / 48  (the packing fix)
+create(PREFIX + "rgb48.tif", 3, gdal.GDT_UInt16, colour(1), interp=("Red", "Green", "Blue"))
+# 48 bpp signed: same layout, different FdoRasterDataType
+create(PREFIX + "rgb48_int16.tif", 3, gdal.GDT_Int16, colour(1), interp=("Red", "Green", "Blue"))
+# 64 bpp RGBA (4 bands of UInt16 with alpha) is not generated: ReadRaster's
+# "else" branch writes a 32 bpp row into the 64 bpp band it creates for it, so
+# the rendered result depends on uninitialised stack bytes.
+# 8 bpp palette: one band with a colour table -> Palette / 8
+create(PREFIX + "palette8.tif", 1, gdal.GDT_Byte, lambda x, y, b: quadrant(x, y),
+       palette=[(0x12, 0x56, 0x9A, 255), (0x00, 0x00, 0xFF, 255),
+                (0x00, 0xFF, 0x01, 255), (0x80, 0x40, 0x10, 255)])
+# 8 bpp single band -> Gray / 8
+# keep the samples within the positive Int8 range: the colour handler maps the value
+# through a signed conversion, so values >= 0x80 would render as null pixels
+create(PREFIX + "gray8.tif", 1, gdal.GDT_Byte, gray([0x12, 0x34, 0x56, 0x78], 1),
+       interp=("Gray",))
+# 16 bpp single band -> Gray / 16
+create(PREFIX + "gray16.tif", 1, gdal.GDT_UInt16, gray([0x1234, 0x5678, 0x9ABC, 0x0100], 1),
+       interp=("Gray",))
+# 32 bpp single band integer -> Gray / 32, exercises the Int32 band accessors
+create(PREFIX + "gray32i.tif", 1, gdal.GDT_Int32,
+       gray([0x00123456, 0x00234567, 0x00345678, 0x00456789], 1), interp=("Gray",))
+# 32 bpp single band float -> Gray / 32, exercises the Double32 band accessors
+create(PREFIX + "gray32f.tif", 1, gdal.GDT_Float32, gray([4660.0, 22136.0, 39612.0, 256.0], 1),
+       interp=("Gray",))
+# 64 bpp single band float -> Gray / 64, exercises the Double64 band accessors
+create(PREFIX + "gray64f.tif", 1, gdal.GDT_Float64, gray([4660.0, 22136.0, 39612.0, 256.0], 1),
+       interp=("Gray",))
+
+for name in ("rgb24.tif", "rgba32.tif", "rgb48.tif", "rgb48_int16.tif",
+             "palette8.tif", "gray8.tif", "gray16.tif", "gray32i.tif", "gray32f.tif",
+             "gray64f.tif"):
+    name = PREFIX + name
+    ds = gdal.Open(name)
+    b = ds.GetRasterBand(1)
+    print("%-16s bands=%d %-8s bpp=%d ci=%s" % (
+        name, ds.RasterCount, gdal.GetDataTypeName(b.DataType),
+        gdal.GetDataTypeSize(b.DataType) * ds.RasterCount // 8,
+        b.GetColorInterpretation()))



More information about the mapguide-commits mailing list