Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.0] - 2025-12-14

- Provide means of using O_DIRECT flag for linux.
- Provide parameters for irriversible compression.

## [0.3.1] - 2025-12-14

- Unify channel order handling.
Expand Down
19 changes: 11 additions & 8 deletions ojph/_imread.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import ctypes
from warnings import warn

from .ojph_bindings import J2CInfile, MemInfile, Codestream
from .ojph_bindings import J2CInfileWithFlags, MemInfile, Codestream

def imread(
uri,
Expand All @@ -16,6 +16,7 @@ def imread(
level=0,
skipped_res_for_data=None,
skipped_res_for_recon=None,
flags=None,
**kwargs,
):
if index is not None:
Expand All @@ -27,7 +28,7 @@ def imread(
if format_hint is not None:
warn(f"format_hint {format_hint} is ignored", stacklevel=2)

return OJPHImageFile(uri, channel_order=channel_order, offset=None).read_image(
return OJPHImageFile(uri, channel_order=channel_order, offset=None, flags=flags).read_image(
level=level,
skipped_res_for_data=skipped_res_for_data,
skipped_res_for_recon=skipped_res_for_recon,
Expand Down Expand Up @@ -71,7 +72,7 @@ def imread_from_memory(data, *, channel_order=None, level=0, skipped_res_for_dat


class OJPHImageFile:
def __init__(self, filename, *, mode='r', channel_order=None, offset=None):
def __init__(self, filename, *, mode='r', channel_order=None, offset=None, flags=None):
if mode != 'r':
raise ValueError(f"We only support mode = 'r' for now. Got {mode}.")
self._codestream = None
Expand All @@ -82,8 +83,9 @@ def __init__(self, filename, *, mode='r', channel_order=None, offset=None):
self._ojph_file = filename
self._is_mem_file = True
else:
ojph_file = J2CInfile()
ojph_file.open(str(filename))
ojph_file = J2CInfileWithFlags()
file_flags = flags if flags is not None else 0
ojph_file.open(str(filename), file_flags)
if offset is not None:
ojph_file.seek(offset, 0)
self._ojph_file = ojph_file
Expand Down Expand Up @@ -216,9 +218,10 @@ def dtype(self):
def levels(self):
return self._num_decompositions

def _open_file(self):
self._ojph_file = J2CInfile()
self._ojph_file.open(self._filename)
def _open_file(self, flags=None):
self._ojph_file = J2CInfileWithFlags()
file_flags = flags if flags is not None else 0
self._ojph_file.open(self._filename, file_flags)
self._codestream = Codestream()
self._codestream.read_headers(self._ojph_file)

Expand Down
19 changes: 12 additions & 7 deletions ojph/_imwrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import inspect
from collections.abc import Buffer

from .ojph_bindings import Codestream, J2COutfile, MemOutfile, Point
from .ojph_bindings import Codestream, J2COutfileWithFlags, MemOutfile, Point


class CompressedData(Buffer):
Expand All @@ -30,15 +30,15 @@ def __buffer__(self, flags: int) -> Buffer:
return self._memoryview


def imwrite_to_memory(image, *, channel_order=None, num_decompositions=None):
def imwrite_to_memory(image, *, channel_order=None, num_decompositions=None, reversible=None, qstep=None):
mem_outfile = MemOutfile()
mem_outfile.open(65536, False)
codestream = Codestream()
imwrite(mem_outfile, image, channel_order=channel_order, codestream=codestream, num_decompositions=num_decompositions)
imwrite(mem_outfile, image, channel_order=channel_order, codestream=codestream, num_decompositions=num_decompositions, reversible=reversible, qstep=qstep)
return np.asarray(CompressedData(mem_outfile, codestream))


def imwrite(filename, image, *, channel_order=None, codestream=None, num_decompositions=None):
def imwrite(filename, image, *, channel_order=None, codestream=None, num_decompositions=None, flags=None, reversible=None, qstep=None):
# Auto-detect channel order if not provided
if channel_order is None:
if image.ndim == 2:
Expand All @@ -65,8 +65,9 @@ def imwrite(filename, image, *, channel_order=None, codestream=None, num_decompo
if isinstance(filename, MemOutfile):
ojph_file = filename
else:
ojph_file = J2COutfile()
ojph_file.open(str(filename))
ojph_file = J2COutfileWithFlags()
file_flags = flags if flags is not None else 0
ojph_file.open(str(filename), file_flags)

close_codestream = codestream is None
if codestream is None:
Expand All @@ -93,10 +94,14 @@ def imwrite(filename, image, *, channel_order=None, codestream=None, num_decompo
is_signed,
)
cod = codestream.access_cod()
cod.set_reversible(True)
if reversible is None:
reversible = True
cod.set_reversible(reversible)
cod.set_color_transform(False)
if num_decompositions is not None:
cod.set_num_decomposition(num_decompositions)
if not reversible and qstep is not None:
codestream.access_qcd().set_irrev_quant(qstep)
codestream.set_planar(num_components > 1)

codestream.write_headers(ojph_file, None, 0)
Expand Down
160 changes: 160 additions & 0 deletions ojph/ojph_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
#include <pybind11/stl.h>
#include <pybind11/numpy.h>

#include <fcntl.h>
#include <unistd.h>
#include <cstdio>

#include <openjph/ojph_file.h>
#include <openjph/ojph_codestream.h>
Expand All @@ -11,6 +14,137 @@
namespace py = pybind11;
using namespace ojph;

class j2c_infile_with_flags : public infile_base {
public:
j2c_infile_with_flags() : fh(nullptr) {}
~j2c_infile_with_flags() override { close(); }

void open(const char* filename, int flags = 0) {
if (fh != nullptr) {
close();
}
if (flags == 0) {
fh = fopen(filename, "rb");
if (fh == nullptr) {
throw std::runtime_error("Failed to open file");
}
} else {
int fd = ::open(filename, flags | O_RDONLY);
if (fd < 0) {
throw std::runtime_error("Failed to open file with specified flags");
}
fh = fdopen(fd, "rb");
if (fh == nullptr) {
::close(fd);
throw std::runtime_error("Failed to convert file descriptor to FILE*");
}
}
}

size_t read(void *ptr, size_t size) override {
if (fh == nullptr) {
throw std::runtime_error("File not open");
}
return fread(ptr, 1, size, fh);
}

int seek(si64 offset, enum infile_base::seek origin) override {
if (fh == nullptr) {
throw std::runtime_error("File not open");
}
return ojph_fseek(fh, offset, origin);
}

si64 tell() override {
if (fh == nullptr) {
throw std::runtime_error("File not open");
}
return ojph_ftell(fh);
}

bool eof() override {
if (fh == nullptr) {
return true;
}
return feof(fh) != 0;
}

void close() override {
if (fh != nullptr) {
fclose(fh);
fh = nullptr;
}
}

private:
FILE* fh;
};

class j2c_outfile_with_flags : public outfile_base {
public:
j2c_outfile_with_flags() : fh(nullptr) {}
~j2c_outfile_with_flags() override { close(); }

void open(const char* filename, int flags = 0) {
if (fh != nullptr) {
close();
}
if (flags == 0) {
fh = fopen(filename, "wb");
if (fh == nullptr) {
throw std::runtime_error("Failed to open file");
}
} else {
int fd = ::open(filename, flags | O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
throw std::runtime_error("Failed to open file with specified flags");
}
fh = fdopen(fd, "wb");
if (fh == nullptr) {
::close(fd);
throw std::runtime_error("Failed to convert file descriptor to FILE*");
}
}
}

size_t write(const void *ptr, size_t size) override {
if (fh == nullptr) {
throw std::runtime_error("File not open");
}
return fwrite(ptr, 1, size, fh);
}

si64 tell() override {
if (fh == nullptr) {
throw std::runtime_error("File not open");
}
return ojph_ftell(fh);
}

void flush() override {
if (fh != nullptr) {
fflush(fh);
}
}

void close() override {
if (fh != nullptr) {
fclose(fh);
fh = nullptr;
}
}

int seek(si64 offset, enum outfile_base::seek origin) override {
if (fh == nullptr) {
throw std::runtime_error("File not open");
}
return ojph_fseek(fh, offset, origin);
}

private:
FILE* fh;
};

PYBIND11_MODULE(ojph_bindings, m) {
py::class_<infile_base>(m, "InfileBase")
.def("read", &infile_base::read)
Expand All @@ -30,6 +164,17 @@ PYBIND11_MODULE(ojph_bindings, m) {
.def("eof", &j2c_infile::eof)
.def("close", &j2c_infile::close);

py::class_<j2c_infile_with_flags, infile_base>(m, "J2CInfileWithFlags")
.def(py::init<>())
.def("open", &j2c_infile_with_flags::open, py::arg("filename"), py::arg("flags") = 0)
.def("read", &j2c_infile_with_flags::read)
.def("seek", [](infile_base& self, si64 offset, int origin) {
return self.seek(offset, static_cast<enum infile_base::seek>(origin));
})
.def("tell", &j2c_infile_with_flags::tell)
.def("eof", &j2c_infile_with_flags::eof)
.def("close", &j2c_infile_with_flags::close);

py::class_<mem_infile, infile_base>(m, "MemInfile")
.def(py::init<>())
.def("open", [](mem_infile& self, py::array_t<ui8> data) {
Expand Down Expand Up @@ -73,6 +218,17 @@ PYBIND11_MODULE(ojph_bindings, m) {
.def("tell", &j2c_outfile::tell)
.def("close", &j2c_outfile::close);

py::class_<j2c_outfile_with_flags, outfile_base>(m, "J2COutfileWithFlags")
.def(py::init<>())
.def("open", &j2c_outfile_with_flags::open, py::arg("filename"), py::arg("flags") = 0)
.def("write", &j2c_outfile_with_flags::write)
.def("tell", &j2c_outfile_with_flags::tell)
.def("flush", &j2c_outfile_with_flags::flush)
.def("seek", [](outfile_base& self, si64 offset, int origin) {
return self.seek(offset, static_cast<enum outfile_base::seek>(origin));
})
.def("close", &j2c_outfile_with_flags::close);

py::class_<mem_outfile, outfile_base>(m, "MemOutfile")
.def(py::init<>())
.def("open", &mem_outfile::open, py::arg("initial_size") = 65536, py::arg("clear_mem") = false)
Expand Down Expand Up @@ -186,6 +342,10 @@ PYBIND11_MODULE(ojph_bindings, m) {
.def("packets_use_eph", &param_cod::packets_use_eph)
.def("get_block_vertical_causality", &param_cod::get_block_vertical_causality);

py::class_<param_qcd>(m, "ParamQcd")
.def("set_irrev_quant", static_cast<void (param_qcd::*)(float)>(&param_qcd::set_irrev_quant), py::arg("delta"))
.def("set_irrev_quant", static_cast<void (param_qcd::*)(ui32, float)>(&param_qcd::set_irrev_quant), py::arg("comp_idx"), py::arg("delta"));

py::class_<line_buf, std::unique_ptr<line_buf, py::nodelete>>(m, "LineBuf")
.def(py::init<>())

Expand Down
49 changes: 49 additions & 0 deletions tests/test_flags_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import os
import sys
import pytest
import numpy as np

from ojph import imread, imwrite


@pytest.mark.skipif(sys.platform != "linux", reason="O_DIRECT is Linux-specific")
def test_imread_with_flags(tmp_path):
test_image = np.random.randint(0, 256, (64, 64), dtype=np.uint8)

filename = tmp_path / 'test.j2c'
imwrite(filename, test_image)

O_DIRECT = os.O_DIRECT
decoded_image = imread(filename, flags=O_DIRECT)

assert np.array_equal(test_image, decoded_image)
assert decoded_image.shape == test_image.shape
assert decoded_image.dtype == test_image.dtype


@pytest.mark.skipif(sys.platform != "linux", reason="O_DIRECT is Linux-specific")
def test_imwrite_with_flags(tmp_path):
test_image = np.random.randint(0, 256, (64, 64), dtype=np.uint8)

filename = tmp_path / 'test.j2c'
O_DIRECT = os.O_DIRECT
imwrite(filename, test_image, flags=O_DIRECT)

decoded_image = imread(filename)

assert np.array_equal(test_image, decoded_image)
assert decoded_image.shape == test_image.shape
assert decoded_image.dtype == test_image.dtype


def test_imread_imwrite_without_flags(tmp_path):
test_image = np.random.randint(0, 256, (64, 64), dtype=np.uint8)

filename = tmp_path / 'test.j2c'
imwrite(filename, test_image)

decoded_image = imread(filename)

assert np.array_equal(test_image, decoded_image)
assert decoded_image.shape == test_image.shape
assert decoded_image.dtype == test_image.dtype
Loading
Loading