Author Topic: Trying to implement a TIM image plugin for Qt 5  (Read 1882 times)

Schala Zeal

  • Radical Dreamer (+2000)
  • *
  • Posts: 2127
  • 7th Elemental Innate, and vtuber
    • View Profile
Trying to implement a TIM image plugin for Qt 5
« on: April 12, 2015, 05:19:43 am »
Qt 5 is a modular C++ framework I find very useful. It has an API for plugin development to extend its capabilities in the areas of image format handling, GUI skins, etc.

I've been spending about a week developing a Qt 5 image handling plugin that allows it to read PlayStation TIM images (with write support being considered but not yet implemented). There's not a lot of open source TIM reader/writer programs out there so I thought, with a Qt TIM handling plugin, any program made with Qt that makes use of the image API only needs to drop in the TIM plugin DLL/dylib/so to have such capabilities.

The plugin code itself is nearly done, but there's a major problem I'm facing. I'm using Serge's Chrono Cross menu portrait's TIM file to test the plugin by having it converted to a PNG file. It spits out a readable image, but something is very wrong with the pixels. Every 2 lines is another 2 lines of what seems to be garbage:



Code: [Select]
#define SCALE5TO8(c) (((c) << 3) | ((c) >> 2))
#define SCALE8TO5(c) (((c) & 0xf8) >> 3)

static inline quint32 rgba5551ToArgb32(quint16 c)
{
quint32 r = SCALE5TO8(c & 0x1f);
quint32 g = SCALE5TO8((c >> 5) & 0x1f);
quint32 b = SCALE5TO8((c >> 10) & 0x1f);
quint32 a = ~SCALE5TO8((c >> 15) & 0x1f);

return ((a << 24) | (r << 16) | (g << 8) | b);
}

bool QTimHandler::read(QImage *img)
{
QDataStream ds(device());
ds.setByteOrder(QDataStream::LittleEndian);
quint32 magic, flags;

ds >> magic >> flags;

if (ds.status() != QDataStream::Ok || magic != 0x10)
return false;
bool hasClut = flags & 0x8 ? true : false;
quint8 pxlMode = flags & 0x7;

quint8 bpp;
switch (pxlMode)
{
case 0: bpp = 4; break;
case 1: bpp = 8; break;
case 2: bpp = 16; break;
case 3: bpp = 24; break;
default: bpp = 4;
}

QVector<QRgb> clut;
if (hasClut)
{
quint16 colors, npals;
ds.skipRawData(8);
ds >> colors >> npals;

for (quint16 i = 0; i < (colors*npals); i++)
{
quint16 c;
ds >> c;
clut.append(rgba5551ToArgb32(c));
}
}

ds.skipRawData(8);
quint16 w, h;
ds >> w >> h;

switch (bpp)
{
case 4: w /= 4; break;
case 8: w /= 2; break;
default: ;
}

QImage result;
m_size = QSize(w, h);

switch (bpp)
{
case 4:
case 8:
result = QImage(w, h, QImage::Format_Indexed8);
result.setColorTable(clut);
break;
default:
result = QImage(w, h, QImage::Format_ARGB32);
}

for (quint16 y = 0; y < h; y++)
{
for (quint16 x = 0; x < w; x++)
{
switch (bpp)
{
case 4:
{
quint8 p;
ds >> p;
result.setPixel(x, y, p & 0xf0);
result.setPixel(x++, y, p & 0xf);
}
break;
case 8:
{
quint8 p;
ds >> p;
result.setPixel(x, y, p);
}
break;
case 16:
{
quint16 p;
ds >> p;
result.setPixel(x, y, rgba5551ToArgb32(p));
}
break;
}
}
}

*img = result;
return ds.status() == QDataStream::Ok;
}
« Last Edit: April 12, 2015, 05:25:01 am by Schala Zeal »

FaustWolf

  • Guru of Time Emeritus
  • Arbiter (+8000)
  • *
  • Posts: 8972
  • Fan Power Advocate
    • View Profile
Re: Trying to implement a TIM image plugin for Qt 5
« Reply #1 on: April 12, 2015, 02:13:41 pm »
PSZ, is the input ripped from a 2048 bytes/sector game image, or a 2352 bytes/sector image? If 2352 bytes/sector, then there's padding that would be uninterpretable to your program. In ISOBuster, you can get a 2048 byte/sector game image by ripping to "User Data."

Vehek

  • Errare Explorer (+1500)
  • *
  • Posts: 1756
    • View Profile
Re: Trying to implement a TIM image plugin for Qt 5
« Reply #2 on: April 12, 2015, 02:26:03 pm »
Is that width calculation correct? I haven't taken a good read of the code, but I thought you're supposed to multiply, not divide...
« Last Edit: April 12, 2015, 02:47:24 pm by Vehek »

Schala Zeal

  • Radical Dreamer (+2000)
  • *
  • Posts: 2127
  • 7th Elemental Innate, and vtuber
    • View Profile
Re: Trying to implement a TIM image plugin for Qt 5
« Reply #3 on: April 12, 2015, 04:00:51 pm »
I did that, but that makes the width 64. A bitmap copy of the TIM converted by someone else says 32x224

Schala Zeal

  • Radical Dreamer (+2000)
  • *
  • Posts: 2127
  • 7th Elemental Innate, and vtuber
    • View Profile
Re: Trying to implement a TIM image plugin for Qt 5
« Reply #4 on: April 12, 2015, 04:03:01 pm »
PSZ, is the input ripped from a 2048 bytes/sector game image, or a 2352 bytes/sector image? If 2352 bytes/sector, then there's padding that would be uninterpretable to your program. In ISOBuster, you can get a 2048 byte/sector game image by ripping to "User Data."

I believe I used those translation tools written in Java by those French guys to extract my files. As for my ISO, i'm not sure if I'm still using an ISO I ripped with ImgBurn or downloaded a pristine ISO. I'm paranoid about even the slightest damage to disc.

EDIT: Used TIMViewer to convert my reference BMP back to TIM, then ran the test program with it: SUCCESS! Qt 5 now has a working TIM image plugin! ... at least as far as 16 BPP goes, because TIMViewer doesn't seem to handle 4/8 BPP, and I'm currently too lazy to implement 24 BPP, but it's on the todo list. Source can be found here: https://github.com/Schala/Qt-extra
« Last Edit: April 12, 2015, 05:08:00 pm by Schala Zeal »

Vehek

  • Errare Explorer (+1500)
  • *
  • Posts: 1756
    • View Profile
Re: Trying to implement a TIM image plugin for Qt 5
« Reply #5 on: April 12, 2015, 09:58:06 pm »
I did that, but that makes the width 64. A bitmap copy of the TIM converted by someone else says 32x224
Huh? Since when is 64 x 2 32? Opening up one of the menu strip portraits in TIMViewer gets a image with dimensions 128x224.

Schala Zeal

  • Radical Dreamer (+2000)
  • *
  • Posts: 2127
  • 7th Elemental Innate, and vtuber
    • View Profile
Re: Trying to implement a TIM image plugin for Qt 5
« Reply #6 on: April 13, 2015, 02:03:08 am »
Well a converted TIM i downloaded months ago shows 32x224. I tweaked the code to be:

width = (width * 16) / BPP

Better results when tested, however, something's a little funky:



Also, started work on an experimental Qt image plugin for Chrono Cross's altered TIM format used in battle textures. Until if the unknown data interspersed in the CLUT and image headers is ever identified, the plugin will only be read only.

Little update: wrote a Free Pascal unit that implements a TIM reader based on the TFPCustomImageReader class as well as a tim2bmp program. This is based on my Qt plugin, so expect that strange output if working with CLUT images. Should only need to type `fpc tim2bmp.pas` to compile both if you have Free Pascal installed and in your Windows PATH

fpreadtim.pas
Code: [Select]
{$mode objfpc}{$h+}

unit FPReadTIM;

interface
uses classes, fpimage;

type
TFPReaderTIM = class (TFPCustomImageReader)
protected
procedure InternalRead(Stream: TStream; Img: TFPCustomImage); override;
function InternalCheck(Stream: TStream): boolean; override;
public
constructor Create; override;
destructor Destroy; override;
end;

function RGBA5551ToFPColor(RGBA: word): TFPColor;
function RGBA5551ToDWord(RGBA: word): dword;

implementation
function Scale5To8(c: byte): byte;
begin
result := (c shl 3) or (c shr 2);
end;

constructor TFPReaderTIM.Create;
begin
inherited create;
end;

destructor TFPReaderTIM.Destroy;
begin
inherited destroy;
end;

function RGBA5551ToFPColor(RGBA: word): TFPColor;
begin
RGBA := leton(RGBA);
with result do
begin
red := swap(word(Scale5To8(RGBA and $1f)));
green := swap(word(Scale5To8((RGBA shr 5) and $1f)));
blue := swap(word(Scale5To8((RGBA shr 10) and $1f)));
alpha := swap(word(not Scale5To8((RGBA shr 15) and $1f)));
end;
end;

function RGBA5551ToDWord(RGBA: word): dword;
var
a, r, g, b: byte;
begin
RGBA := leton(RGBA);
r := Scale5To8(RGBA and $1f);
g := Scale5To8((RGBA shr 5) and $1f);
b := Scale5To8((RGBA shr 10) and $1f);
a := not Scale5To8((RGBA shr 15) and $1f);

result := swap(word((a shl 24) or (r shl 16) or (g shl 8) or b));
end;

function TFPReaderTIM.InternalCheck(Stream: TStream): boolean;
var
magic: dword;
begin
magic := leton(Stream.readdword);
result := (magic = $10);
end;

procedure TFPReaderTIM.InternalRead(Stream: TStream; Img: TFPCustomImage);
var
flags, clutsize, imgsize: dword;
colors, npals, i, w, h, x, y: word;
hasCLUT: boolean;
bpp, p: byte;
begin
flags := leton(Stream.readdword);
hasCLUT := (flags and $8) <> 0;
if (flags and $7) > 0 then bpp := (flags and $7)*8 else bpp := 4;

if hasCLUT then
begin
clutsize := leton(Stream.readdword)-12;
Stream.seek(4, socurrent);
colors := leton(Stream.readword);
npals := leton(Stream.readword);

Img.usepalette := true;
Img.palette.clear;

for i := 0 to colors*npals-1 do
Img.palette.add(RGBA5551ToFPColor(leton(Stream.readword)));
end;

imgsize := leton(Stream.readdword)-12;
Stream.seek(4, socurrent);

w := (leton(Stream.readword)*16) div bpp;

h := leton(Stream.readword);
Img.setsize(w, h);

x := 0;
y := 0;
while y < h do
begin
while x < w do
begin
case bpp of
4:
begin
p := Stream.readbyte;
Img.pixels[x,y] := swap(dword(p and $f0));
inc(x);
Img.pixels[x,y] := swap(dword(p and $f));
end;
8: Img.pixels[x,y] := swap(dword(Stream.readbyte));
16: Img.pixels[x,y] := swap(RGBA5551ToDWord(leton(Stream.readword)));
else
end;

inc(x);
end;
x := 0;
inc(y);
end;
end;

end.

tim2bmp.pas
Code: [Select]
{$mode objfpc}{$h+}
{$apptype console}
program tim2bmp;

uses FPReadTIM, FPImage, classes, FPWriteBMP, sysutils;

var
img: TFPMemoryImage;
r: TFPCustomImageReader;
w: TFPCustomImageWriter;
rf, wf: string;
begin
if paramcount <> 2 then
begin
writeln('tim2bmp [source] [dest]');
exit;
end;

try
r := TFPReaderTIM.Create;
w := tfpwriterbmp.create;
tfpwriterbmp(w).bitsperpixel := 32;
img := TFPMemoryImage.create(0,0);
rf := paramstr(1);
wf := paramstr(2);

img.loadfromfile(rf, r);
img.savetofile(wf, w);
r.free;
w.free;
img.free;
except
on e: exception do
writeln('error: ', e.message);
end;
end.
« Last Edit: April 17, 2015, 07:45:35 pm by Schala Zeal »