[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