mirror of
https://github.com/drewcassidy/quicktex.git
synced 2024-09-13 06:37:34 +00:00
Add encode command
This commit is contained in:
parent
560acb20ea
commit
0152e99c44
@ -88,8 +88,8 @@ class RawTexture : public Texture {
|
||||
size_t NBytes() 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::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.");
|
||||
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
|
||||
ColorBlock<N, M> block;
|
||||
@ -181,7 +181,7 @@ template <typename B> class BlockTexture final : public Texture {
|
||||
size_t NBytes() const noexcept override { return _blocks.size() * sizeof(B); }
|
||||
|
||||
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()); }
|
||||
uint8_t *Data() noexcept override { return reinterpret_cast<uint8_t *>(_blocks.data()); }
|
||||
};
|
||||
|
||||
} // namespace quicktex
|
@ -53,8 +53,8 @@ def path_pairs(inputs, output, suffix, extension):
|
||||
# decode to a file
|
||||
if len(inputs) > 1:
|
||||
raise click.BadOptionUsage('output', 'Output is a single file, but multiple input files were provided.')
|
||||
if outpath.suffix not in decoded_extensions:
|
||||
raise click.BadOptionUsage('output', f'File has incorrect extension for decoded file. Valid extensions are:\n{decoded_extensions}')
|
||||
# if outpath.suffix not in decoded_extensions:
|
||||
# raise click.BadOptionUsage('output', f'File has incorrect extension for decoded file. Valid extensions are:\n{decoded_extensions}')
|
||||
|
||||
return [(inpath, outpath) for inpath in inpaths]
|
||||
else:
|
||||
|
@ -1,6 +1,139 @@
|
||||
import click
|
||||
import os
|
||||
import pathlib
|
||||
import quicktex.s3tc.bc1
|
||||
import quicktex.s3tc.bc3
|
||||
import quicktex.s3tc.bc4
|
||||
import quicktex.s3tc.bc5
|
||||
import quicktex.dds as dds
|
||||
import quicktex.cli.common as common
|
||||
from PIL import Image
|
||||
|
||||
|
||||
@click.group()
|
||||
def encode():
|
||||
"""Encode an input image to a texture file of the given format"""
|
||||
"""Decode DDS files to images of the given format."""
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('-f/-F', '--flip/--no-flip', default=True, show_default=True, help="Vertically flip image before converting.")
|
||||
@click.option('-r', '--remove', is_flag=True, help="Remove input images after converting.")
|
||||
@click.option('-s', '--suffix', type=str, default='', help="Suffix to append to output file(s). Ignored if output is a single file.")
|
||||
@click.option('-o', '--output',
|
||||
type=click.Path(writable=True), default=None,
|
||||
help="Output file or directory. If outputting to a file, input filenames must be only a single item. By default, files are decoded in place.")
|
||||
@click.argument('filenames', nargs=-1, type=click.Path(exists=True, readable=True, dir_okay=False))
|
||||
def encode_format(encoder, four_cc, flip, remove, suffix, output, filenames):
|
||||
path_pairs = common.path_pairs(filenames, output, suffix, '.dds')
|
||||
|
||||
with click.progressbar(path_pairs, show_eta=False, show_pos=True, item_show_func=lambda x: str(x[0]) if x else '') as bar:
|
||||
for inpath, outpath in bar:
|
||||
image = Image.open(inpath)
|
||||
|
||||
if flip:
|
||||
image = image.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
|
||||
dds.encode(image, encoder, four_cc).save(outpath)
|
||||
|
||||
if remove:
|
||||
os.remove(inpath)
|
||||
|
||||
|
||||
@click.command('auto')
|
||||
@click.option('-l', '--level', type=click.IntRange(0, 18), default=18, help='Quality level to use. Higher values = higher quality, but slower.')
|
||||
@click.option('-b/-B', '--black/--no-black',
|
||||
help='[BC1 only] Enable 3-color mode for blocks containing black or very dark pixels. --3color must also be enabled for this to work.'
|
||||
' (Important: engine/shader MUST ignore decoded texture alpha if this flag is enabled!)')
|
||||
@click.option('-3/-4', '--3color/--4color', 'threecolor', default=True, help='[BC1 only] Enable 3-color mode for non-black pixels. Higher quality, but slightly slower.')
|
||||
@click.option('-f/-F', '--flip/--no-flip', default=True, show_default=True, help="Vertically flip image before converting.")
|
||||
@click.option('-r', '--remove', is_flag=True, help="Remove input images after converting.")
|
||||
@click.option('-s', '--suffix', type=str, default='', help="Suffix to append to output file(s). Ignored if output is a single file.")
|
||||
@click.option('-o', '--output',
|
||||
type=click.Path(writable=True), default=None,
|
||||
help="Output file or directory. If outputting to a file, input filenames must be only a single item. By default, files are decoded in place.")
|
||||
@click.argument('filenames', nargs=-1, type=click.Path(exists=True, readable=True, dir_okay=False))
|
||||
def encode_auto(level, black, threecolor, flip, remove, suffix, output, filenames):
|
||||
"""Encode images to BC1 or BC3, with the format chosen based on each image's alpha channel."""
|
||||
|
||||
color_mode = quicktex.s3tc.bc1.BC1Encoder.ColorMode
|
||||
if not threecolor:
|
||||
mode = color_mode.FourColor
|
||||
elif not black:
|
||||
mode = color_mode.ThreeColor
|
||||
else:
|
||||
mode = color_mode.ThreeColorBlack
|
||||
|
||||
bc1_encoder = quicktex.s3tc.bc1.BC1Encoder(level, mode)
|
||||
bc3_encoder = quicktex.s3tc.bc3.BC3Encoder(level)
|
||||
path_pairs = common.path_pairs(filenames, output, suffix, '.dds')
|
||||
|
||||
with click.progressbar(path_pairs, show_eta=False, show_pos=True, item_show_func=lambda x: str(x[0]) if x else '') as bar:
|
||||
for inpath, outpath in bar:
|
||||
image = Image.open(inpath)
|
||||
|
||||
if flip:
|
||||
image = image.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
|
||||
if 'A' not in image.mode:
|
||||
has_alpha = False
|
||||
else:
|
||||
alpha_hist = image.getchannel('A').histogram()
|
||||
has_alpha = any([a > 0 for a in alpha_hist[:-1]])
|
||||
|
||||
if has_alpha:
|
||||
dds.encode(image, bc3_encoder, 'DXT5').save(outpath)
|
||||
else:
|
||||
dds.encode(image, bc1_encoder, 'DXT1').save(outpath)
|
||||
|
||||
if remove:
|
||||
os.remove(inpath)
|
||||
|
||||
|
||||
@click.command('bc1')
|
||||
@click.option('-l', '--level', type=click.IntRange(0, 18), default=18, help='Quality level to use. Higher values = higher quality, but slower.')
|
||||
@click.option('-b/-B', '--black/--no-black',
|
||||
help='Enable 3-color mode for blocks containing black or very dark pixels. --3color must also be enabled for this to work.'
|
||||
' (Important: engine/shader MUST ignore decoded texture alpha if this flag is enabled!)')
|
||||
@click.option('-3/-4', '--3color/--4color', 'threecolor', default=True, help='Enable 3-color mode for non-black pixels. Higher quality, but slightly slower.')
|
||||
def encode_bc1(level, black, threecolor, **kwargs):
|
||||
"""Encode images to BC1 (RGB, no alpha)."""
|
||||
color_mode = quicktex.s3tc.bc1.BC1Encoder.ColorMode
|
||||
if not threecolor:
|
||||
mode = color_mode.FourColor
|
||||
elif not black:
|
||||
mode = color_mode.ThreeColor
|
||||
else:
|
||||
mode = color_mode.ThreeColorBlack
|
||||
|
||||
encode_format.callback(encoder=quicktex.s3tc.bc1.BC1Encoder(level, mode), four_cc='DXT1', **kwargs)
|
||||
|
||||
|
||||
@click.command('bc3')
|
||||
@click.option('-l', '--level', type=click.IntRange(0, 18), default=18, help='Quality level to use. Higher values = higher quality, but slower.')
|
||||
def encode_bc3(level, **kwargs):
|
||||
"""Encode images to BC4 (RGBA, 8-bit interpolated alpha)."""
|
||||
encode_format.callback(quicktex.s3tc.bc3.BC3Encoder(level), 'DXT5', **kwargs)
|
||||
|
||||
|
||||
@click.command('bc4')
|
||||
def encode_bc4(**kwargs):
|
||||
"""Encode images to BC4 (Single channel, 8-bit interpolated red channel)."""
|
||||
encode_format.callback(quicktex.s3tc.bc4.BC4Encoder(), 'ATI1', **kwargs)
|
||||
|
||||
|
||||
@click.command('bc5')
|
||||
def encode_bc5(**kwargs):
|
||||
"""Encode images to BC5 (2-channel, 8-bit interpolated red and green channels)."""
|
||||
encode_format.callback(quicktex.s3tc.bc5.BC5Encoder(), 'ATI2', **kwargs)
|
||||
|
||||
|
||||
encode_bc1.params += encode_format.params
|
||||
encode_bc3.params += encode_format.params
|
||||
encode_bc4.params += encode_format.params
|
||||
encode_bc5.params += encode_format.params
|
||||
|
||||
encode.add_command(encode_bc1)
|
||||
encode.add_command(encode_bc3)
|
||||
encode.add_command(encode_bc4)
|
||||
encode.add_command(encode_bc5)
|
||||
encode.add_command(encode_auto)
|
||||
|
@ -87,6 +87,19 @@ class DDSFlags(enum.IntFlag):
|
||||
TEXTURE = CAPS | HEIGHT | WIDTH | PIXEL_FORMAT
|
||||
|
||||
|
||||
class Caps0(enum.IntFlag):
|
||||
"""Flags to indicate surface complexity"""
|
||||
|
||||
COMPLEX = 0x8
|
||||
"""Optional; must be used on any file that contains more than one surface (a mipmap, a cubic environment map, or mipmapped volume texture)."""
|
||||
|
||||
MIPMAP = 0x400000
|
||||
"""Optional; should be used for a mipmap."""
|
||||
|
||||
TEXTURE = 0x1000
|
||||
"""Required"""
|
||||
|
||||
|
||||
@typing.final
|
||||
class DDSFile:
|
||||
"""
|
||||
@ -134,7 +147,7 @@ class DDSFile:
|
||||
self.pixel_bitmasks: typing.Tuple[int, int, int, int] = (0, 0, 0, 0)
|
||||
"""Tuple of bitmasks for each channel"""
|
||||
|
||||
self.caps: typing.Tuple[int, int, int, int] = (0, 0, 0, 0)
|
||||
self.caps: typing.Tuple[Caps0, int, int, int] = (Caps0.TEXTURE, 0, 0, 0)
|
||||
"""Specifies the complexity of the surfaces stored."""
|
||||
|
||||
self.textures: typing.List = []
|
||||
@ -149,12 +162,6 @@ class DDSFile:
|
||||
:param path: string or path-like object to write to
|
||||
"""
|
||||
with open(path, 'wb') as file:
|
||||
self.size = self.textures[0].size
|
||||
self.pitch = self.textures[0].nbytes
|
||||
self.mipmap_count = len(self.textures)
|
||||
|
||||
assert quicktex.image_utils.mip_sizes(self.size, self.mipmap_count) == [tex.size for tex in self.textures], 'incorrect mipmap sizes'
|
||||
|
||||
file.write(DDSFile.magic)
|
||||
|
||||
# WRITE HEADER
|
||||
@ -241,3 +248,33 @@ def read(path: os.PathLike) -> DDSFile:
|
||||
dds.textures.append(texture)
|
||||
|
||||
return dds
|
||||
|
||||
|
||||
def encode(image: Image.Image, encoder, four_cc: str, mip_count: typing.Optional[int] = None) -> DDSFile:
|
||||
if image.mode != 'RGBA' or image.mode != 'RGBX':
|
||||
mode = 'RGBA' if 'A' in image.mode else 'RGBX'
|
||||
image = image.convert(mode)
|
||||
|
||||
sizes = quicktex.image_utils.mip_sizes(image.size, mip_count)
|
||||
images = [image] + [image.resize(size, Image.BILINEAR) for size in sizes[1:]]
|
||||
dds = DDSFile()
|
||||
|
||||
for i in images:
|
||||
rawtex = quicktex.RawTexture.frombytes(i.tobytes('raw', mode), *i.size)
|
||||
dds.textures.append(encoder.encode(rawtex))
|
||||
|
||||
dds.flags = DDSFlags.TEXTURE | DDSFlags.LINEAR_SIZE
|
||||
caps0 = Caps0.TEXTURE
|
||||
|
||||
if len(images) > 1:
|
||||
dds.flags |= DDSFlags.MIPMAPCOUNT
|
||||
caps0 |= Caps0.MIPMAP | Caps0.COMPLEX
|
||||
|
||||
dds.caps = (caps0, 0, 0, 0)
|
||||
dds.mipmap_count = len(images)
|
||||
dds.pitch = dds.textures[0].nbytes
|
||||
dds.size = dds.textures[0].size
|
||||
dds.pf_flags = PFFlags.FOURCC
|
||||
dds.four_cc = four_cc
|
||||
|
||||
return dds
|
||||
|
@ -367,7 +367,7 @@ BC1Block BC1Encoder::WriteBlockSolid(Color color) const {
|
||||
min16 = result.low.Pack565Unscaled();
|
||||
max16 = result.high.Pack565Unscaled();
|
||||
|
||||
if (result.solid) {
|
||||
if (result.color_mode == ColorMode::FourColor) {
|
||||
if (min16 == max16) {
|
||||
// make sure this isnt accidentally a 3-color block
|
||||
// so make max16 > min16 (l > h)
|
||||
|
@ -143,9 +143,9 @@ class BC1Encoder final : public BlockEncoder<BlockTexture<BC1Block>> {
|
||||
struct EncodeResults {
|
||||
Color low;
|
||||
Color high;
|
||||
std::array<uint8_t, 16> selectors;
|
||||
ColorMode color_mode;
|
||||
bool solid;
|
||||
std::array<uint8_t, 16> selectors = {0};
|
||||
ColorMode color_mode = ColorMode::Incomplete;
|
||||
bool solid = false;
|
||||
unsigned error = UINT_MAX;
|
||||
};
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import unittest
|
||||
import nose
|
||||
import os.path
|
||||
from parameterized import parameterized, parameterized_class
|
||||
import quicktex
|
||||
from quicktex.s3tc.bc1 import BC1Block, BC1Texture, BC1Encoder, BC1Decoder
|
||||
from tests.images import BC1Blocks
|
||||
from tests.images import BC1Blocks, image_path
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
in_endpoints = ((253, 254, 255), (65, 70, 67)) # has some small changes that should encode the same
|
||||
@ -180,6 +182,11 @@ class TestBC1Encoder(unittest.TestCase):
|
||||
else:
|
||||
self.assertFalse(out_block.is_3color, 'returned 3-color block in 4-color mode')
|
||||
|
||||
def test_image(self):
|
||||
image = Image.open(os.path.join(image_path, 'Bun.png'))
|
||||
rawtex = quicktex.RawTexture.frombytes(image.tobytes('raw', 'RGBA'), *image.size)
|
||||
out_tex = self.bc1_encoder.encode(rawtex)
|
||||
|
||||
|
||||
class TestBC1Decoder(unittest.TestCase):
|
||||
"""Test BC1Decoder"""
|
||||
|
Loading…
Reference in New Issue
Block a user