Skip to content

Add a mechanism to register external algorithm, in particular for drivers #12126

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 14, 2025
2 changes: 0 additions & 2 deletions apps/data/gdal_algorithm.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@
},
"required": [
"description",
"short_url",
"url",
"sub_algorithms",
"input_arguments",
"output_arguments",
Expand Down
76 changes: 76 additions & 0 deletions autotest/cpp/test_gdal_algorithm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3696,4 +3696,80 @@ TEST_F(test_gdal_algorithm, raster_edit_failures_unset_metadata)
"edit: SetMetadataItem('foo', NULL) failed");
}

TEST_F(test_gdal_algorithm, register_plugin_algorithms)
{
auto &singleton = GDALGlobalAlgorithmRegistry::GetSingleton();
bool flag = false;
singleton.DeclareAlgorithm(
{"foo", "bar"},
[&flag]() -> std::unique_ptr<GDALAlgorithm>
{
flag = true;
return std::make_unique<GDALContainerAlgorithm>("dummy");
});

{
EXPECT_NE(singleton.Instantiate("foo"), nullptr);
EXPECT_FALSE(flag);
}

{
auto got = singleton.GetDeclaredSubAlgorithmNames({"gdal"});
EXPECT_TRUE(std::find(got.begin(), got.end(), "foo") != got.end());
EXPECT_FALSE(flag);
}

{
auto got = singleton.GetDeclaredSubAlgorithmNames({"gdal", "foo"});
EXPECT_TRUE(std::find(got.begin(), got.end(), "bar") != got.end());
EXPECT_TRUE(flag);
flag = false;
}

{
auto got =
singleton.GetDeclaredSubAlgorithmNames({"gdal", "foo", "bar"});
EXPECT_TRUE(got.empty());
EXPECT_FALSE(flag);
}

{
auto got = singleton.GetDeclaredSubAlgorithmNames({"gdal", "bar"});
EXPECT_TRUE(got.empty());
EXPECT_FALSE(flag);
}

{
auto alg = singleton.InstantiateDeclaredSubAlgorithm({"gdal", "foo"});
ASSERT_NE(alg, nullptr);
EXPECT_TRUE(alg->HasSubAlgorithms());
EXPECT_EQ(alg->GetSubAlgorithmNames().size(), 1);
EXPECT_TRUE(flag);
flag = false;
}

{
auto alg =
singleton.InstantiateDeclaredSubAlgorithm({"gdal", "foo", "bar"});
ASSERT_NE(alg, nullptr);
EXPECT_TRUE(flag);
flag = false;
}

{
auto alg = singleton.Instantiate("foo")->InstantiateSubAlgorithm("bar");
ASSERT_NE(alg, nullptr);
EXPECT_TRUE(flag);
}

{
auto alg = singleton.InstantiateDeclaredSubAlgorithm({"gdal", "bar"});
ASSERT_EQ(alg, nullptr);
}

singleton.DeclareAlgorithm({"foo", "bar"},
[]() -> std::unique_ptr<GDALAlgorithm>
{ return nullptr; });
}

} // namespace test_gdal_algorithm
33 changes: 33 additions & 0 deletions autotest/gcore/driver_algorithms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env pytest
# -*- coding: utf-8 -*-
###############################################################################
# Project: GDAL/OGR Test Suite
# Purpose: Check that drivers that declare algorithms actually implement them
# Author: Even Rouault <even dot rouault @ spatialys.com>
#
###############################################################################
# Copyright (c) 2025, Even Rouault <even dot rouault at spatialys.com>
#
# SPDX-License-Identifier: MIT
###############################################################################


from osgeo import gdal


def check(alg, expected_name):
assert alg.GetName() == expected_name
if alg.HasSubAlgorithms():
for subalg_name in alg.GetSubAlgorithmNames():
subalg = alg[subalg_name]
assert subalg, subalg_name
check(subalg, subalg_name)

assert alg.InstantiateSubAlgorithm("i_do_not_exist") is None


def test_driver_algorithms():

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]
if alg:
check(alg, "driver")
43 changes: 43 additions & 0 deletions autotest/gdrivers/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# SPDX-License-Identifier: MIT
###############################################################################

import json
import os
import shutil
import sys
Expand Down Expand Up @@ -3097,3 +3098,45 @@ def test_pdf_iso32000_invalid_srs():

# Just test that this does not crash
gdal.Open("data/pdf/invalid_srs.pdf")


###############################################################################
#


@gdaltest.enable_exceptions()
@pytest.mark.skipif(not have_read_support(), reason="no read support available")
def test_pdf_gdal_driver_pdf_list_layer():

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]
assert alg.GetName() == "driver"

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["pdf"]
assert alg.GetName() == "pdf"

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["pdf"]["list-layers"]
assert alg.GetName() == "list-layers"
alg["input"] = "data/pdf/adobe_style_geospatial.pdf"
assert alg.Run()
assert json.loads(alg["output-string"]) == [
"New_Data_Frame",
"New_Data_Frame.Graticule",
"Layers",
"Layers.Measured_Grid",
"Layers.Graticule",
]

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["pdf"]["list-layers"]
alg["input"] = "data/pdf/adobe_style_geospatial.pdf"
alg["output-format"] = "text"
assert alg.Run()
assert (
alg["output-string"]
== "New_Data_Frame\nNew_Data_Frame.Graticule\nLayers\nLayers.Measured_Grid\nLayers.Graticule\n"
)

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["pdf"]["list-layers"]
assert alg.GetName() == "list-layers"
alg["input"] = "data/byte.tif"
with pytest.raises(Exception, match="is not a PDF"):
alg.Run()
42 changes: 42 additions & 0 deletions autotest/ogr/ogr_openfilegdb_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -4601,3 +4601,45 @@ def test_ogr_openfilegdb_write_check_golden_file(tmp_path, src_directory):
assert (
open(src_filename, "rb").read() == open(out_filename, "rb").read()
), filename


###############################################################################
# Test 'gdal driver openfilegdb repack'


@gdaltest.enable_exceptions()
def test_ogropenfilegdb_write_gdal_driver_openfilegdb_repack(tmp_path):

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]
assert alg.GetName() == "driver"

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["openfilegdb"]
assert alg.GetName() == "openfilegdb"

out_directory = str(tmp_path / "test.gdb")
gdal.VectorTranslate(
out_directory, "data/openfilegdb/polygon_golden.gdb", format="OpenFileGDB"
)

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["openfilegdb"]["repack"]
assert alg.GetName() == "repack"
alg["input"] = "data/poly.shp"
with pytest.raises(Exception, match="is not a FileGeoDatabase"):
alg.Run()

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["openfilegdb"]["repack"]
assert alg.GetName() == "repack"
alg["input"] = out_directory
assert alg.Run()

last_pct = [0]

def my_progress(pct, msg, user_data):
last_pct[0] = pct
return True

alg = gdal.GetGlobalAlgorithmRegistry()["driver"]["openfilegdb"]["repack"]
assert alg.GetName() == "repack"
alg["input"] = out_directory
assert alg.Run(my_progress)
assert last_pct[0] == 1.0
85 changes: 85 additions & 0 deletions frmts/pdf/pdfdataset.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@

#include "gdal_pdf.h"

#include "cpl_json_streaming_writer.h"
#include "cpl_vsi_virtual.h"
#include "cpl_spawn.h"
#include "cpl_string.h"
#include "gdal_frmts.h"
#include "gdalalgorithm.h"
#include "ogr_spatialref.h"
#include "ogr_geometry.h"

Expand Down Expand Up @@ -7711,6 +7713,88 @@ CPLString PDFSanitizeLayerName(const char *pszName)
return osName;
}

/************************************************************************/
/* GDALPDFListLayersAlgorithm */
/************************************************************************/

#ifdef HAVE_PDF_READ_SUPPORT

class GDALPDFListLayersAlgorithm final : public GDALAlgorithm
{
public:
GDALPDFListLayersAlgorithm()
: GDALAlgorithm("list-layers",
std::string("List layers of a PDF dataset"),
"/drivers/raster/pdf.html")
{
AddInputDatasetArg(&m_dataset, GDAL_OF_RASTER | GDAL_OF_VECTOR);
AddOutputFormatArg(&m_format).SetDefault(m_format).SetChoices("json",
"text");
AddOutputStringArg(&m_output);
}

protected:
bool RunImpl(GDALProgressFunc, void *) override
{
auto poDS = dynamic_cast<PDFDataset *>(m_dataset.GetDatasetRef());
if (!poDS)
{
ReportError(CE_Failure, CPLE_AppDefined, "%s is not a PDF",
m_dataset.GetName().c_str());
return false;
}
if (m_format == "json")
{
CPLJSonStreamingWriter oWriter(nullptr, nullptr);
oWriter.StartArray();
for (const auto &[key, value] : cpl::IterateNameValue(
const_cast<CSLConstList>(poDS->GetMetadata("LAYERS"))))
{
CPL_IGNORE_RET_VAL(key);
oWriter.Add(value);
}
oWriter.EndArray();
m_output = oWriter.GetString();
m_output += '\n';
}
else
{
for (const auto &[key, value] : cpl::IterateNameValue(
const_cast<CSLConstList>(poDS->GetMetadata("LAYERS"))))
{
CPL_IGNORE_RET_VAL(key);
m_output += value;
m_output += '\n';
}
}
return true;
}

private:
GDALArgDatasetValue m_dataset{};
std::string m_format = "json";
std::string m_output{};
};

/************************************************************************/
/* GDALPDFInstantiateAlgorithm() */
/************************************************************************/

static GDALAlgorithm *
GDALPDFInstantiateAlgorithm(const std::vector<std::string> &aosPath)
{
if (aosPath.size() == 1 && aosPath[0] == "list-layers")
{
return std::make_unique<GDALPDFListLayersAlgorithm>().release();
}
else
{
return nullptr;
}
}

#endif // HAVE_PDF_READ_SUPPORT

/************************************************************************/
/* GDALRegister_PDF() */
/************************************************************************/
Expand All @@ -7729,6 +7813,7 @@ void GDALRegister_PDF()

#ifdef HAVE_PDF_READ_SUPPORT
poDriver->pfnOpen = PDFDataset::OpenWrapper;
poDriver->pfnInstantiateAlgorithm = GDALPDFInstantiateAlgorithm;
#endif // HAVE_PDF_READ_SUPPORT

poDriver->pfnCreateCopy = GDALPDFCreateCopy;
Expand Down
2 changes: 2 additions & 0 deletions frmts/pdf/pdfdrivercore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ void PDFDriverSetCommonMetadata(GDALDriver *poDriver)
poDriver->SetMetadataItem(GDAL_DCAP_UPDATE, "YES");
poDriver->SetMetadataItem(GDAL_DMD_UPDATE_ITEMS,
"GeoTransform SRS GCPs DatasetMetadata");

poDriver->DeclareAlgorithm({"list-layers"});
#endif

poDriver->SetMetadataItem(GDAL_DCAP_CREATE, "YES");
Expand Down
23 changes: 23 additions & 0 deletions gcore/gdal_priv.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class GDALProxyDataset;
class GDALProxyRasterBand;
class GDALAsyncReader;
class GDALRelationship;
class GDALAlgorithm;

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

typedef GDALAlgorithm *(*InstantiateAlgorithmCallback)(
const std::vector<std::string> &aosPath);
InstantiateAlgorithmCallback pfnInstantiateAlgorithm = nullptr;

virtual InstantiateAlgorithmCallback GetInstantiateAlgorithmCallback()
{
return pfnInstantiateAlgorithm;
}

/** Instantiate an algorithm by its full path (omitting leading "gdal").
* For example {"driver", "pdf", "list-layers"}
*/
GDALAlgorithm *
InstantiateAlgorithm(const std::vector<std::string> &aosPath);

/** Declare an algorithm by its full path (omitting leading "gdal").
* For example {"driver", "pdf", "list-layers"}
*/
void DeclareAlgorithm(const std::vector<std::string> &aosPath);

//! @endcond

/* -------------------------------------------------------------------- */
Expand Down Expand Up @@ -2517,6 +2538,8 @@ class GDALPluginDriverProxy : public GDALDriver
RenameCallback GetRenameCallback() override;

CopyFilesCallback GetCopyFilesCallback() override;

InstantiateAlgorithmCallback GetInstantiateAlgorithmCallback() override;
//! @endcond

CPLErr SetMetadataItem(const char *pszName, const char *pszValue,
Expand Down
Loading
Loading