mirror of
https://github.com/drewcassidy/quicktex.git
synced 2024-09-13 06:37:34 +00:00
Add BC1 tests
This commit is contained in:
parent
8ab324a661
commit
901dcc45df
@ -1,61 +0,0 @@
|
||||
/* Python-rgbcx Texture Compression Library
|
||||
Copyright (C) 2021 Andrew Cassidy <drewcassidy@me.com>
|
||||
Partially derived from rgbcx.h written by Richard Geldreich <richgel99@gmail.com>
|
||||
and licenced under the public domain
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "Texture.h"
|
||||
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
|
||||
#include "Color.h"
|
||||
|
||||
namespace quicktex {
|
||||
RawTexture::RawTexture(int width, int height) : Base(width, height) { _pixels = new Color[(size_t)(_width * _height)]; }
|
||||
|
||||
RawTexture::RawTexture(RawTexture&& other) : Base(other._width, other._height) {
|
||||
_pixels = other._pixels;
|
||||
other._pixels = nullptr;
|
||||
}
|
||||
|
||||
RawTexture::RawTexture(const RawTexture& other) : RawTexture(other._width, other._height) {
|
||||
std::memcpy(_pixels, other._pixels, (size_t)(_width * _height) * sizeof(Color));
|
||||
}
|
||||
|
||||
RawTexture& RawTexture::operator=(RawTexture other) noexcept {
|
||||
swap(*this, other);
|
||||
return *this;
|
||||
}
|
||||
|
||||
void swap(RawTexture& first, RawTexture& second) noexcept {
|
||||
using std::swap; // enable ADL
|
||||
swap(first._pixels, second._pixels);
|
||||
swap(first._width, second._width);
|
||||
swap(first._height, second._height);
|
||||
}
|
||||
|
||||
Color RawTexture::GetPixel(int x, int y) const {
|
||||
if (x < 0 || x >= _width) throw std::invalid_argument("x value out of range.");
|
||||
if (y < 0 || y >= _height) throw std::invalid_argument("y value out of range.");
|
||||
return _pixels[x + (y * _width)];
|
||||
}
|
||||
|
||||
void RawTexture::SetPixel(int x, int y, Color val) {
|
||||
if (x < 0 || x >= _width) throw std::invalid_argument("x value out of range.");
|
||||
if (y < 0 || y >= _height) throw std::invalid_argument("y value out of range.");
|
||||
_pixels[x + (y * _width)] = val;
|
||||
}
|
||||
} // namespace quicktex
|
@ -22,11 +22,13 @@
|
||||
#include <array>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "Color.h"
|
||||
#include "ColorBlock.h"
|
||||
@ -69,40 +71,25 @@ class RawTexture : public Texture {
|
||||
* @param width width of the texture in pixels
|
||||
* @param height height of the texture in pixels
|
||||
*/
|
||||
RawTexture(int width, int height);
|
||||
RawTexture(int width, int height) : Base(width, height), _pixels(_width * _height) {}
|
||||
|
||||
/**
|
||||
* Move constructor
|
||||
* @param other object to move
|
||||
*/
|
||||
RawTexture(RawTexture &&other);
|
||||
Color GetPixel(int x, int y) const {
|
||||
if (x < 0 || x >= _width) throw std::invalid_argument("x value out of range.");
|
||||
if (y < 0 || y >= _height) throw std::invalid_argument("y value out of range.");
|
||||
return _pixels.at(x + (y * _width));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor
|
||||
* @param other object to copy
|
||||
*/
|
||||
RawTexture(const RawTexture &other);
|
||||
void SetPixel(int x, int y, Color val) {
|
||||
if (x < 0 || x >= _width) throw std::invalid_argument("x value out of range.");
|
||||
if (y < 0 || y >= _height) throw std::invalid_argument("y value out of range.");
|
||||
_pixels.at(x + (y * _width)) = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* assignment operator
|
||||
* @param other object to copy (passed by value)
|
||||
* @return this
|
||||
*/
|
||||
RawTexture &operator=(RawTexture other) noexcept;
|
||||
|
||||
virtual ~RawTexture() override { delete[] _pixels; }
|
||||
|
||||
friend void swap(RawTexture &first, RawTexture &second) noexcept;
|
||||
|
||||
virtual Color GetPixel(int x, int y) const;
|
||||
|
||||
virtual void SetPixel(int x, int y, Color val);
|
||||
|
||||
virtual size_t Size() const noexcept override { return static_cast<unsigned long>(Width() * Height()) * sizeof(Color); }
|
||||
size_t Size() const noexcept override { return static_cast<unsigned long>(Width() * Height()) * sizeof(Color); }
|
||||
|
||||
template <int N, int M> ColorBlock<N, M> GetBlock(int block_x, int block_y) const {
|
||||
if (block_x < 0 || (block_x + 1) * N > _width) throw std::invalid_argument("x value out of range.");
|
||||
if (block_y < 0 || (block_y + 1) * M > _height) throw std::invalid_argument("y value out of range.");
|
||||
if (block_x < 0 || (block_x + 1) * N > _width) throw std::out_of_range("x value out of range.");
|
||||
if (block_y < 0 || (block_y + 1) * M > _height) throw std::out_of_range("y value out of range.");
|
||||
|
||||
// coordinates in the image of the top-left pixel of the selected block
|
||||
ColorBlock<N, M> block;
|
||||
@ -126,8 +113,8 @@ class RawTexture : public Texture {
|
||||
}
|
||||
|
||||
template <int N, int M> void SetBlock(int block_x, int block_y, const ColorBlock<N, M> &block) {
|
||||
if (block_x < 0) throw std::invalid_argument("x value out of range.");
|
||||
if (block_y < 0) throw std::invalid_argument("y value out of range.");
|
||||
if (block_x < 0) throw std::out_of_range("x value out of range.");
|
||||
if (block_y < 0) throw std::out_of_range("y value out of range.");
|
||||
|
||||
// coordinates in the image of the top-left pixel of the selected block
|
||||
int pixel_x = block_x * N;
|
||||
@ -147,14 +134,19 @@ class RawTexture : public Texture {
|
||||
}
|
||||
}
|
||||
|
||||
virtual const uint8_t *Data() const noexcept override { return reinterpret_cast<const uint8_t *>(_pixels); }
|
||||
virtual uint8_t *Data() noexcept override { return reinterpret_cast<uint8_t *>(_pixels); }
|
||||
virtual const uint8_t *Data() const noexcept override { return reinterpret_cast<const uint8_t *>(_pixels.data()); }
|
||||
virtual uint8_t *Data() noexcept override { return reinterpret_cast<uint8_t *>(_pixels.data()); }
|
||||
|
||||
protected:
|
||||
Color *_pixels;
|
||||
std::vector<Color> _pixels;
|
||||
};
|
||||
|
||||
template <typename B> class BlockTexture final: public Texture {
|
||||
template <typename B> class BlockTexture final : public Texture {
|
||||
private:
|
||||
std::vector<B> _blocks;
|
||||
int _width_b;
|
||||
int _height_b;
|
||||
|
||||
public:
|
||||
using BlockType = B;
|
||||
using Base = Texture;
|
||||
@ -164,65 +156,32 @@ template <typename B> class BlockTexture final: public Texture {
|
||||
* @param width width of the texture in pixels. must be divisible by B::Width
|
||||
* @param height height of the texture in pixels. must be divisible by B::Height
|
||||
*/
|
||||
BlockTexture(int width, int height) : Base(width, height) { _blocks = new B[(size_t)(BlocksX() * BlocksY())]; }
|
||||
|
||||
/**
|
||||
* Move constructor
|
||||
* @param other object to move
|
||||
*/
|
||||
BlockTexture(BlockTexture<B> &&other) : Base(other._width, other._height) {
|
||||
_blocks = other._blocks;
|
||||
other._blocks = nullptr;
|
||||
BlockTexture(int width, int height) : Base(width, height) {
|
||||
_width_b = (_width + B::Width - 1) / B::Width;
|
||||
_height_b = (_height + B::Height - 1) / B::Height;
|
||||
_blocks = std::vector<B>(_width_b * _height_b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor
|
||||
* @param other object to copy
|
||||
*/
|
||||
BlockTexture(const BlockTexture<B> &other) : BlockTexture(other._width, other._height) { std::memcpy(_blocks, other._blocks, Size()); }
|
||||
|
||||
/**
|
||||
* assignment operator
|
||||
* @param other object to copy (passed by value)
|
||||
* @return this
|
||||
*/
|
||||
BlockTexture &operator=(BlockTexture<B> other) {
|
||||
swap(*this, other);
|
||||
return *this;
|
||||
}
|
||||
|
||||
friend void swap(BlockTexture<B> &first, BlockTexture<B> &second) noexcept {
|
||||
using std::swap; // enable ADL
|
||||
swap(first._blocks, second._blocks);
|
||||
swap(first._width, second._width);
|
||||
swap(first._height, second._height);
|
||||
}
|
||||
|
||||
~BlockTexture() { delete[] _blocks; }
|
||||
|
||||
constexpr int BlocksX() const { return (_width + B::Width - 1) / B::Width; }
|
||||
constexpr int BlocksY() const { return (_height + B::Height - 1) / B::Height; }
|
||||
constexpr std::tuple<int, int> BlocksXY() const { return std::tuple<int, int>(BlocksX(), BlocksY()); }
|
||||
constexpr int BlocksX() const { return _width_b; }
|
||||
constexpr int BlocksY() const { return _height_b; }
|
||||
constexpr std::tuple<int, int> BlocksXY() const { return std::tuple<int, int>(_width_b, _height_b); }
|
||||
|
||||
B GetBlock(int x, int y) const {
|
||||
if (x < 0 || x >= BlocksX()) throw std::out_of_range("x value out of range.");
|
||||
if (y < 0 || y >= BlocksY()) throw std::out_of_range("y value out of range.");
|
||||
return _blocks[x + (y * _width)];
|
||||
if (x < 0 || x >= _width_b) throw std::out_of_range("x value out of range.");
|
||||
if (y < 0 || y >= _height_b) throw std::out_of_range("y value out of range.");
|
||||
return _blocks.at(x + (y * _width_b));
|
||||
}
|
||||
|
||||
void SetBlock(int x, int y, const B &val) {
|
||||
if (x < 0 || x >= BlocksX()) throw std::out_of_range("x value out of range.");
|
||||
if (y < 0 || y >= BlocksY()) throw std::out_of_range("y value out of range.");
|
||||
_blocks[x + (y * _width)] = val;
|
||||
if (x < 0 || x >= _width_b) throw std::out_of_range("x value out of range.");
|
||||
if (y < 0 || y >= _height_b) throw std::out_of_range("y value out of range.");
|
||||
_blocks.at(x + (y * _width_b)) = val;
|
||||
}
|
||||
|
||||
size_t Size() const noexcept override { return (size_t)(BlocksX() * BlocksY()) * sizeof(B); }
|
||||
size_t Size() const noexcept override { return _blocks.size() * sizeof(B); }
|
||||
|
||||
const uint8_t *Data() const noexcept override { return reinterpret_cast<const uint8_t *>(_blocks); }
|
||||
uint8_t *Data() noexcept override { return reinterpret_cast<uint8_t *>(_blocks); }
|
||||
|
||||
protected:
|
||||
B *_blocks;
|
||||
const uint8_t *Data() const noexcept override { return reinterpret_cast<const uint8_t *>(_blocks.data()); }
|
||||
uint8_t *Data() noexcept override{ return reinterpret_cast<uint8_t *>(_blocks.data()); }
|
||||
};
|
||||
|
||||
} // namespace quicktex
|
@ -52,7 +52,7 @@ PYBIND11_MODULE(_quicktex, m) {
|
||||
|
||||
// RawTexture
|
||||
|
||||
py::class_<RawTexture> raw_texture(m, "RawTexture", texture);
|
||||
py::class_<RawTexture, Texture> raw_texture(m, "RawTexture");
|
||||
|
||||
raw_texture.def(py::init<int, int>(), "width"_a, "height"_a);
|
||||
raw_texture.def_static("frombytes", &BufferToTexture<RawTexture>, "data"_a, "width"_a, "height"_a);
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/operators.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <cstdint>
|
||||
@ -140,7 +141,8 @@ template <typename T, typename Getter, typename Setter, typename Extent> void De
|
||||
"__setitem__", [set, ext](T& self, int index, V val) { (self.*set)(PyIndex(index, (self.*ext)()), val); }, "key"_a, "value"_a);
|
||||
}
|
||||
|
||||
template <typename T, typename Getter, typename Setter, typename Extent> void DefSubscript2D(py::class_<T> t, Getter&& get, Setter&& set, Extent&& ext) {
|
||||
template <typename Tpy, typename Getter, typename Setter, typename Extent> void DefSubscript2D(Tpy t, Getter&& get, Setter&& set, Extent&& ext) {
|
||||
using T = typename Tpy::type;
|
||||
using V = typename std::invoke_result<Getter, T*, int, int>::type;
|
||||
using Coords = std::tuple<int, int>;
|
||||
t.def(
|
||||
@ -154,7 +156,7 @@ template <typename T, typename Getter, typename Setter, typename Extent> void De
|
||||
"key"_a);
|
||||
t.def(
|
||||
"__setitem__",
|
||||
[set, ext](T& self, Coords pnt, V val) {
|
||||
[set, ext](T& self, Coords pnt, const V& val) {
|
||||
Coords s = (self.*ext)();
|
||||
int x = PyIndex(std::get<0>(pnt), std::get<0>(s), "x");
|
||||
int y = PyIndex(std::get<1>(pnt), std::get<1>(s), "y");
|
||||
@ -186,6 +188,8 @@ template <typename B> py::class_<B> BindBlock(py::module_& m, const char* name)
|
||||
block.def_property_readonly_static(
|
||||
"size", [](py::object) { return sizeof(B); }, "The size of the block in bytes.");
|
||||
|
||||
block.def(py::self == py::self);
|
||||
|
||||
block.def_buffer([](B& b) { return py::buffer_info(reinterpret_cast<uint8_t*>(&b), sizeof(B)); });
|
||||
block.def(
|
||||
"tobytes", [](const B& b) { return py::bytes(reinterpret_cast<const char*>(&b), sizeof(B)); },
|
||||
@ -216,7 +220,7 @@ template <typename B> py::class_<BlockTexture<B>> BindBlockTexture(py::module_&
|
||||
|
||||
using BTex = BlockTexture<B>;
|
||||
|
||||
py::class_<BTex> block_texture(m, name, py::type::of<Texture>(), py::is_final());
|
||||
py::class_<BTex, Texture> block_texture(m, name);
|
||||
|
||||
block_texture.def(py::init<int, int>(), "width"_a, "height"_a, Format(constructor_str, name).c_str());
|
||||
block_texture.def_static("from_bytes", &BufferToTexture<BTex>, "data"_a, "width"_a, "height"_a, Format(from_bytes_str, name).c_str());
|
||||
|
@ -38,8 +38,8 @@ void InitS3TC(py::module_ &m) {
|
||||
|
||||
InitInterpolator(s3tc);
|
||||
InitBC1(s3tc);
|
||||
InitBC4(s3tc);
|
||||
InitBC3(s3tc);
|
||||
InitBC5(s3tc);
|
||||
// InitBC4(s3tc);
|
||||
// InitBC3(s3tc);
|
||||
// InitBC5(s3tc);
|
||||
}
|
||||
} // namespace quicktex::bindings
|
||||
|
@ -122,5 +122,8 @@ class alignas(8) BC1Block {
|
||||
void SetSelectors(const SelectorArray& unpacked);
|
||||
|
||||
bool Is3Color() const { return GetColor0Raw() <= GetColor1Raw(); }
|
||||
|
||||
bool operator==(const BC1Block& other) const = default;
|
||||
bool operator!=(const BC1Block& other) const = default;
|
||||
};
|
||||
} // namespace quicktex::s3tc
|
@ -55,5 +55,8 @@ class alignas(8) BC3Block {
|
||||
alpha_block = blocks.first;
|
||||
color_block = blocks.second;
|
||||
}
|
||||
|
||||
bool operator==(const BC3Block& other) const = default;
|
||||
bool operator!=(const BC3Block& other) const = default;
|
||||
};
|
||||
} // namespace quicktex::s3tc
|
@ -99,6 +99,9 @@ class alignas(8) BC4Block {
|
||||
/// The interpolated values of this block as an array of 8 integers.
|
||||
std::array<uint8_t, 8> GetValues() const { return Is6Value() ? GetValues6() : GetValues8(); }
|
||||
|
||||
bool operator==(const BC4Block& other) const = default;
|
||||
bool operator!=(const BC4Block& other) const = default;
|
||||
|
||||
private:
|
||||
std::array<uint8_t, 8> GetValues6() const;
|
||||
std::array<uint8_t, 8> GetValues8() const;
|
||||
|
@ -53,5 +53,8 @@ class alignas(8) BC5Block {
|
||||
chan0_block = pair.first;
|
||||
chan1_block = pair.second;
|
||||
}
|
||||
|
||||
bool operator==(const BC5Block& other) const = default;
|
||||
bool operator!=(const BC5Block& other) const = default;
|
||||
};
|
||||
} // namespace quicktex::s3tc
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
@ -1,30 +0,0 @@
|
||||
from s3tc import BC1Block
|
||||
from color import Color
|
||||
from PIL import Image
|
||||
import os
|
||||
|
||||
image_path = os.path.dirname(os.path.realpath(__file__)) + "/images"
|
||||
|
||||
# A block that should always encode greyscale, where every row of pixels is identical, and the left side is lighter than the right side
|
||||
greyscale = Image.open(image_path + "/blocks/greyscale.png").tobytes("raw", "RGBX")
|
||||
|
||||
# A block that should always encode 3-color when available.
|
||||
# from left to right: red, yellow, yellow, green
|
||||
three_color = Image.open(image_path + "/blocks/3color.png").tobytes("raw", "RGBX")
|
||||
|
||||
# A block that should always encode 3-color with black when available
|
||||
# from left to right: black, red, yellow, green
|
||||
three_color_black = Image.open(image_path + "/blocks/3color black.png").tobytes("raw", "RGBX")
|
||||
|
||||
bc1_test_blocks = [
|
||||
# A block that should always encode greyscale, where every row of pixels is identical, and the left side is lighter than the right side
|
||||
{"name": "greyscale",
|
||||
"image": Image.open(image_path + "/blocks/greyscale.png").tobytes("raw", "RGBX"),
|
||||
"expected": BC1Block(Color(0xFF, 0xFF, 0xFF), Color(0x44, 0x44, 0x44), [[0, 2, 3, 1]] * 4)},
|
||||
|
||||
# A block that should always encode 3-color when available.
|
||||
# from left to right: red, yellow, yellow, green
|
||||
{"name": "3color",
|
||||
"image": Image.open(image_path + "/blocks/3color.png").tobytes("raw", "RGBX"),
|
||||
"expected": BC1Block(Color(0x00, 0xFF, 0x00), Color(0xFF, 0x00, 0x00), [[0, 2, 3, 1]] * 4)}
|
||||
]
|
@ -1,81 +0,0 @@
|
||||
class Color:
|
||||
def __init__(self, r=0, g=0, b=0, a=0xFF):
|
||||
self.r = r
|
||||
self.g = g
|
||||
self.b = b
|
||||
self.a = a
|
||||
|
||||
def __add__(self, a):
|
||||
return Color(self.r + a.r, self.g + a.g, self.b + a.b, self.a + a.a)
|
||||
|
||||
def __mul__(self, c):
|
||||
return Color(self.r * c, self.g * c, self.b * c, self.a * c)
|
||||
|
||||
def __rmul__(self, c):
|
||||
return Color(self.r * c, self.g * c, self.b * c, self.a * c)
|
||||
|
||||
def __iter__(self):
|
||||
return iter([self.r, self.g, self.b, self.a])
|
||||
|
||||
def __repr__(self):
|
||||
return f'r: {self.r} g: {self.g} b: {self.b} a: {self.a}'
|
||||
|
||||
def __str__(self):
|
||||
return self.to_hex()
|
||||
|
||||
def error(self, other):
|
||||
assert isinstance(other, Color)
|
||||
return ((self.r - other.r) ** 2) + ((self.g - other.g) ** 2) + ((self.b - other.b) ** 2)
|
||||
|
||||
@classmethod
|
||||
def from_565(cls, int_value):
|
||||
r = float((int_value & 0xF800) >> 11) / 0x1F
|
||||
g = float((int_value & 0x07E0) >> 5) / 0x3F
|
||||
b = float(int_value & 0x001F) / 0x1F
|
||||
|
||||
return cls(r, g, b)
|
||||
|
||||
def to_565(self):
|
||||
r = int(self.r * 0x1F)
|
||||
g = int(self.g * 0x3F)
|
||||
b = int(self.b * 0x1F)
|
||||
|
||||
return (r << 11) | (g << 5) | b
|
||||
|
||||
@classmethod
|
||||
def from_rgb24(cls, int_value):
|
||||
r = float((int_value & 0xFF0000) >> 16) / 0xFF
|
||||
g = float((int_value & 0x00FF00) >> 8) / 0xFF
|
||||
b = float(int_value & 0x0000FF) / 0xFF
|
||||
|
||||
return cls(r, g, b)
|
||||
|
||||
def to_rgb24(self):
|
||||
r = int(self.r * 0xFF)
|
||||
g = int(self.g * 0xFF)
|
||||
b = int(self.b * 0xFF)
|
||||
|
||||
return (r << 16) | (g << 8) | b
|
||||
|
||||
@classmethod
|
||||
def from_rgba32(cls, int_value):
|
||||
r = float((int_value & 0xFF000000) >> 24) / 0xFF
|
||||
g = float((int_value & 0x00FF0000) >> 16) / 0xFF
|
||||
b = float((int_value & 0x0000FF00) >> 8) / 0xFF
|
||||
a = float(int_value & 0x000000FF) / 0xFF
|
||||
|
||||
return cls(r, g, b, a)
|
||||
|
||||
def to_rgba32(self):
|
||||
r = int(self.r * 0xFF)
|
||||
g = int(self.g * 0xFF)
|
||||
b = int(self.b * 0xFF)
|
||||
a = int(self.a * 0xFF)
|
||||
|
||||
return (r << 24) | (g << 16) | (b << 8) | a
|
||||
|
||||
def to_hex(self):
|
||||
if self.a < 1:
|
||||
return hex(self.to_rgba32())
|
||||
else:
|
||||
return hex(self.to_rgb24())
|
@ -25,6 +25,7 @@
|
||||
#include <string>
|
||||
|
||||
namespace py = pybind11;
|
||||
using namespace pybind11::literals;
|
||||
|
||||
#define STRINGIFY(x) #x
|
||||
#define MACRO_STRINGIFY(x) STRINGIFY(x)
|
||||
@ -37,6 +38,7 @@ int main() {
|
||||
site.attr("addsitedir")(CUSTOM_SYS_PATH);
|
||||
|
||||
py::module_ nose = py::module_::import("nose");
|
||||
py::module_ tests = py::module_::import("test_texture");
|
||||
nose.attr("main")("test_texture");
|
||||
py::module_ tests = py::module_::import("tests");
|
||||
py::list argv(1);
|
||||
nose.attr("runmodule")("name"_a = "tests.test_bc1", "exit"_a = false);
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import struct
|
||||
import math
|
||||
import operator
|
||||
from functools import reduce
|
||||
from color import Color
|
||||
|
||||
|
||||
def bit_slice(value, size, count):
|
||||
mask = (2 ** size) - 1
|
||||
return [(value >> offset) & mask for offset in range(0, size * count, size)]
|
||||
|
||||
|
||||
def bit_merge(values, size):
|
||||
offsets = range(0, len(values) * size, size)
|
||||
return reduce(operator.__or__, map(operator.lshift, values, offsets))
|
||||
|
||||
|
||||
def triple_slice(triplet):
|
||||
values = bit_slice(bit_merge(triplet, 8), 3, 8)
|
||||
return [values[0:4], values[4:8]]
|
||||
|
||||
|
||||
def triple_merge(rows):
|
||||
values = rows[0] + rows[1]
|
||||
return bit_slice(bit_merge(values, 3), 8, 3)
|
||||
|
||||
|
||||
class BC1Block:
|
||||
size = 8
|
||||
|
||||
def __init__(self, color0=Color(), color1=Color(), selectors=[[0] * 4] * 4):
|
||||
self.color0 = color0
|
||||
self.color1 = color1
|
||||
self.selectors = selectors
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
def __str__(self):
|
||||
return f'color0: {str(self.color0)} color1: {str(self.color1)}, selectors:{self.selectors}'
|
||||
|
||||
@staticmethod
|
||||
def frombytes(data):
|
||||
block = struct.unpack_from('<2H4B', data)
|
||||
result = BC1Block()
|
||||
|
||||
result.color0 = Color.from_565(block[0])
|
||||
result.color1 = Color.from_565(block[1])
|
||||
result.selectors = [bit_slice(row, 2, 4) for row in block[2:6]]
|
||||
return result
|
||||
|
||||
def tobytes(self):
|
||||
return struct.pack('<2H4B',
|
||||
self.color0.to_565(), self.color1.to_565(),
|
||||
*(bit_merge(row, 2) for row in self.selectors))
|
||||
|
||||
def is_3color(self):
|
||||
return self.color0.to_565() <= self.color1.to_565()
|
||||
|
||||
def is_3color_black(self):
|
||||
return self.is_3color() and any(3 in row for row in self.selectors)
|
||||
|
||||
|
||||
class BC4Block:
|
||||
size = 8
|
||||
|
||||
def __init__(self):
|
||||
self.alpha0 = 1
|
||||
self.alpha1 = 1
|
||||
self.selectors = [[0] * 4] * 4
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.__dict__)
|
||||
|
||||
@staticmethod
|
||||
def frombytes(data):
|
||||
block = struct.unpack_from('<2B6B', data)
|
||||
result = BC4Block()
|
||||
|
||||
result.alpha0 = block[0] / 0xFF
|
||||
result.alpha1 = block[1] / 0xFF
|
||||
result.selectors = triple_slice(block[2:5]) + triple_slice(block[5:8])
|
||||
return result
|
||||
|
||||
def tobytes(self):
|
||||
return struct.pack('<2B6B',
|
||||
int(self.alpha0 * 0xFF), int(self.alpha1 * 0xFF),
|
||||
*triple_merge(self.selectors[0:2]),
|
||||
*triple_merge(self.selectors[2:4]))
|
@ -1,32 +1,122 @@
|
||||
import unittest
|
||||
import nose
|
||||
from parameterized import parameterized_class
|
||||
from s3tc import BC1Block
|
||||
from images import Blocks
|
||||
# import quicktex.s3tc.bc1 as bc1
|
||||
# import quicktex.s3tc.interpolator as interpolator
|
||||
#
|
||||
# ColorMode = bc1.BC1Encoder.ColorMode
|
||||
from parameterized import parameterized, parameterized_class
|
||||
import quicktex.s3tc.bc1 as bc1
|
||||
|
||||
in_endpoints = ((253, 254, 255), (65, 70, 67)) # has some small changes that should encode the same
|
||||
out_endpoints = ((255, 255, 255, 255), (66, 69, 66, 255))
|
||||
selectors = [[0, 2, 3, 1]] * 4
|
||||
block_bytes = b'\xff\xff\x28\x42\x78\x78\x78\x78'
|
||||
|
||||
|
||||
class TestBC1Block(unittest.TestCase):
|
||||
"""Tests for the BC1Block class"""
|
||||
|
||||
def test_size(self):
|
||||
"""Test the size and dimensions of BC1Block"""
|
||||
self.assertEqual(bc1.BC1Block.size, 8, 'incorrect block size')
|
||||
self.assertEqual(bc1.BC1Block.width, 4, 'incorrect block width')
|
||||
self.assertEqual(bc1.BC1Block.height, 4, 'incorrect block width')
|
||||
self.assertEqual(bc1.BC1Block.dimensions, (4, 4), 'incorrect block dimensions')
|
||||
|
||||
def test_buffer(self):
|
||||
"""Test the buffer protocol of BC1Block"""
|
||||
block = bc1.BC1Block()
|
||||
mv = memoryview(block)
|
||||
|
||||
self.assertFalse(mv.readonly, 'buffer is readonly')
|
||||
self.assertTrue(mv.c_contiguous, 'buffer is not contiguous')
|
||||
self.assertEqual(mv.ndim, 1, 'buffer is multidimensional')
|
||||
self.assertEqual(mv.nbytes, bc1.BC1Block.size, 'buffer is the wrong size')
|
||||
self.assertEqual(mv.format, 'B', 'buffer has the wrong format')
|
||||
|
||||
mv[:] = block_bytes
|
||||
self.assertEqual(mv.tobytes(), block_bytes, 'incorrect buffer data')
|
||||
|
||||
def test_constructor(self):
|
||||
"""Test constructing a block out of endpoints and selectors"""
|
||||
block = bc1.BC1Block(*in_endpoints, selectors)
|
||||
self.assertEqual(block.tobytes(), block_bytes, 'incorrect block bytes')
|
||||
self.assertEqual(block.selectors, selectors, 'incorrect selectors')
|
||||
self.assertEqual(block.endpoints, out_endpoints, 'incorrect endpoints')
|
||||
self.assertFalse(block.is_3color, 'incorrect color mode')
|
||||
|
||||
def test_frombytes(self):
|
||||
"""Test constructing a block out of raw data"""
|
||||
block = bc1.BC1Block.frombytes(block_bytes)
|
||||
self.assertEqual(block.tobytes(), block_bytes, 'incorrect block bytes')
|
||||
self.assertEqual(block.selectors, selectors, 'incorrect selectors')
|
||||
self.assertEqual(block.endpoints, out_endpoints, 'incorrect endpoints')
|
||||
self.assertFalse(block.is_3color, 'incorrect color mode')
|
||||
|
||||
def test_eq(self):
|
||||
block1 = bc1.BC1Block.frombytes(block_bytes)
|
||||
block2 = bc1.BC1Block.frombytes(block_bytes)
|
||||
self.assertEqual(block1, block2, 'identical blocks not equal')
|
||||
|
||||
|
||||
@parameterized_class(
|
||||
("name", "w", "h", "wb", "hb"), [
|
||||
("8x8", 8, 8, 2, 2),
|
||||
("9x9", 9, 9, 3, 3),
|
||||
("7x7", 7, 7, 2, 2),
|
||||
("7x9", 7, 9, 2, 3)
|
||||
])
|
||||
class TestBC1Texture(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tex = bc1.BC1Texture(self.w, self.h)
|
||||
self.size = self.wb * self.hb * bc1.BC1Block.size
|
||||
|
||||
def test_size(self):
|
||||
"""Test size and dimensions of BC1Texture"""
|
||||
self.assertEqual(self.tex.size, self.size, 'incorrect texture size')
|
||||
self.assertEqual(len(self.tex.tobytes()), self.size, 'incorrect texture size from tobytes')
|
||||
|
||||
self.assertEqual(self.tex.width, self.w, 'incorrect texture width')
|
||||
self.assertEqual(self.tex.height, self.h, 'incorrect texture height')
|
||||
self.assertEqual(self.tex.dimensions, (self.w, self.h), 'incorrect texture dimensions')
|
||||
|
||||
self.assertEqual(self.tex.width_blocks, self.wb, 'incorrect texture width_blocks')
|
||||
self.assertEqual(self.tex.height_blocks, self.hb, 'incorrect texture width_blocks')
|
||||
self.assertEqual(self.tex.dimensions_blocks, (self.wb, self.hb), 'incorrect texture dimensions_blocks')
|
||||
|
||||
def test_blocks(self):
|
||||
"""Test getting and setting blocks to BC1Texture"""
|
||||
blocks = [[bc1.BC1Block.frombytes(bytes([x, y] + [0] * 6)) for x in range(self.wb)] for y in range(self.hb)]
|
||||
for x in range(self.wb):
|
||||
for y in range(self.hb):
|
||||
self.tex[x, y] = blocks[y][x]
|
||||
|
||||
b = self.tex.tobytes()
|
||||
for x in range(self.wb):
|
||||
for y in range(self.hb):
|
||||
index = (x + (y * self.wb)) * bc1.BC1Block.size
|
||||
tb = self.tex[x, y]
|
||||
fb = bc1.BC1Block.frombytes(b[index:index + bc1.BC1Block.size])
|
||||
self.assertEqual(tb, blocks[y][x], 'incorrect block read from texture')
|
||||
self.assertEqual(fb, blocks[y][x], 'incorrect block read from texture bytes')
|
||||
|
||||
self.assertEqual(self.tex[-1, -1], self.tex[self.wb - 1, self.hb - 1], 'incorrect negative subscripting')
|
||||
|
||||
def test_buffer(self):
|
||||
"""Test the buffer protocol of BC1Texture"""
|
||||
mv = memoryview(self.tex)
|
||||
|
||||
self.assertFalse(mv.readonly, 'buffer is readonly')
|
||||
self.assertTrue(mv.c_contiguous, 'buffer is not contiguous')
|
||||
self.assertEqual(mv.ndim, 1, 'buffer is multidimensional')
|
||||
self.assertEqual(mv.nbytes, self.size, 'buffer is the wrong size')
|
||||
self.assertEqual(mv.format, 'B', 'buffer has the wrong format')
|
||||
|
||||
data = block_bytes * self.wb * self.hb
|
||||
mv[:] = data
|
||||
self.assertEqual(mv.tobytes(), data, 'incorrect buffer data')
|
||||
|
||||
|
||||
def get_class_name_blocks(cls, num, params_dict):
|
||||
return "%s%s" % (cls.__name__, params_dict['color_mode'].name,)
|
||||
|
||||
|
||||
#
|
||||
# class TestBC1Encoder(unittest.TestCase):
|
||||
# """Test BC1 Encoder"""
|
||||
#
|
||||
# def setUp(self):
|
||||
# self.bc1_encoder = bc1.BC1Encoder(5)
|
||||
#
|
||||
# def test_block_size(self):
|
||||
# """Ensure encoded block size is 8 bytes."""
|
||||
# out = self.bc1_encoder.encode_image(Blocks.greyscale, 4, 4)
|
||||
#
|
||||
# self.assertEqual(self.bc1_encoder.block_width, 4, 'incorrect reported block width')
|
||||
# self.assertEqual(self.bc1_encoder.block_height, 4, 'incorrect reported block height')
|
||||
# self.assertEqual(self.bc1_encoder.block_size, 8, 'incorrect reported block size')
|
||||
# self.assertEqual(len(out), 8, 'incorrect returned block size')
|
||||
#
|
||||
#
|
||||
# def get_class_name_blocks(cls, num, params_dict):
|
||||
# return "%s%s" % (cls.__name__, params_dict['color_mode'].name,)
|
||||
#
|
||||
#
|
||||
# @parameterized_class([
|
||||
@ -69,7 +159,7 @@ from images import Blocks
|
||||
# self.assertEqual(selectors, out.selectors, "block has incorrect selectors for 3 color with black test block")
|
||||
# else:
|
||||
# self.assertFalse(out.is_3color_black(), "returned incorrect block color mode for 3 color with black test block")
|
||||
#
|
||||
#
|
||||
# if __name__ == '__main__':
|
||||
# nose.main()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
nose.main()
|
||||
|
@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
import nose
|
||||
import quicktex
|
||||
import images
|
||||
import tests.images as images
|
||||
import tempfile
|
||||
from PIL import Image
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user