Skip to content

Commit 0b0d699

Browse files
authoredApr 14, 2025
Merge pull request #12126 from rouault/plugin_algorithm
Add a mechanism to register external algorithm, in particular for drivers
2 parents 3dbc60b + 6f8935c commit 0b0d699

20 files changed

+774
-54
lines changed
 

Diff for: ‎apps/data/gdal_algorithm.schema.json

-2
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@
7979
},
8080
"required": [
8181
"description",
82-
"short_url",
83-
"url",
8482
"sub_algorithms",
8583
"input_arguments",
8684
"output_arguments",

Diff for: ‎autotest/cpp/test_gdal_algorithm.cpp

+76
Original file line numberDiff line numberDiff line change
@@ -3696,4 +3696,80 @@ TEST_F(test_gdal_algorithm, raster_edit_failures_unset_metadata)
36963696
"edit: SetMetadataItem('foo', NULL) failed");
36973697
}
36983698

3699+
TEST_F(test_gdal_algorithm, register_plugin_algorithms)
3700+
{
3701+
auto &singleton = GDALGlobalAlgorithmRegistry::GetSingleton();
3702+
bool flag = false;
3703+
singleton.DeclareAlgorithm(
3704+
{"foo", "bar"},
3705+
[&flag]() -> std::unique_ptr<GDALAlgorithm>
3706+
{
3707+
flag = true;
3708+
return std::make_unique<GDALContainerAlgorithm>("dummy");
3709+
});
3710+
3711+
{
3712+
EXPECT_NE(singleton.Instantiate("foo"), nullptr);
3713+
EXPECT_FALSE(flag);
3714+
}
3715+
3716+
{
3717+
auto got = singleton.GetDeclaredSubAlgorithmNames({"gdal"});
3718+
EXPECT_TRUE(std::find(got.begin(), got.end(), "foo") != got.end());
3719+
EXPECT_FALSE(flag);
3720+
}
3721+
3722+
{
3723+
auto got = singleton.GetDeclaredSubAlgorithmNames({"gdal", "foo"});
3724+
EXPECT_TRUE(std::find(got.begin(), got.end(), "bar") != got.end());
3725+
EXPECT_TRUE(flag);
3726+
flag = false;
3727+
}
3728+
3729+
{
3730+
auto got =
3731+
singleton.GetDeclaredSubAlgorithmNames({"gdal", "foo", "bar"});
3732+
EXPECT_TRUE(got.empty());
3733+
EXPECT_FALSE(flag);
3734+
}
3735+
3736+
{
3737+
auto got = singleton.GetDeclaredSubAlgorithmNames({"gdal", "bar"});
3738+
EXPECT_TRUE(got.empty());
3739+
EXPECT_FALSE(flag);
3740+
}
3741+
3742+
{
3743+
auto alg = singleton.InstantiateDeclaredSubAlgorithm({"gdal", "foo"});
3744+
ASSERT_NE(alg, nullptr);
3745+
EXPECT_TRUE(alg->HasSubAlgorithms());
3746+
EXPECT_EQ(alg->GetSubAlgorithmNames().size(), 1);
3747+
EXPECT_TRUE(flag);
3748+
flag = false;
3749+
}
3750+
3751+
{
3752+
auto alg =
3753+
singleton.InstantiateDeclaredSubAlgorithm({"gdal", "foo", "bar"});
3754+
ASSERT_NE(alg, nullptr);
3755+
EXPECT_TRUE(flag);
3756+
flag = false;
3757+
}
3758+
3759+
{
3760+
auto alg = singleton.Instantiate("foo")->InstantiateSubAlgorithm("bar");
3761+
ASSERT_NE(alg, nullptr);
3762+
EXPECT_TRUE(flag);
3763+
}
3764+
3765+
{
3766+
auto alg = singleton.InstantiateDeclaredSubAlgorithm({"gdal", "bar"});
3767+
ASSERT_EQ(alg, nullptr);
3768+
}
3769+
3770+
singleton.DeclareAlgorithm({"foo", "bar"},
3771+
[]() -> std::unique_ptr<GDALAlgorithm>
3772+
{ return nullptr; });
3773+
}
3774+
36993775
} // namespace test_gdal_algorithm

Diff for: ‎autotest/gcore/driver_algorithms.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env pytest
2+
# -*- coding: utf-8 -*-
3+
###############################################################################
4+
# Project: GDAL/OGR Test Suite
5+
# Purpose: Check that drivers that declare algorithms actually implement them
6+
# Author: Even Rouault <even dot rouault @ spatialys.com>
7+
#
8+
###############################################################################
9+
# Copyright (c) 2025, Even Rouault <even dot rouault at spatialys.com>
10+
#
11+
# SPDX-License-Identifier: MIT
12+
###############################################################################
13+
14+
15+
from osgeo import gdal
16+
17+
18+
def check(alg, expected_name):
19+
assert alg.GetName() == expected_name
20+
if alg.HasSubAlgorithms():
21+
for subalg_name in alg.GetSubAlgorithmNames():
22+
subalg = alg[subalg_name]
23+
assert subalg, subalg_name
24+
check(subalg, subalg_name)
25+
26+
assert alg.InstantiateSubAlgorithm("i_do_not_exist") is None
27+
28+
29+
def test_driver_algorithms():
30+
31+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]
32+
if alg:
33+
check(alg, "driver")

Diff for: ‎autotest/gdrivers/pdf.py

+43
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# SPDX-License-Identifier: MIT
1313
###############################################################################
1414

15+
import json
1516
import os
1617
import shutil
1718
import sys
@@ -3097,3 +3098,45 @@ def test_pdf_iso32000_invalid_srs():
30973098

30983099
# Just test that this does not crash
30993100
gdal.Open("data/pdf/invalid_srs.pdf")
3101+
3102+
3103+
###############################################################################
3104+
#
3105+
3106+
3107+
@gdaltest.enable_exceptions()
3108+
@pytest.mark.skipif(not have_read_support(), reason="no read support available")
3109+
def test_pdf_gdal_driver_pdf_list_layer():
3110+
3111+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]
3112+
assert alg.GetName() == "driver"
3113+
3114+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["pdf"]
3115+
assert alg.GetName() == "pdf"
3116+
3117+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["pdf"]["list-layers"]
3118+
assert alg.GetName() == "list-layers"
3119+
alg["input"] = "data/pdf/adobe_style_geospatial.pdf"
3120+
assert alg.Run()
3121+
assert json.loads(alg["output-string"]) == [
3122+
"New_Data_Frame",
3123+
"New_Data_Frame.Graticule",
3124+
"Layers",
3125+
"Layers.Measured_Grid",
3126+
"Layers.Graticule",
3127+
]
3128+
3129+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["pdf"]["list-layers"]
3130+
alg["input"] = "data/pdf/adobe_style_geospatial.pdf"
3131+
alg["output-format"] = "text"
3132+
assert alg.Run()
3133+
assert (
3134+
alg["output-string"]
3135+
== "New_Data_Frame\nNew_Data_Frame.Graticule\nLayers\nLayers.Measured_Grid\nLayers.Graticule\n"
3136+
)
3137+
3138+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["pdf"]["list-layers"]
3139+
assert alg.GetName() == "list-layers"
3140+
alg["input"] = "data/byte.tif"
3141+
with pytest.raises(Exception, match="is not a PDF"):
3142+
alg.Run()

Diff for: ‎autotest/ogr/ogr_openfilegdb_write.py

+42
Original file line numberDiff line numberDiff line change
@@ -4601,3 +4601,45 @@ def test_ogr_openfilegdb_write_check_golden_file(tmp_path, src_directory):
46014601
assert (
46024602
open(src_filename, "rb").read() == open(out_filename, "rb").read()
46034603
), filename
4604+
4605+
4606+
###############################################################################
4607+
# Test 'gdal driver openfilegdb repack'
4608+
4609+
4610+
@gdaltest.enable_exceptions()
4611+
def test_ogropenfilegdb_write_gdal_driver_openfilegdb_repack(tmp_path):
4612+
4613+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]
4614+
assert alg.GetName() == "driver"
4615+
4616+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["openfilegdb"]
4617+
assert alg.GetName() == "openfilegdb"
4618+
4619+
out_directory = str(tmp_path / "test.gdb")
4620+
gdal.VectorTranslate(
4621+
out_directory, "data/openfilegdb/polygon_golden.gdb", format="OpenFileGDB"
4622+
)
4623+
4624+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["openfilegdb"]["repack"]
4625+
assert alg.GetName() == "repack"
4626+
alg["input"] = "data/poly.shp"
4627+
with pytest.raises(Exception, match="is not a FileGeoDatabase"):
4628+
alg.Run()
4629+
4630+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["openfilegdb"]["repack"]
4631+
assert alg.GetName() == "repack"
4632+
alg["input"] = out_directory
4633+
assert alg.Run()
4634+
4635+
last_pct = [0]
4636+
4637+
def my_progress(pct, msg, user_data):
4638+
last_pct[0] = pct
4639+
return True
4640+
4641+
alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["openfilegdb"]["repack"]
4642+
assert alg.GetName() == "repack"
4643+
alg["input"] = out_directory
4644+
assert alg.Run(my_progress)
4645+
assert last_pct[0] == 1.0

Diff for: ‎frmts/pdf/pdfdataset.cpp

+85
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919

2020
#include "gdal_pdf.h"
2121

22+
#include "cpl_json_streaming_writer.h"
2223
#include "cpl_vsi_virtual.h"
2324
#include "cpl_spawn.h"
2425
#include "cpl_string.h"
2526
#include "gdal_frmts.h"
27+
#include "gdalalgorithm.h"
2628
#include "ogr_spatialref.h"
2729
#include "ogr_geometry.h"
2830

@@ -7711,6 +7713,88 @@ CPLString PDFSanitizeLayerName(const char *pszName)
77117713
return osName;
77127714
}
77137715

7716+
/************************************************************************/
7717+
/* GDALPDFListLayersAlgorithm */
7718+
/************************************************************************/
7719+
7720+
#ifdef HAVE_PDF_READ_SUPPORT
7721+
7722+
class GDALPDFListLayersAlgorithm final : public GDALAlgorithm
7723+
{
7724+
public:
7725+
GDALPDFListLayersAlgorithm()
7726+
: GDALAlgorithm("list-layers",
7727+
std::string("List layers of a PDF dataset"),
7728+
"/drivers/raster/pdf.html")
7729+
{
7730+
AddInputDatasetArg(&m_dataset, GDAL_OF_RASTER | GDAL_OF_VECTOR);
7731+
AddOutputFormatArg(&m_format).SetDefault(m_format).SetChoices("json",
7732+
"text");
7733+
AddOutputStringArg(&m_output);
7734+
}
7735+
7736+
protected:
7737+
bool RunImpl(GDALProgressFunc, void *) override
7738+
{
7739+
auto poDS = dynamic_cast<PDFDataset *>(m_dataset.GetDatasetRef());
7740+
if (!poDS)
7741+
{
7742+
ReportError(CE_Failure, CPLE_AppDefined, "%s is not a PDF",
7743+
m_dataset.GetName().c_str());
7744+
return false;
7745+
}
7746+
if (m_format == "json")
7747+
{
7748+
CPLJSonStreamingWriter oWriter(nullptr, nullptr);
7749+
oWriter.StartArray();
7750+
for (const auto &[key, value] : cpl::IterateNameValue(
7751+
const_cast<CSLConstList>(poDS->GetMetadata("LAYERS"))))
7752+
{
7753+
CPL_IGNORE_RET_VAL(key);
7754+
oWriter.Add(value);
7755+
}
7756+
oWriter.EndArray();
7757+
m_output = oWriter.GetString();
7758+
m_output += '\n';
7759+
}
7760+
else
7761+
{
7762+
for (const auto &[key, value] : cpl::IterateNameValue(
7763+
const_cast<CSLConstList>(poDS->GetMetadata("LAYERS"))))
7764+
{
7765+
CPL_IGNORE_RET_VAL(key);
7766+
m_output += value;
7767+
m_output += '\n';
7768+
}
7769+
}
7770+
return true;
7771+
}
7772+
7773+
private:
7774+
GDALArgDatasetValue m_dataset{};
7775+
std::string m_format = "json";
7776+
std::string m_output{};
7777+
};
7778+
7779+
/************************************************************************/
7780+
/* GDALPDFInstantiateAlgorithm() */
7781+
/************************************************************************/
7782+
7783+
static GDALAlgorithm *
7784+
GDALPDFInstantiateAlgorithm(const std::vector<std::string> &aosPath)
7785+
{
7786+
if (aosPath.size() == 1 && aosPath[0] == "list-layers")
7787+
{
7788+
return std::make_unique<GDALPDFListLayersAlgorithm>().release();
7789+
}
7790+
else
7791+
{
7792+
return nullptr;
7793+
}
7794+
}
7795+
7796+
#endif // HAVE_PDF_READ_SUPPORT
7797+
77147798
/************************************************************************/
77157799
/* GDALRegister_PDF() */
77167800
/************************************************************************/
@@ -7729,6 +7813,7 @@ void GDALRegister_PDF()
77297813

77307814
#ifdef HAVE_PDF_READ_SUPPORT
77317815
poDriver->pfnOpen = PDFDataset::OpenWrapper;
7816+
poDriver->pfnInstantiateAlgorithm = GDALPDFInstantiateAlgorithm;
77327817
#endif // HAVE_PDF_READ_SUPPORT
77337818

77347819
poDriver->pfnCreateCopy = GDALPDFCreateCopy;

Diff for: ‎frmts/pdf/pdfdrivercore.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ void PDFDriverSetCommonMetadata(GDALDriver *poDriver)
234234
poDriver->SetMetadataItem(GDAL_DCAP_UPDATE, "YES");
235235
poDriver->SetMetadataItem(GDAL_DMD_UPDATE_ITEMS,
236236
"GeoTransform SRS GCPs DatasetMetadata");
237+
238+
poDriver->DeclareAlgorithm({"list-layers"});
237239
#endif
238240

239241
poDriver->SetMetadataItem(GDAL_DCAP_CREATE, "YES");

Diff for: ‎gcore/gdal_priv.h

+23
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class GDALProxyDataset;
3434
class GDALProxyRasterBand;
3535
class GDALAsyncReader;
3636
class GDALRelationship;
37+
class GDALAlgorithm;
3738

3839
/* -------------------------------------------------------------------- */
3940
/* Pull in the public declarations. This gets the C apis, and */
@@ -2368,6 +2369,26 @@ class CPL_DLL GDALDriver : public GDALMajorObject
23682369
GDALSubdatasetInfo *(*pfnGetSubdatasetInfoFunc)(const char *pszFileName) =
23692370
nullptr;
23702371

2372+
typedef GDALAlgorithm *(*InstantiateAlgorithmCallback)(
2373+
const std::vector<std::string> &aosPath);
2374+
InstantiateAlgorithmCallback pfnInstantiateAlgorithm = nullptr;
2375+
2376+
virtual InstantiateAlgorithmCallback GetInstantiateAlgorithmCallback()
2377+
{
2378+
return pfnInstantiateAlgorithm;
2379+
}
2380+
2381+
/** Instantiate an algorithm by its full path (omitting leading "gdal").
2382+
* For example {"driver", "pdf", "list-layers"}
2383+
*/
2384+
GDALAlgorithm *
2385+
InstantiateAlgorithm(const std::vector<std::string> &aosPath);
2386+
2387+
/** Declare an algorithm by its full path (omitting leading "gdal").
2388+
* For example {"driver", "pdf", "list-layers"}
2389+
*/
2390+
void DeclareAlgorithm(const std::vector<std::string> &aosPath);
2391+
23712392
//! @endcond
23722393

23732394
/* -------------------------------------------------------------------- */
@@ -2517,6 +2538,8 @@ class GDALPluginDriverProxy : public GDALDriver
25172538
RenameCallback GetRenameCallback() override;
25182539

25192540
CopyFilesCallback GetCopyFilesCallback() override;
2541+
2542+
InstantiateAlgorithmCallback GetInstantiateAlgorithmCallback() override;
25202543
//! @endcond
25212544

25222545
CPLErr SetMetadataItem(const char *pszName, const char *pszValue,

0 commit comments

Comments
 (0)
Failed to load comments.