mirror of
https://github.com/ARMSX2/ARMSX1.git
synced 2026-08-24 16:53:35 -07:00
609 lines
18 KiB
C
609 lines
18 KiB
C
/*
|
|
ARMSX PS1 core — save-state preview thumbnails. See thumbnail.h for what is
|
|
captured and why the encoder is here rather than in the front-end.
|
|
*/
|
|
|
|
#include "thumbnail.h"
|
|
#include "state.h"
|
|
#include "psx.h"
|
|
#include "bus_init.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Checksums */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/* Nibble-table CRC-32 (polynomial 0xedb88320, reflected), used for the PNG
|
|
chunk CRCs. A 16-entry table rather than 256 keeps the constant data small
|
|
without making the per-byte cost matter at thumbnail sizes. */
|
|
static uint32_t png_crc32(uint32_t crc, const uint8_t* data, size_t size) {
|
|
static const uint32_t table[16] = {
|
|
0x00000000u, 0x1db71064u, 0x3b6e20c8u, 0x26d930acu,
|
|
0x76dc4190u, 0x6b6b51f4u, 0x4db26158u, 0x5005713cu,
|
|
0xedb88320u, 0xf00f9344u, 0xd6d6a3e8u, 0xcb61b38cu,
|
|
0x9b64c2b0u, 0x86d3d2d4u, 0xa00ae278u, 0xbdbdf21cu
|
|
};
|
|
size_t i;
|
|
|
|
crc = ~crc;
|
|
|
|
for (i = 0; i < size; i++) {
|
|
crc = table[(crc ^ data[i]) & 0x0f] ^ (crc >> 4);
|
|
crc = table[(crc ^ (data[i] >> 4)) & 0x0f] ^ (crc >> 4);
|
|
}
|
|
|
|
return ~crc;
|
|
}
|
|
|
|
/* Adler-32 of the UNCOMPRESSED data — the zlib stream's trailer. */
|
|
static uint32_t png_adler32(const uint8_t* data, size_t size) {
|
|
uint32_t a = 1;
|
|
uint32_t b = 0;
|
|
size_t i;
|
|
|
|
for (i = 0; i < size; i++) {
|
|
a = (a + data[i]) % 65521u;
|
|
b = (b + a) % 65521u;
|
|
}
|
|
|
|
return (b << 16) | a;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Bit-level output */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
/* psx_state_writer_t already is a growable byte buffer with a sticky error flag
|
|
(state.h), so this only adds DEFLATE's bit accumulator on top of it. */
|
|
typedef struct {
|
|
psx_state_writer_t w;
|
|
uint32_t bits;
|
|
int bit_count;
|
|
} png_bitwriter_t;
|
|
|
|
/* DEFLATE packs bits into bytes starting at the least-significant bit.
|
|
bit_count is always < 8 on entry and count is at most 13 (the widest extra-
|
|
bits field), so the accumulator never overflows. */
|
|
static void png_put_bits(png_bitwriter_t* b, uint32_t value, int count) {
|
|
if (count <= 0)
|
|
return;
|
|
|
|
b->bits |= (value & ((1u << count) - 1u)) << b->bit_count;
|
|
b->bit_count += count;
|
|
|
|
while (b->bit_count >= 8) {
|
|
psx_sw_u8(&b->w, (uint8_t)(b->bits & 0xffu));
|
|
|
|
b->bits >>= 8;
|
|
b->bit_count -= 8;
|
|
}
|
|
}
|
|
|
|
/* Huffman codes go out most-significant bit FIRST, which is the opposite order
|
|
to everything else in the stream, so the code is reversed before packing
|
|
(RFC 1951 3.1.1). */
|
|
static void png_put_code(png_bitwriter_t* b, uint32_t code, int count) {
|
|
uint32_t reversed = 0;
|
|
int i;
|
|
|
|
for (i = 0; i < count; i++)
|
|
reversed = (reversed << 1) | ((code >> i) & 1u);
|
|
|
|
png_put_bits(b, reversed, count);
|
|
}
|
|
|
|
/* The fixed literal/length alphabet, RFC 1951 3.2.6. */
|
|
static void png_put_symbol(png_bitwriter_t* b, int symbol) {
|
|
if (symbol < 144)
|
|
png_put_code(b, (uint32_t)(0x30 + symbol), 8);
|
|
else if (symbol < 256)
|
|
png_put_code(b, (uint32_t)(0x190 + (symbol - 144)), 9);
|
|
else if (symbol < 280)
|
|
png_put_code(b, (uint32_t)(symbol - 256), 7);
|
|
else
|
|
png_put_code(b, (uint32_t)(0xc0 + (symbol - 280)), 8);
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* DEFLATE (one fixed-Huffman block, greedy LZ77) */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
static const uint16_t png_length_base[29] = {
|
|
3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59,
|
|
67, 83, 99, 115, 131, 163, 195, 227, 258
|
|
};
|
|
|
|
static const uint8_t png_length_extra[29] = {
|
|
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
|
|
4, 4, 4, 4, 5, 5, 5, 5, 0
|
|
};
|
|
|
|
static const uint16_t png_dist_base[30] = {
|
|
1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513,
|
|
769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577
|
|
};
|
|
|
|
static const uint8_t png_dist_extra[30] = {
|
|
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8,
|
|
9, 9, 10, 10, 11, 11, 12, 12, 13, 13
|
|
};
|
|
|
|
static void png_put_match(png_bitwriter_t* b, int length, int distance) {
|
|
int code = 28;
|
|
int dist_code = 29;
|
|
|
|
while (code > 0 && png_length_base[code] > length)
|
|
code--;
|
|
|
|
png_put_symbol(b, 257 + code);
|
|
png_put_bits(b, (uint32_t)(length - png_length_base[code]), png_length_extra[code]);
|
|
|
|
while (dist_code > 0 && png_dist_base[dist_code] > distance)
|
|
dist_code--;
|
|
|
|
/* Fixed distance codes are 5-bit, and the code IS the index. */
|
|
png_put_code(b, (uint32_t)dist_code, 5);
|
|
png_put_bits(b, (uint32_t)(distance - png_dist_base[dist_code]), png_dist_extra[dist_code]);
|
|
}
|
|
|
|
#define PNG_HASH_BITS 15
|
|
#define PNG_HASH_SIZE (1 << PNG_HASH_BITS)
|
|
#define PNG_MIN_MATCH 3
|
|
#define PNG_MAX_MATCH 258
|
|
#define PNG_WINDOW 32768
|
|
|
|
static uint32_t png_hash3(const uint8_t* p) {
|
|
uint32_t key = ((uint32_t)p[0] << 16) ^ ((uint32_t)p[1] << 8) ^ (uint32_t)p[2];
|
|
|
|
return (key * 2654435761u) >> (32 - PNG_HASH_BITS);
|
|
}
|
|
|
|
/*
|
|
One BFINAL fixed-Huffman block over the whole input.
|
|
|
|
The match search keeps only the MOST RECENT position for each 3-byte hash
|
|
(no chain walk). That is a deliberate trade: on PNG-filtered image data the
|
|
win comes overwhelmingly from long runs — a filtered flat region is a run of
|
|
zeroes, and the most recent candidate for those is one byte back, which
|
|
extends into a single 258-byte overlapping match. A full chain search would
|
|
buy a few more percent for several times the code.
|
|
*/
|
|
static void png_deflate(png_bitwriter_t* b, const uint8_t* data, size_t size) {
|
|
int32_t* head;
|
|
size_t pos = 0;
|
|
int i;
|
|
|
|
head = (int32_t*)malloc(PNG_HASH_SIZE * sizeof(int32_t));
|
|
|
|
if (!head) {
|
|
b->w.error = 1;
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < PNG_HASH_SIZE; i++)
|
|
head[i] = -1;
|
|
|
|
png_put_bits(b, 1, 1); /* BFINAL */
|
|
png_put_bits(b, 1, 2); /* BTYPE = 01, fixed Huffman */
|
|
|
|
while (pos < size) {
|
|
int best_length = 0;
|
|
int best_distance = 0;
|
|
|
|
if ((size - pos) >= PNG_MIN_MATCH) {
|
|
uint32_t slot = png_hash3(data + pos);
|
|
int32_t candidate = head[slot];
|
|
|
|
head[slot] = (int32_t)pos;
|
|
|
|
if (candidate >= 0) {
|
|
size_t distance = pos - (size_t)candidate;
|
|
|
|
if (distance > 0 && distance <= PNG_WINDOW) {
|
|
size_t limit = size - pos;
|
|
size_t length = 0;
|
|
|
|
if (limit > PNG_MAX_MATCH)
|
|
limit = PNG_MAX_MATCH;
|
|
|
|
/* candidate + length may run past pos: an overlapping match
|
|
is legal DEFLATE and is exactly what turns a run into one
|
|
length/distance pair. */
|
|
while (length < limit && data[(size_t)candidate + length] == data[pos + length])
|
|
length++;
|
|
|
|
if (length >= PNG_MIN_MATCH) {
|
|
best_length = (int)length;
|
|
best_distance = (int)distance;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (best_length >= PNG_MIN_MATCH) {
|
|
size_t k;
|
|
|
|
png_put_match(b, best_length, best_distance);
|
|
|
|
/* Index the bytes the match covered so later positions can still
|
|
find them. */
|
|
for (k = 1; k < (size_t)best_length; k++) {
|
|
if ((pos + k + PNG_MIN_MATCH) > size)
|
|
break;
|
|
|
|
head[png_hash3(data + pos + k)] = (int32_t)(pos + k);
|
|
}
|
|
|
|
pos += (size_t)best_length;
|
|
} else {
|
|
png_put_symbol(b, data[pos]);
|
|
|
|
pos++;
|
|
}
|
|
}
|
|
|
|
png_put_symbol(b, 256); /* end of block */
|
|
|
|
if (b->bit_count > 0)
|
|
png_put_bits(b, 0, 8 - b->bit_count);
|
|
|
|
free(head);
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* PNG container */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
static void png_put_be32(psx_state_writer_t* w, uint32_t value) {
|
|
psx_sw_u8(w, (uint8_t)((value >> 24) & 0xffu));
|
|
psx_sw_u8(w, (uint8_t)((value >> 16) & 0xffu));
|
|
psx_sw_u8(w, (uint8_t)((value >> 8) & 0xffu));
|
|
psx_sw_u8(w, (uint8_t)(value & 0xffu));
|
|
}
|
|
|
|
/* length + type + payload + CRC(type, payload). */
|
|
static void png_put_chunk(psx_state_writer_t* w, const char* type,
|
|
const uint8_t* data, size_t size) {
|
|
uint32_t crc;
|
|
|
|
png_put_be32(w, (uint32_t)size);
|
|
psx_sw_bytes(w, type, 4);
|
|
psx_sw_bytes(w, data, size);
|
|
|
|
crc = png_crc32(0, (const uint8_t*)type, 4);
|
|
crc = png_crc32(crc, data, size);
|
|
|
|
png_put_be32(w, crc);
|
|
}
|
|
|
|
/* PNG's five per-row filters (RFC 2083 6). bpp is the byte offset of the
|
|
pixel to the left; a/b/c are left / above / above-left. */
|
|
static uint8_t png_paeth(int a, int b, int c) {
|
|
int p = a + b - c;
|
|
int pa = p > a ? p - a : a - p;
|
|
int pb = p > b ? p - b : b - p;
|
|
int pc = p > c ? p - c : c - p;
|
|
|
|
if (pa <= pb && pa <= pc)
|
|
return (uint8_t)a;
|
|
|
|
if (pb <= pc)
|
|
return (uint8_t)b;
|
|
|
|
return (uint8_t)c;
|
|
}
|
|
|
|
static void png_filter_row(int filter, const uint8_t* row, const uint8_t* prev,
|
|
size_t row_bytes, int bpp, uint8_t* out) {
|
|
size_t i;
|
|
|
|
for (i = 0; i < row_bytes; i++) {
|
|
int a = (i >= (size_t)bpp) ? row[i - bpp] : 0;
|
|
int b = prev ? prev[i] : 0;
|
|
int c = (prev && i >= (size_t)bpp) ? prev[i - bpp] : 0;
|
|
int predictor;
|
|
|
|
switch (filter) {
|
|
case 1: predictor = a; break;
|
|
case 2: predictor = b; break;
|
|
case 3: predictor = (a + b) / 2; break;
|
|
case 4: predictor = png_paeth(a, b, c); break;
|
|
default: predictor = 0; break;
|
|
}
|
|
|
|
out[i] = (uint8_t)((int)row[i] - predictor);
|
|
}
|
|
}
|
|
|
|
/* The reference heuristic: pick the filter whose output has the smallest sum of
|
|
absolute values read as signed bytes, which correlates well with how well the
|
|
row will then compress. */
|
|
static size_t png_filter_score(const uint8_t* data, size_t size) {
|
|
size_t score = 0;
|
|
size_t i;
|
|
|
|
for (i = 0; i < size; i++)
|
|
score += (data[i] < 128) ? data[i] : (size_t)(256 - data[i]);
|
|
|
|
return score;
|
|
}
|
|
|
|
int psx_png_encode_rgb(const uint8_t* rgb, int width, int height,
|
|
void** out_data, size_t* out_size) {
|
|
png_bitwriter_t b;
|
|
psx_state_writer_t idat;
|
|
uint8_t header[13];
|
|
uint8_t* raw;
|
|
uint8_t* candidate;
|
|
size_t row_bytes;
|
|
size_t raw_size;
|
|
int y;
|
|
int filter;
|
|
|
|
if (!out_data || !out_size)
|
|
return -1;
|
|
|
|
*out_data = NULL;
|
|
*out_size = 0;
|
|
|
|
if (!rgb || width <= 0 || height <= 0)
|
|
return -1;
|
|
|
|
row_bytes = (size_t)width * 3u;
|
|
raw_size = (size_t)height * (row_bytes + 1u);
|
|
|
|
raw = (uint8_t*)malloc(raw_size);
|
|
candidate = (uint8_t*)malloc(row_bytes * 5u);
|
|
|
|
if (!raw || !candidate) {
|
|
free(raw);
|
|
free(candidate);
|
|
return -1;
|
|
}
|
|
|
|
/* Filter every row, keeping the cheapest of the five. */
|
|
for (y = 0; y < height; y++) {
|
|
const uint8_t* row = rgb + ((size_t)y * row_bytes);
|
|
const uint8_t* prev = y ? (rgb + ((size_t)(y - 1) * row_bytes)) : NULL;
|
|
uint8_t* dst = raw + ((size_t)y * (row_bytes + 1u));
|
|
int best = 0;
|
|
size_t best_score = 0;
|
|
|
|
for (filter = 0; filter < 5; filter++) {
|
|
uint8_t* slot = candidate + ((size_t)filter * row_bytes);
|
|
size_t score;
|
|
|
|
png_filter_row(filter, row, prev, row_bytes, 3, slot);
|
|
|
|
score = png_filter_score(slot, row_bytes);
|
|
|
|
if (filter == 0 || score < best_score) {
|
|
best = filter;
|
|
best_score = score;
|
|
}
|
|
}
|
|
|
|
dst[0] = (uint8_t)best;
|
|
memcpy(dst + 1, candidate + ((size_t)best * row_bytes), row_bytes);
|
|
}
|
|
|
|
free(candidate);
|
|
|
|
/* zlib stream: 0x78 0x01 (deflate, 32K window, no preset dictionary; the
|
|
header word is a multiple of 31 as the format requires), the block, then
|
|
the Adler-32 of the unfiltered-through-filtered bytes we just built. */
|
|
psx_sw_init(&b.w);
|
|
b.bits = 0;
|
|
b.bit_count = 0;
|
|
|
|
psx_sw_u8(&b.w, 0x78);
|
|
psx_sw_u8(&b.w, 0x01);
|
|
|
|
png_deflate(&b, raw, raw_size);
|
|
|
|
png_put_be32(&b.w, png_adler32(raw, raw_size));
|
|
|
|
free(raw);
|
|
|
|
if (b.w.error) {
|
|
psx_sw_free(&b.w);
|
|
return -1;
|
|
}
|
|
|
|
idat = b.w;
|
|
|
|
psx_sw_init(&b.w);
|
|
|
|
psx_sw_bytes(&b.w, "\x89PNG\r\n\x1a\n", 8);
|
|
|
|
header[0] = (uint8_t)(((uint32_t)width >> 24) & 0xffu);
|
|
header[1] = (uint8_t)(((uint32_t)width >> 16) & 0xffu);
|
|
header[2] = (uint8_t)(((uint32_t)width >> 8) & 0xffu);
|
|
header[3] = (uint8_t)((uint32_t)width & 0xffu);
|
|
header[4] = (uint8_t)(((uint32_t)height >> 24) & 0xffu);
|
|
header[5] = (uint8_t)(((uint32_t)height >> 16) & 0xffu);
|
|
header[6] = (uint8_t)(((uint32_t)height >> 8) & 0xffu);
|
|
header[7] = (uint8_t)((uint32_t)height & 0xffu);
|
|
header[8] = 8; /* bit depth */
|
|
header[9] = 2; /* colour type: truecolour RGB */
|
|
header[10] = 0; /* compression: deflate */
|
|
header[11] = 0; /* filter method: adaptive */
|
|
header[12] = 0; /* interlace: none */
|
|
|
|
png_put_chunk(&b.w, "IHDR", header, sizeof(header));
|
|
png_put_chunk(&b.w, "IDAT", idat.buf, idat.size);
|
|
png_put_chunk(&b.w, "IEND", NULL, 0);
|
|
|
|
psx_sw_free(&idat);
|
|
|
|
if (b.w.error) {
|
|
psx_sw_free(&b.w);
|
|
return -1;
|
|
}
|
|
|
|
*out_data = b.w.buf;
|
|
*out_size = b.w.size;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* Frame capture */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
static uint8_t thumb_expand5(uint32_t component) {
|
|
return (uint8_t)((component << 3) | (component >> 2));
|
|
}
|
|
|
|
int psx_thumbnail_capture_png(psx_t* psx, void** out_data, size_t* out_size,
|
|
int* out_width, int* out_height) {
|
|
const uint8_t* source;
|
|
uint8_t* rgb;
|
|
uint32_t stride = PSX_GPU_FB_STRIDE;
|
|
int scale = 1;
|
|
int is_24bpp;
|
|
int use_vram;
|
|
int width;
|
|
int height;
|
|
int source_width;
|
|
int source_height;
|
|
int factor = 1;
|
|
int thumb_width;
|
|
int thumb_height;
|
|
int x;
|
|
int y;
|
|
int result;
|
|
|
|
if (!out_data || !out_size)
|
|
return -1;
|
|
|
|
*out_data = NULL;
|
|
*out_size = 0;
|
|
|
|
if (out_width)
|
|
*out_width = 0;
|
|
|
|
if (out_height)
|
|
*out_height = 0;
|
|
|
|
if (!psx || !psx->gpu || !psx->gpu->vram)
|
|
return -1;
|
|
|
|
width = (int)psx_get_display_width(psx);
|
|
height = (int)psx_get_display_height(psx);
|
|
|
|
if (width <= 0 || height <= 0)
|
|
return -1;
|
|
|
|
/* Mirrors frontend/main.cpp updateTexture(): 24bpp reinterprets VRAM bytes
|
|
as packed RGB888, and a display window that reaches past the bottom of
|
|
VRAM is read from the VRAM origin instead. Both force the native surface. */
|
|
is_24bpp = psx_get_display_format(psx) ? 1 : 0;
|
|
use_vram = ((int)psx->gpu->disp_y + height) > PSX_GPU_FB_HEIGHT;
|
|
|
|
source = NULL;
|
|
|
|
#ifdef USE_HARDWARE
|
|
if (!is_24bpp && !use_vram) {
|
|
uint32_t backend_stride = PSX_GPU_FB_STRIDE;
|
|
const void* surface = psx_gpu_get_display_surface(psx->gpu, 0, &scale, &backend_stride);
|
|
|
|
/* scale == 1 means it handed back the native buffer, which the branch
|
|
below reaches anyway. */
|
|
if (surface && scale > 1 && backend_stride) {
|
|
source = (const uint8_t*)surface;
|
|
stride = backend_stride;
|
|
} else {
|
|
scale = 1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (!source) {
|
|
scale = 1;
|
|
stride = PSX_GPU_FB_STRIDE;
|
|
source = (const uint8_t*)(use_vram ? (void*)psx->gpu->vram : psx_gpu_get_display_buffer(psx->gpu));
|
|
}
|
|
|
|
if (!source)
|
|
return -1;
|
|
|
|
source_width = width * scale;
|
|
source_height = height * scale;
|
|
|
|
while ((source_width / factor) > PSX_THUMBNAIL_MAX_W ||
|
|
(source_height / factor) > PSX_THUMBNAIL_MAX_H)
|
|
factor++;
|
|
|
|
thumb_width = source_width / factor;
|
|
thumb_height = source_height / factor;
|
|
|
|
if (thumb_width < 1 || thumb_height < 1)
|
|
return -1;
|
|
|
|
rgb = (uint8_t*)malloc((size_t)thumb_width * (size_t)thumb_height * 3u);
|
|
|
|
if (!rgb)
|
|
return -1;
|
|
|
|
/* Box-average each factor x factor source block. At factor 1 this is a
|
|
straight format conversion, which is the common (native-resolution) case. */
|
|
for (y = 0; y < thumb_height; y++) {
|
|
for (x = 0; x < thumb_width; x++) {
|
|
uint32_t red = 0;
|
|
uint32_t green = 0;
|
|
uint32_t blue = 0;
|
|
uint32_t count = 0;
|
|
uint8_t* out = rgb + (((size_t)y * (size_t)thumb_width + (size_t)x) * 3u);
|
|
int sy;
|
|
|
|
for (sy = y * factor; sy < (y + 1) * factor && sy < source_height; sy++) {
|
|
const uint8_t* row = source + ((size_t)sy * (size_t)stride);
|
|
int sx;
|
|
|
|
for (sx = x * factor; sx < (x + 1) * factor && sx < source_width; sx++) {
|
|
if (is_24bpp) {
|
|
const uint8_t* texel = row + ((size_t)sx * 3u);
|
|
|
|
red += texel[0];
|
|
green += texel[1];
|
|
blue += texel[2];
|
|
} else {
|
|
uint16_t texel = (uint16_t)(row[(size_t)sx * 2u] |
|
|
((uint16_t)row[((size_t)sx * 2u) + 1u] << 8));
|
|
|
|
red += thumb_expand5((uint32_t)(texel & 0x1fu));
|
|
green += thumb_expand5((uint32_t)((texel >> 5) & 0x1fu));
|
|
blue += thumb_expand5((uint32_t)((texel >> 10) & 0x1fu));
|
|
}
|
|
|
|
count++;
|
|
}
|
|
}
|
|
|
|
if (!count)
|
|
count = 1;
|
|
|
|
out[0] = (uint8_t)(red / count);
|
|
out[1] = (uint8_t)(green / count);
|
|
out[2] = (uint8_t)(blue / count);
|
|
}
|
|
}
|
|
|
|
result = psx_png_encode_rgb(rgb, thumb_width, thumb_height, out_data, out_size);
|
|
|
|
free(rgb);
|
|
|
|
if (result == 0) {
|
|
if (out_width)
|
|
*out_width = thumb_width;
|
|
|
|
if (out_height)
|
|
*out_height = thumb_height;
|
|
}
|
|
|
|
return result;
|
|
}
|