From 7cf630dd6732946df7b9a12af40636cfed267468 Mon Sep 17 00:00:00 2001 From: Oleksii Leonov Date: Thu, 2 May 2024 21:37:14 +0000 Subject: [PATCH] feat: added nearblack (GDALNearblack) --- lib/gdal/utils.rb | 1 + lib/gdal/utils/nearblack.rb | 119 ++++++++++++++++++ lib/gdal/utils/nearblack/options.rb | 52 ++++++++ .../unit/gdal/utils/nearblack/options_spec.rb | 40 ++++++ spec/unit/gdal/utils/nearblack_spec.rb | 97 ++++++++++++++ 5 files changed, 309 insertions(+) create mode 100644 lib/gdal/utils/nearblack.rb create mode 100644 lib/gdal/utils/nearblack/options.rb create mode 100644 spec/unit/gdal/utils/nearblack/options_spec.rb create mode 100644 spec/unit/gdal/utils/nearblack_spec.rb diff --git a/lib/gdal/utils.rb b/lib/gdal/utils.rb index 5cf6dc20..71ba8c95 100644 --- a/lib/gdal/utils.rb +++ b/lib/gdal/utils.rb @@ -12,6 +12,7 @@ module Utils # GDAL Utils autoload :DEM, File.expand_path("utils/dem", __dir__) autoload :Grid, File.expand_path("utils/grid", __dir__) + autoload :Nearblack, File.expand_path("utils/nearblack", __dir__) autoload :Rasterize, File.expand_path("utils/rasterize", __dir__) autoload :Info, File.expand_path("utils/info", __dir__) autoload :Translate, File.expand_path("utils/translate", __dir__) diff --git a/lib/gdal/utils/nearblack.rb b/lib/gdal/utils/nearblack.rb new file mode 100644 index 00000000..8b324d79 --- /dev/null +++ b/lib/gdal/utils/nearblack.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require_relative "nearblack/options" + +module GDAL + module Utils + # Wrapper for nearblack using GDALNearblack C API. + # + # @see https://gdal.org/programs/nearblack.html nearblack utility documentation. + # @see https://gdal.org/api/gdal_utils.html#_CPPv413GDALNearblackPKc12GDALDatasetH12GDALDatasetHPK20GDALNearblackOptionsPi + # GDALNearblack C API. + class Nearblack + # Perform the nearblack (GDALNearblack) operation. + # + # @example Perform nearblack on dataset (for dst_dataset_path). + # src_dataset = GDAL::Dataset.open("source.tif", "r") + # + # dataset = GDAL::Utils::Nearblack.perform(dst_dataset_path: "destination.tif", src_dataset: src_dataset) + # + # # Do something with the dataset. + # puts dataset.raster_x_size + # + # # You must close the dataset when you are done with it. + # dataset.close + # src_dataset.close + # + # @example Perform nearblack on dataset with options (for dst_dataset_path). + # src_dataset = GDAL::Dataset.open("source.tif", "r") + # options = GDAL::Utils::Nearblack::Options.new(options: ["-near", "10"]) + # + # dataset = GDAL::Utils::Nearblack.perform( + # dst_dataset_path: "destination.tif", + # src_dataset: src_dataset, + # options: options + # ) + # + # # Do something with the dataset. + # puts dataset.raster_x_size + # + # # You must close the dataset when you are done with it. + # dataset.close + # src_dataset.close + # + # @example Perform nearblack on dataset (for dst_dataset_path) using block syntax. + # src_dataset = GDAL::Dataset.open("source.tif", "r") + # options = GDAL::Utils::Nearblack::Options.new(options: ["-near", "10"]) + # + # GDAL::Utils::Nearblack.perform( + # dst_dataset_path: "destination.tif", + # src_dataset: src_dataset, + # options: options + # ) do |dataset| + # # Do something with the dataset. + # puts dataset.raster_x_size + # + # # Dataset will be closed automatically. + # end + # src_dataset.close + # + # @example Perform nearblack on dataset (for dst_dataset). + # src_dataset = GDAL::Dataset.open("source.tif", "r") + # dst_dataset = GDAL::Dataset.open("destination.tif", "w") + # + # GDAL::Utils::Nearblack.perform(dst_dataset: dst_dataset, src_dataset: src_dataset) + # + # # You must close the dataset when you are done with it. + # dst_dataset.close + # src_dataset.close + # + # @param dst_dataset_path [String] The path to the destination dataset. + # @param dst_dataset [GDAL::Dataset] The destination dataset. + # @param src_dataset [GDAL::Dataset] The source dataset. + # @param options [GDAL::Utils::Nearblack::Options] Options. + # @yield [GDAL::Dataset] The destination dataset. + # @return [GDAL::Dataset] The destination dataset (only if block is not specified; dataset must be closed). + def self.perform(src_dataset:, dst_dataset: nil, dst_dataset_path: nil, options: Options.new, &block) + if dst_dataset + for_dataset(dst_dataset: dst_dataset, src_dataset: src_dataset, options: options) + else + for_dataset_path(dst_dataset_path: dst_dataset_path, src_dataset: src_dataset, options: options, &block) + end + end + + def self.for_dataset(dst_dataset:, src_dataset:, options: Options.new) + result_dataset_ptr(dst_dataset: dst_dataset, src_dataset: src_dataset, options: options) + + # Return the input dataset as the output dataset (dataset is modified in place). + dst_dataset + end + private_class_method :for_dataset + + def self.for_dataset_path(dst_dataset_path:, src_dataset:, options: Options.new, &block) + dst_dataset_ptr = result_dataset_ptr( + dst_dataset_path: dst_dataset_path, src_dataset: src_dataset, options: options + ) + + ::GDAL::Dataset.open(dst_dataset_ptr, "w", &block) + end + private_class_method :for_dataset_path + + def self.result_dataset_ptr(src_dataset:, dst_dataset_path: nil, dst_dataset: nil, options: Options.new) + result_code_ptr = ::FFI::MemoryPointer.new(:int) + dst_dataset_ptr = ::FFI::GDAL::Utils.GDALNearblack( + dst_dataset_path, + dst_dataset&.c_pointer, + src_dataset.c_pointer, + options.c_pointer, + result_code_ptr + ) + success = result_code_ptr.read_int.zero? + + raise ::GDAL::Error, "GDALNearblack failed." if dst_dataset_ptr.null? || !success + + dst_dataset_ptr + end + private_class_method :result_dataset_ptr + end + end +end diff --git a/lib/gdal/utils/nearblack/options.rb b/lib/gdal/utils/nearblack/options.rb new file mode 100644 index 00000000..7a30ad30 --- /dev/null +++ b/lib/gdal/utils/nearblack/options.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module GDAL + module Utils + class Nearblack + # Ruby wrapper for GDALNearblackOptions C API (options for nearblack utility). + # + # @see GDAL::Utils::Nearblack + # @see https://gdal.org/programs/nearblack.html nearblack utility documentation. + class Options + # @private + class AutoPointer < ::FFI::AutoPointer + # @param pointer [FFI::Pointer] + def self.release(pointer) + return unless pointer && !pointer.null? + + ::FFI::GDAL::Utils.GDALNearblackOptionsFree(pointer) + end + end + + # @return [AutoPointer] C pointer to the GDALNearblackOptions. + attr_reader :c_pointer + + # @return [Array] The options. + attr_reader :options + + # Create a new instance. + # + # @see https://gdal.org/programs/nearblack.html + # List of available options could be found in nearblack utility documentation. + # + # @example Create a new instance. + # options = GDAL::Utils::Nearblack::Options.new(options: ["-of", "GTiff", "-near", "10"]) + # + # @param options [Array] The options list. + def initialize(options: []) + @options = options + @string_list = ::GDAL::Utils::Helpers::StringList.new(strings: options) + @c_pointer = AutoPointer.new(options_pointer) + end + + private + + attr_reader :string_list + + def options_pointer + ::FFI::GDAL::Utils.GDALNearblackOptionsNew(string_list.c_pointer, nil) + end + end + end + end +end diff --git a/spec/unit/gdal/utils/nearblack/options_spec.rb b/spec/unit/gdal/utils/nearblack/options_spec.rb new file mode 100644 index 00000000..1b56499f --- /dev/null +++ b/spec/unit/gdal/utils/nearblack/options_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" +require "gdal" + +RSpec.describe GDAL::Utils::Nearblack::Options do + context "when no options are provided" do + it "returns a new instance of Options" do + subject { described_class.new } + + expect(subject).to be_a(described_class) + expect(subject.c_pointer).to be_a(described_class::AutoPointer) + expect(subject.c_pointer).not_to be_null + end + end + + context "when options are provided" do + subject { described_class.new(options: options) } + + let(:options) { ["-of", "GTiff", "-near", "10"] } + + it "returns a new instance of Options with options" do + expect(subject).to be_a(described_class) + expect(subject.c_pointer).to be_a(described_class::AutoPointer) + expect(subject.c_pointer).not_to be_null + end + end + + context "when incorrect options are provided" do + subject { described_class.new(options: options) } + + let(:options) { ["-unknown123"] } + + it "raises exception" do + expect { subject }.to raise_exception( + GDAL::UnsupportedOperation, "Unknown option name '-unknown123'" + ) + end + end +end diff --git a/spec/unit/gdal/utils/nearblack_spec.rb b/spec/unit/gdal/utils/nearblack_spec.rb new file mode 100644 index 00000000..aa95fe23 --- /dev/null +++ b/spec/unit/gdal/utils/nearblack_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require "spec_helper" +require "gdal" + +RSpec.describe GDAL::Utils::Nearblack do + let(:src_dataset_path) do + path = "../../../../spec/support/images/osgeo/geotiff/GeogToWGS84GeoKey/GeogToWGS84GeoKey5.tif" + File.expand_path(path, __dir__) + end + + let(:src_dataset) { GDAL::Dataset.open(src_dataset_path, "r") } + after { src_dataset.close } + + describe ".perform" do + context "when dst_dataset_path used" do + let(:dst_dataset_path) { "/vsimem/test-#{SecureRandom.uuid}.tif" } + + context "when no options are provided" do + it "returns new dataset" do + new_dataset = described_class.perform(dst_dataset_path: dst_dataset_path, src_dataset: src_dataset) + + expect(new_dataset).to be_a(GDAL::Dataset) + expect(GDAL::Utils::Info.perform(dataset: new_dataset)).not_to include("Block=256x256") + + new_dataset.close + end + + it "returns new dataset in block" do + described_class.perform(dst_dataset_path: dst_dataset_path, src_dataset: src_dataset) do |new_dataset| + expect(new_dataset).to be_a(GDAL::Dataset) + end + end + end + + context "when options are provided" do + it "returns new dataset with options applied" do + options = GDAL::Utils::Nearblack::Options.new(options: ["-co", "TILED=YES", "-near", "10"]) + + new_dataset = described_class.perform( + dst_dataset_path: dst_dataset_path, src_dataset: src_dataset, options: options + ) + + expect(new_dataset).to be_a(GDAL::Dataset) + expect(GDAL::Utils::Info.perform(dataset: new_dataset)).to include("Block=256x256") + + new_dataset.close + end + end + + context "when operation fails without GDAL internal exception" do + it "raises exception" do + options = GDAL::Utils::Nearblack::Options.new(options: ["-of", "UnknownFormat123"]) + + expect do + described_class.perform(dst_dataset_path: dst_dataset_path, src_dataset: src_dataset, options: options) + end.to raise_exception( + GDAL::Error, "GDALNearblack failed." + ) + end + end + end + + context "when dst_dataset used" do + context "when no options are provided" do + it "returns dst_dataset with changes applied" do + dst_dataset_path = "/vsimem/test-#{SecureRandom.uuid}.tif" + dst_dataset = GDAL::Utils::Translate.perform(dst_dataset_path: dst_dataset_path, src_dataset: src_dataset) + + result_dataset = described_class.perform(dst_dataset: dst_dataset, src_dataset: src_dataset) + expect(result_dataset).to eq(dst_dataset) + + dst_dataset.close + end + end + + context "when operation fails with GDAL internal exception" do + it "raises exception" do + dst_dataset_path = "/vsimem/test-#{SecureRandom.uuid}.tif" + dst_dataset = GDAL::Utils::Translate.perform( + dst_dataset_path: dst_dataset_path, + src_dataset: src_dataset, + options: GDAL::Utils::Translate::Options.new(options: ["-outsize", "50%", "50%"]) + ) + + expect do + described_class.perform(dst_dataset: dst_dataset, src_dataset: src_dataset) + end.to raise_exception( + GDAL::Error, "The dimensions of the output dataset don't match the dimensions of the input dataset." + ) + + dst_dataset.close + end + end + end + end +end