Skip to content

Commit

Permalink
feat: added nearblack (GDALNearblack)
Browse files Browse the repository at this point in the history
  • Loading branch information
oleksii-leonov committed May 2, 2024
1 parent 7814cc8 commit 7cf630d
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/gdal/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
119 changes: 119 additions & 0 deletions lib/gdal/utils/nearblack.rb
Original file line number Diff line number Diff line change
@@ -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
52 changes: 52 additions & 0 deletions lib/gdal/utils/nearblack/options.rb
Original file line number Diff line number Diff line change
@@ -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<String>] 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<String>] 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
40 changes: 40 additions & 0 deletions spec/unit/gdal/utils/nearblack/options_spec.rb
Original file line number Diff line number Diff line change
@@ -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
97 changes: 97 additions & 0 deletions spec/unit/gdal/utils/nearblack_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 7cf630d

Please sign in to comment.