[mapguide-commits] r10231 - in branches/4.0/MgDev: Server/src/UnitTesting UnitTest/TestData/RasterStylization

svn_mapguide at osgeo.org svn_mapguide at osgeo.org
Fri Oct 23 18:07:41 PDT 2026


Author: jng
Date: 2026-10-23 18:07:41 -0700 (Fri, 23 Oct 2026)
New Revision: 10231

Added:
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif
Modified:
   branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
   branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md
Log:
#2174: Add failing test for striped raster rendering. Test raster from TrueMarble

Modified: branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp
===================================================================
--- branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp	2026-10-23 22:11:40 UTC (rev 10230)
+++ branches/4.0/MgDev/Server/src/UnitTesting/TestRasterStylization.cpp	2026-10-24 01:07:41 UTC (rev 10231)
@@ -460,6 +460,65 @@
     return wkt;
 }
 
+// RAII guard that raises the max raster image size for the lifetime of the
+// object and restores the previous values on destruction. The rendering service
+// reads the setting from configuration when it is constructed, so this must be
+// created before the service. The destructor restores the values even when a
+// REQUIRE aborts the test case early, so later cases keep their defaults.
+struct MaxRasterImageGuard
+{
+    MaxRasterImageGuard()
+        : m_oldWidth(MgConfigProperties::DefaultRenderingServicePropertyMaxRasterImageWidth),
+          m_oldHeight(MgConfigProperties::DefaultRenderingServicePropertyMaxRasterImageHeight)
+    {
+        MgConfiguration* config = MgConfiguration::GetInstance();
+        if (NULL != config)
+        {
+            config->GetIntValue(MgConfigProperties::RenderingServicePropertiesSection,
+                MgConfigProperties::RenderingServicePropertyMaxRasterImageWidth,
+                m_oldWidth, MgConfigProperties::DefaultRenderingServicePropertyMaxRasterImageWidth);
+            config->GetIntValue(MgConfigProperties::RenderingServicePropertiesSection,
+                MgConfigProperties::RenderingServicePropertyMaxRasterImageHeight,
+                m_oldHeight, MgConfigProperties::DefaultRenderingServicePropertyMaxRasterImageHeight);
+
+            Set(config, 8192, 8192);
+        }
+    }
+
+    ~MaxRasterImageGuard()
+    {
+        MgConfiguration* config = MgConfiguration::GetInstance();
+        if (NULL != config)
+            Set(config, m_oldWidth, m_oldHeight);
+    }
+
+    static void Set(MgConfiguration* config, INT32 width, INT32 height)
+    {
+        // MgConfiguration::SetProperties only accepts string properties: every
+        // entry in the ini file is stored as a string, and an Int32 property
+        // makes it throw MgInvalidPropertyTypeException.
+        STRING widthValue;
+        STRING heightValue;
+        MgUtil::Int32ToString(width, widthValue);
+        MgUtil::Int32ToString(height, heightValue);
+
+        Ptr<MgStringProperty> widthProp = new MgStringProperty();
+        widthProp->SetName(MgConfigProperties::RenderingServicePropertyMaxRasterImageWidth);
+        widthProp->SetValue(widthValue);
+        Ptr<MgStringProperty> heightProp = new MgStringProperty();
+        heightProp->SetName(MgConfigProperties::RenderingServicePropertyMaxRasterImageHeight);
+        heightProp->SetValue(heightValue);
+
+        Ptr<MgPropertyCollection> props = new MgPropertyCollection();
+        props->Add(widthProp);
+        props->Add(heightProp);
+        config->SetProperties(MgConfigProperties::RenderingServicePropertiesSection, props);
+    }
+
+    INT32 m_oldWidth;
+    INT32 m_oldHeight;
+};
+
 }
 
 TEST_CASE("Raster grid styling", "[RasterStylization]")
@@ -654,3 +713,139 @@
         MgFdoConnectionManager::GetInstance()->ClearCache();
     }
 }
+
+TEST_CASE("TrueMarble striped rendering", "[RasterStylization]")
+{
+    // Reproducer for the GDAL provider "striped" rendering defect, using a
+    // single checked-in tile of the free "Global True Marble" dataset (see
+    // UnitTest/TestData/RasterStylization/PROVENANCE.md). The tile itself is a
+    // 4.5 MB GeoTIFF; the full dataset is ~70 GB, which is why only this one
+    // tile is checked in.
+    //
+    // It requests the small 32 km overview tile (1350x675, 3 x Byte) at
+    // 4000x2000, an upscale that crosses the provider's hard-coded 4 MP
+    // scanline-tiling threshold (FdoRfpBandRaster::GetDataModel). Above that
+    // threshold the provider reads one scanline per tile, and the per-row
+    // source window in FdoRfpStreamReaderByTile::_getTile rounds to zero
+    // height for some rows when upsampling. Those rows are never overwritten,
+    // so they keep the tile's 0xFF prefill and survive the pipeline as whole
+    // rows of pure white: the stripes.
+    //
+    // The defect is in the provider's output stream, so it can be detected
+    // objectively without a human looking at the render: real imagery never
+    // contains a full-width, exactly-white row. For this 4000x2000 upscale the
+    // per-row source window rounds to zero height for a large fraction of the
+    // output rows (653 of 2000 in the actual Release run), and GDAL treats a
+    // zero-height window as a no-op, leaving the 0xFF prefill. The PNG is
+    // still written to Results/ for visual confirmation.
+
+    // Raise the max raster image size so the 4000x2000 request is not halved by
+    // RasterAdapter before it reaches the provider (the halving loop halves any
+    // image >= MaxRasterImageWidth/Height). Each rendering service reads this
+    // from configuration when it is constructed, so the guard is created before
+    // the service and restores the values when it goes out of scope.
+    MaxRasterImageGuard rasterGuard;
+
+    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;
+    const int renderWidth = 4000;
+    const int renderHeight = 2000;
+
+    const STRING name = L"TrueMarble_striped";
+    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("RealRaster_truemarble32.tif")),
+                                              rasterProperty);
+
+    // The raster's own footprint drives the map view, so the image shows the
+    // whole file (stretched to the 4000x2000 request, which is the upscale that
+    // triggers the defect).
+    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();
+
+    STRING csWkt = GetRasterCoordinateSystem(svcFeature, fsId);
+    if (csWkt.empty())
+        csWkt = LL84_WKT;
+
+    // The 3 x Byte tile reads through GridData::ReadRaster's 24 bpp branch,
+    // which packs the RGB samples into one ARGB value, so the plain <Band>
+    // colour style passes it through unchanged.
+    PublishResource(svcResource, layerId, LayerXml(fsId, featureName, rasterProperty, StyleBand));
+    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 spans (maxX - minX) / renderWidth map units, so the
+    // requested raster size comes out at renderWidth x renderHeight.
+    map->SetViewScale(((maxX - minX) / renderWidth) * map->GetMetersPerUnit() * dpi / metersPerInch);
+
+    int width = 0;
+    int height = 0;
+    unsigned int* rawPixels = RenderPng(svcRendering, map, width, height, name + L".png");
+    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 == renderWidth);
+    REQUIRE(height == renderHeight);
+
+    // Count rows that are exactly opaque white. A striped row is the provider's
+    // 0xFF prefill passed through the 24 bpp branch, which packs it to
+    // 0xFFFFFFFF. DecodePNG returns the image bottom up, which does not matter
+    // for a per-row count.
+    int whiteRows = 0;
+    for (int y = 0; y < height; ++y)
+    {
+        bool allWhite = true;
+        for (int x = 0; x < width; ++x)
+        {
+            if ((rawPixels[y * width + x] & 0x00FFFFFF) != 0xFFFFFF)
+            {
+                allWhite = false;
+                break;
+            }
+        }
+        if (allWhite)
+            ++whiteRows;
+    }
+
+    ACE_DEBUG((LM_INFO, ACE_TEXT("  wrote %W (%dx%d from %dx%d), %d uniformly white rows\n"),
+               ResultPath(name + L".png").c_str(), width, height, imageXSize, imageYSize, whiteRows));
+
+    INFO("rendered " << width << "x" << height << " with " << whiteRows
+         << " uniformly white rows out of " << height);
+    // Real imagery never contains a full-width, exactly-white row. Allow a tiny
+    // tolerance for safety; the defect produces on the order of a third of the
+    // image height in white rows, so this fails hard while the bug is present.
+    CHECK(whiteRows <= height / 100);
+
+    // Release the pooled raster connection now, for the same reason as the
+    // other cases in this suite.
+    MgFdoConnectionManager::GetInstance()->ClearCache();
+}

Modified: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md	2026-10-23 22:11:40 UTC (rev 10230)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/PROVENANCE.md	2026-10-24 01:07:41 UTC (rev 10231)
@@ -325,6 +325,30 @@
 5. Record the upstream path, hash, size, derivation command and what the file is
    in the tables above.
 
+## The True Marble tile (`RealRaster_truemarble32.tif`)
+
+This one is not derived from the FDO provider test data. It is a single tile of
+"Global True Marble", the free reduced-resolution release of Unearthed
+Outdoors' True Marble global colour imagery. It drives the `[RasterStylization]`
+"TrueMarble striped rendering" case, which reproduces the GDAL provider's
+striped upsampling defect: that case renders the 1350x675 tile at 4000x2000, an
+upscale that crosses the provider's 4 MP scanline-tiling threshold and leaves
+whole rows as the provider's 0xFF prefill.
+
+| Property | Value |
+|----------|-------|
+| Origin | Unearthed Outdoors, LLC - "Global True Marble", 32 km resolution global tile |
+| Source file | `TrueMarble.32km.1350x675.tif` from the free Global True Marble download |
+| Licence | Creative Commons Attribution 3.0 United States (`License.html` in the download) |
+| Attribution | "We only ask that copyright be attributed to Unearthed Outdoors when reproduced." |
+| Size | 1350x675 px, 4,719,372 bytes, 3 x Byte (RGB), WGS84 geographic |
+| Reprocessing | None - checked in byte for byte as downloaded |
+
+The requested render width is 4000 pixels, so `width * bitsPerPixel` is
+`4000 * 24 = 96000`, a multiple of 64, and the tile still reaches the
+`GridData::ReadRaster` 24 bpp branch this suite exists to cover - the 64-bit
+alignment constraint is on the *requested* width, not the file's native width.
+
 ## File inventory
 
 | File | Bytes | SHA-256 |
@@ -333,6 +357,7 @@
 | `RealRaster_cm13ct.tif` | 2,287,461 | `998430170a82cabf8cad85aa0cf876e7765f35fa01fb4d7f6e252612c5f1fb0f` |
 | `RealRaster_mtjefferson.tif` | 315,773 | `e2b7016d355f3cc17e586cd4c834e63dfe0008253443f8eb54330c4339606f1f` |
 | `RealRaster_spif83.tif` | 262,871 | `49e48c375e721bea9a72d19cfec2a48641b83aa6951b97393d8a4ad992394310` |
+| `RealRaster_truemarble32.tif` | 4,719,372 | `dd9de7a8486350c795b81a350f202c649c4b40727af1a3cd391f93b33af1c72f` |
 
 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

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

Index: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif
===================================================================
--- branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif	2026-10-23 22:11:40 UTC (rev 10230)
+++ branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif	2026-10-24 01:07:41 UTC (rev 10231)

Property changes on: branches/4.0/MgDev/UnitTest/TestData/RasterStylization/RealRaster_truemarble32.tif
___________________________________________________________________
Added: svn:executable
## -0,0 +1 ##
+*
\ No newline at end of property
Added: svn:mime-type
## -0,0 +1 ##
+image/tiff
\ No newline at end of property


More information about the mapguide-commits mailing list