Skip to content

Commit 9330caa

Browse files
committed
Feature: Add support for zip textures.
Signed-off-by: Rgeditv1 <rgeditv1@protonmail.com>
1 parent cde2448 commit 9330caa

4 files changed

Lines changed: 369 additions & 60 deletions

File tree

‎common/ZipHelpers.h‎

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,128 @@
22
// SPDX-License-Identifier: GPL-3.0+
33

44
#pragma once
5+
#include <algorithm>
56
#include <memory>
67
#include <optional>
78
#include <string>
89
#include <vector>
10+
911
#include "zip.h"
1012

13+
#include "common/Path.h"
14+
#include "common/StringUtil.h"
1115
#include "Console.h"
1216

13-
static inline std::unique_ptr<zip_t, void (*)(zip_t*)> zip_open_managed(const char* filename, int flags, zip_error_t* ze)
17+
// ZIP replacement texture support
18+
static inline std::unique_ptr<zip_t, void (*)(zip_t*)> zip_open_managed(const char* filename, int flags, zip_error_t* ze);
19+
static inline std::unique_ptr<zip_file_t, int (*)(zip_file_t*)> zip_fopen_managed(zip_t* zip, const char* filename, zip_flags_t flags);
20+
static inline std::unique_ptr<zip_file_t, int (*)(zip_file_t*)> zip_fopen_index_managed(zip_t* zip, zip_uint64_t index, zip_flags_t flags);
21+
static inline std::optional<std::vector<u8>> ReadZipEntryBytes(const std::string& archive_path, const std::string& entry_name);
22+
static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_t* zip, const char* name);
23+
static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_file_t* file, u32 chunk_size = 4096);
24+
25+
static inline bool IsArchiveFile(std::string_view path)
1426
{
15-
zip_source_t* zs = zip_source_file_create(filename, 0, 0, ze);
16-
zip_t* zip = nullptr;
17-
if (zs && !(zip = zip_open_from_source(zs, flags, ze)))
18-
{
19-
// have to clean up source
20-
zip_source_free(zs);
21-
}
27+
const std::string_view ext = Path::GetExtension(path);
28+
return (StringUtil::Strncasecmp(ext.data(), "zip", ext.size()) == 0);
29+
}
2230

23-
return std::unique_ptr<zip_t, void (*)(zip_t*)>(zip, [](zip_t* zf) {
24-
if (!zf)
25-
return;
31+
static inline bool ArchiveEntryMatches(const std::string_view archive_entry_name, const std::string_view wanted_name)
32+
{
33+
const std::string_view archive_name = Path::GetFileName(archive_entry_name);
34+
const std::string_view normalized_wanted_name = Path::GetFileName(wanted_name);
2635

27-
int err = zip_close(zf);
28-
if (err != 0)
29-
{
30-
Console.Error("Failed to close zip file: %d", err);
31-
zip_discard(zf);
32-
}
33-
});
36+
if (archive_name.size() == normalized_wanted_name.size() &&
37+
StringUtil::Strncasecmp(archive_name.data(), normalized_wanted_name.data(), archive_name.size()) == 0)
38+
{
39+
return true;
40+
}
41+
42+
return (StringUtil::Strncasecmp(archive_entry_name.data(), wanted_name.data(), std::min(archive_entry_name.size(), wanted_name.size())) == 0) &&
43+
(archive_entry_name.size() == wanted_name.size());
3444
}
3545

36-
static inline std::unique_ptr<zip_t, void (*)(zip_t*)> zip_open_buffer_managed(const void* buffer, size_t size, int flags, int freep, zip_error_t* ze)
46+
static inline std::vector<std::string> ListArchiveEntryNames(const std::string& archive_path)
3747
{
38-
zip_source_t* zs = zip_source_buffer_create(buffer, size, freep, ze);
48+
std::vector<std::string> entries;
49+
const std::string_view ext = Path::GetExtension(archive_path);
50+
51+
if (StringUtil::Strncasecmp(ext.data(), "zip", ext.size()) != 0)
52+
return entries;
53+
54+
zip_error_t zip_error = {};
55+
auto archive = zip_open_managed(archive_path.c_str(), ZIP_RDONLY, &zip_error);
56+
if (!archive)
57+
return entries;
58+
59+
const zip_uint64_t total_entries = static_cast<zip_uint64_t>(zip_get_num_entries(archive.get(), 0));
60+
for (zip_uint64_t i = 0; i < total_entries; ++i)
61+
{
62+
const char* name = zip_get_name(archive.get(), i, 0);
63+
if (name)
64+
entries.emplace_back(name);
65+
}
66+
return entries;
67+
}
68+
69+
static inline std::optional<std::vector<u8>> ReadArchiveEntryBytes(const std::string& archive_path, const std::string& entry_name)
70+
{
71+
const std::string_view ext = Path::GetExtension(archive_path);
72+
if (StringUtil::Strncasecmp(ext.data(), "zip", ext.size()) == 0)
73+
return ReadZipEntryBytes(archive_path, entry_name);
74+
75+
return std::nullopt;
76+
}
77+
78+
static inline std::optional<std::vector<u8>> ReadZipEntryBytes(const std::string& archive_path, const std::string& entry_name)
79+
{
80+
zip_error_t zip_error = {};
81+
auto archive = zip_open_managed(archive_path.c_str(), ZIP_RDONLY, &zip_error);
82+
if (!archive)
83+
return std::nullopt;
84+
85+
zip_int64_t index = zip_name_locate(archive.get(), entry_name.c_str(), ZIP_FL_NOCASE);
86+
if (index < 0)
87+
{
88+
const std::string_view wanted_name = Path::GetFileName(entry_name);
89+
const zip_uint64_t total_entries = static_cast<zip_uint64_t>(zip_get_num_entries(archive.get(), 0));
90+
for (zip_uint64_t i = 0; i < total_entries; ++i)
91+
{
92+
const char* name = zip_get_name(archive.get(), i, 0);
93+
if (!name)
94+
continue;
95+
96+
const std::string_view archive_name = Path::GetFileName(name);
97+
if (archive_name.size() == wanted_name.size() &&
98+
StringUtil::Strncasecmp(archive_name.data(), wanted_name.data(), archive_name.size()) == 0)
99+
{
100+
index = static_cast<zip_int64_t>(i);
101+
break;
102+
}
103+
}
104+
}
105+
106+
if (index < 0)
107+
return std::nullopt;
108+
109+
zip_stat_t stat = {};
110+
if (zip_stat_index(archive.get(), index, ZIP_FL_NOCASE, &stat) != 0)
111+
return std::nullopt;
112+
113+
auto file = zip_fopen_index_managed(archive.get(), static_cast<zip_uint64_t>(index), ZIP_FL_NOCASE);
114+
if (!file)
115+
return std::nullopt;
116+
117+
auto data = ReadBinaryFileInZip(file.get());
118+
if (!data.has_value())
119+
return std::nullopt;
120+
121+
return data;
122+
}
123+
124+
static inline std::unique_ptr<zip_t, void (*)(zip_t*)> zip_open_managed(const char* filename, int flags, zip_error_t* ze)
125+
{
126+
zip_source_t* zs = zip_source_file_create(filename, 0, 0, ze);
39127
zip_t* zip = nullptr;
40128
if (zs && !(zip = zip_open_from_source(zs, flags, ze)))
41129
{
@@ -136,7 +224,7 @@ static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_t* zip, con
136224
return ReadFileInZipToContainer<std::vector<u8>>(zip, name);
137225
}
138226

139-
static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_file_t* file, u32 chunk_size = 4096)
227+
static inline std::optional<std::vector<u8>> ReadBinaryFileInZip(zip_file_t* file, u32 chunk_size)
140228
{
141229
return ReadFileInZipToContainer<std::vector<u8>>(file, chunk_size);
142230
}

‎pcsx2/GS/Renderers/HW/GSTextureReplacementLoaders.cpp‎

Lines changed: 148 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
#include "common/Path.h"
88
#include "common/StringUtil.h"
99
#include "common/ScopedGuard.h"
10+
#include "common/ZipHelpers.h"
1011

1112
#include "GS/Renderers/HW/GSTextureReplacements.h"
1213

14+
#include <cstdio>
1315
#include <csetjmp>
1416
#include <png.h>
1517

@@ -146,7 +148,7 @@ static void ConvertTexture_R8G8B8(u32 width, u32 height, std::vector<u8>& data,
146148
// PNG Handlers
147149
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
148150

149-
bool PNGLoader(const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
151+
static bool PNGLoaderFromFile(FILE* fp, const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
150152
{
151153
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
152154
if (!png_ptr)
@@ -163,14 +165,106 @@ bool PNGLoader(const std::string& filename, GSTextureReplacements::ReplacementTe
163165
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
164166
});
165167

168+
if (setjmp(png_jmpbuf(png_ptr)))
169+
return false;
170+
171+
png_init_io(png_ptr, fp);
172+
png_read_info(png_ptr, info_ptr);
173+
174+
png_uint_32 width = 0;
175+
png_uint_32 height = 0;
176+
int bitDepth = 0;
177+
int colorType = -1;
178+
if (png_get_IHDR(png_ptr, info_ptr, &width, &height, &bitDepth, &colorType, nullptr, nullptr, nullptr) != 1 ||
179+
width == 0 || height == 0)
180+
{
181+
return false;
182+
}
183+
184+
const u32 pitch = width * sizeof(u32);
185+
tex->width = width;
186+
tex->height = height;
187+
tex->format = GSTexture::Format::Color;
188+
tex->pitch = pitch;
189+
tex->data.resize(pitch * height);
190+
191+
const png_uint_32 row_bytes = png_get_rowbytes(png_ptr, info_ptr);
192+
std::vector<u8> row_data(row_bytes);
193+
194+
for (u32 y = 0; y < height; y++)
195+
{
196+
png_read_row(png_ptr, static_cast<png_bytep>(row_data.data()), nullptr);
197+
198+
const u8* row_ptr = row_data.data();
199+
u8* out_ptr = tex->data.data() + y * pitch;
200+
if (colorType == PNG_COLOR_TYPE_RGB)
201+
{
202+
for (u32 x = 0; x < width; x++)
203+
{
204+
u32 pixel = static_cast<u32>(*(row_ptr)++);
205+
pixel |= static_cast<u32>(*(row_ptr)++) << 8;
206+
pixel |= static_cast<u32>(*(row_ptr)++) << 16;
207+
pixel |= 0x80000000u; // make opaque
208+
std::memcpy(out_ptr, &pixel, sizeof(pixel));
209+
out_ptr += sizeof(pixel);
210+
}
211+
}
212+
else if (colorType == PNG_COLOR_TYPE_RGBA)
213+
{
214+
std::memcpy(out_ptr, row_ptr, pitch);
215+
}
216+
}
217+
218+
return true;
219+
}
220+
221+
bool PNGLoader(const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
222+
{
166223
auto fp = FileSystem::OpenManagedCFile(filename.c_str(), "rb");
167224
if (!fp)
168225
return false;
169226

227+
return PNGLoaderFromFile(fp.get(), filename, tex, only_base_image);
228+
}
229+
230+
static void PNGReadFromMemory(png_structp png_ptr, png_bytep out, png_size_t count)
231+
{
232+
auto* src = static_cast<std::vector<u8>*>(png_get_io_ptr(png_ptr));
233+
if (src->empty())
234+
{
235+
png_error(png_ptr, "unexpected end of PNG data");
236+
return;
237+
}
238+
239+
const size_t available = std::min<size_t>(count, src->size());
240+
std::memcpy(out, src->data(), available);
241+
src->erase(src->begin(), src->begin() + static_cast<std::ptrdiff_t>(available));
242+
if (available < count)
243+
png_error(png_ptr, "unexpected end of PNG data");
244+
}
245+
246+
static bool PNGLoaderFromBuffer(const std::vector<u8>& data, const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
247+
{
248+
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
249+
if (!png_ptr)
250+
return false;
251+
252+
png_infop info_ptr = png_create_info_struct(png_ptr);
253+
if (!info_ptr)
254+
{
255+
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
256+
return false;
257+
}
258+
259+
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
260+
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
261+
});
262+
263+
std::vector<u8> buffer = data;
170264
if (setjmp(png_jmpbuf(png_ptr)))
171265
return false;
172266

173-
png_init_io(png_ptr, fp.get());
267+
png_set_read_fn(png_ptr, &buffer, PNGReadFromMemory);
174268
png_read_info(png_ptr, info_ptr);
175269

176270
png_uint_32 width = 0;
@@ -206,7 +300,7 @@ bool PNGLoader(const std::string& filename, GSTextureReplacements::ReplacementTe
206300
u32 pixel = static_cast<u32>(*(row_ptr)++);
207301
pixel |= static_cast<u32>(*(row_ptr)++) << 8;
208302
pixel |= static_cast<u32>(*(row_ptr)++) << 16;
209-
pixel |= 0x80000000u; // make opaque
303+
pixel |= 0x80000000u;
210304
std::memcpy(out_ptr, &pixel, sizeof(pixel));
211305
out_ptr += sizeof(pixel);
212306
}
@@ -593,25 +687,21 @@ static bool ReadDDSMipLevel(std::FILE* fp, const std::string& filename, u32 mip_
593687
return true;
594688
}
595689

596-
bool DDSLoader(const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
690+
static bool DDSLoaderFromFile(FILE* fp, const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
597691
{
598-
auto fp = FileSystem::OpenManagedCFile(filename.c_str(), "rb");
599-
if (!fp)
600-
return false;
601-
602692
DDSLoadInfo info;
603-
if (!ParseDDSHeader(fp.get(), &info))
693+
if (!ParseDDSHeader(fp, &info))
604694
return false;
605695

606696
// always load the base image
607-
if (FileSystem::FSeek64(fp.get(), info.base_image_offset, SEEK_SET) != 0)
697+
if (FileSystem::FSeek64(fp, info.base_image_offset, SEEK_SET) != 0)
608698
return false;
609699

610700
tex->format = info.format;
611701
tex->width = info.width;
612702
tex->height = info.height;
613703
tex->pitch = info.base_image_pitch;
614-
if (!ReadDDSMipLevel(fp.get(), filename, 0, info, tex->width, tex->height, tex->data, tex->pitch, info.base_image_size))
704+
if (!ReadDDSMipLevel(fp, filename, 0, info, tex->width, tex->height, tex->data, tex->pitch, info.base_image_size))
615705
return false;
616706

617707
// Read in any remaining mip levels in the file.
@@ -622,7 +712,7 @@ bool DDSLoader(const std::string& filename, GSTextureReplacements::ReplacementTe
622712
GSTextureReplacements::ReplacementTexture::MipData md;
623713
u32 mip_size;
624714
CalcBlockMipmapSize(info.block_size, info.bytes_per_block, info.width, info.height, level, md.width, md.height, md.pitch, mip_size);
625-
if (!ReadDDSMipLevel(fp.get(), filename, level, info, md.width, md.height, md.data, md.pitch, mip_size))
715+
if (!ReadDDSMipLevel(fp, filename, level, info, md.width, md.height, md.data, md.pitch, mip_size))
626716
break;
627717

628718
tex->mips.push_back(std::move(md));
@@ -631,3 +721,49 @@ bool DDSLoader(const std::string& filename, GSTextureReplacements::ReplacementTe
631721

632722
return true;
633723
}
724+
725+
bool DDSLoader(const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
726+
{
727+
auto fp = FileSystem::OpenManagedCFile(filename.c_str(), "rb");
728+
if (!fp)
729+
return false;
730+
731+
return DDSLoaderFromFile(fp.get(), filename, tex, only_base_image);
732+
}
733+
734+
static bool DDSLoaderFromBuffer(const std::vector<u8>& data, const std::string& filename, GSTextureReplacements::ReplacementTexture* tex, bool only_base_image)
735+
{
736+
FILE* fp = std::tmpfile();
737+
if (!fp)
738+
return false;
739+
740+
ScopedGuard cleanup([&fp]() {
741+
std::fclose(fp);
742+
});
743+
744+
if (std::fwrite(data.data(), 1, data.size(), fp) != data.size())
745+
return false;
746+
if (std::fflush(fp) != 0)
747+
return false;
748+
std::rewind(fp);
749+
750+
return DDSLoaderFromFile(fp, filename, tex, only_base_image);
751+
}
752+
753+
bool GSTextureReplacements::LoadImageFromArchiveEntry(const std::string& archive_path,
754+
const std::string& normalized_name,
755+
GSTextureReplacements::ReplacementTexture* tex,
756+
bool only_base_image)
757+
{
758+
const auto data = ReadArchiveEntryBytes(archive_path, normalized_name);
759+
if (!data.has_value())
760+
return false;
761+
762+
const std::string_view ext = Path::GetExtension(normalized_name);
763+
if (StringUtil::Strncasecmp(ext.data(), "png", ext.size()) == 0)
764+
return PNGLoaderFromBuffer(*data, normalized_name, tex, only_base_image);
765+
if (StringUtil::Strncasecmp(ext.data(), "dds", ext.size()) == 0)
766+
return DDSLoaderFromBuffer(*data, normalized_name, tex, only_base_image);
767+
768+
return false;
769+
}

0 commit comments

Comments
 (0)