-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathActImage.cs
475 lines (448 loc) · 19.4 KB
/
ActImage.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
/*
* Idmr.ImageFormat.Act, Allows editing capability of LucasArts *.ACT files.
* Copyright (C) 2009-2023 Michael Gaisser (mjgaisser@gmail.com)
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the Mozilla Public License; either version 2.0 of the
* License, or (at your option) any later version.
*
* This library is "as is" without warranty of any kind; including that the
* library is free of defects, merchantable, fit for a particular purpose or
* non-infringing. See the full license text for more details.
*
* If a copy of the MPL (License.txt) was not distributed with this file,
* you can obtain one at http://mozilla.org/MPL/2.0/.
*
* VERSION: 2.2
*/
/* CHANGE LOG
* v2.2, 230708
* [NEW] Global colors read support
* [UPD] Proper use of jumps instead of fixed header lengths
* [UPD] renamed frame shift to lengthBitCount (private)
* v2.1, 141214
* [UPD] switch to MPL
* v2.0, 121024
* [UPD] major re-write...
*/
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using Idmr.Common;
namespace Idmr.ImageFormat.Act
{
/// <summary>Object to work with *.ACT image files found in TIE95, XvT and BoP. Also reads legacy XACT resources from TIE LFD files.</summary>
/// <remarks>Acts can be loaded from file or created starting with a Bitmap. Each frame retains an individual palette.<br/>
/// XACT resources must be saved as separate .ACT files instead of within their original LFD resource. To edit XACT resource, use Idmr.LfdReader.dll.</remarks>
public class ActImage
{
string _filePath;
FrameCollection _frames;
readonly byte[] _header = new byte[_fileHeaderLength];
Point _center;
const int _fileHeaderLength = 0x34;
internal static string _validationErrorMessage = "Validation error, data is not a LucasArts Act Image or is corrupted.";
static readonly string _extension = ".ACT";
Color[] _colors;
bool _useGlobalColors;
#region constructors
/// <summary>Loads an Act image from file</summary>
/// <param name="file">Full path to the ACT file</param>
/// <exception cref="LoadFileException">Error loading <i>file</i></exception>
public ActImage(string file)
{
FileStream fs = null;
try
{
if (!file.ToUpper().EndsWith(_extension)) throw new ArgumentException("Invalid file extension, must be " + _extension, "file");
fs = File.OpenRead(file);
DecodeFile(new BinaryReader(fs).ReadBytes((int)fs.Length));
fs.Close();
}
catch (Exception x)
{
fs?.Close();
System.Diagnostics.Debug.WriteLine(x.StackTrace);
throw new LoadFileException(x);
}
_filePath = file;
}
/// <summary>Loads an XACT resource from file</summary>
/// <param name="raw">RawData from an LFD resource</param>
/// <exception cref="ArgumentException">Error processing <i>raw</i></exception>
public ActImage(byte[] raw)
{
try { DecodeFile(raw); }
catch (Exception x)
{
System.Diagnostics.Debug.WriteLine(x.StackTrace);
throw x;
}
_filePath = "LFD";
}
/// <summary>Creates a new Act image from bitmap</summary>
/// <remarks><see cref="FilePath"/> defaults to <b>"NewImage.act"</b>, <see cref="Frames"/> is initialized as a single <see cref="Frame"/> using <paramref name="image"/>.<br/>
/// <see cref="Center"/> defaults to the center pixel of <paramref name="image"/>.</remarks>
/// <param name="image">Initial <see cref="PixelFormat.Format8bppIndexed"/> image to be used</param>
/// <exception cref="BoundaryException"><paramref name="image"/> exceeds allowable size.</exception>
public ActImage(Bitmap image)
{
_frames = new FrameCollection(this)
{
new Frame(this, image)
};
_filePath = "NewImage.act";
_center = new Point(Width/2, Height/2);
_frames[0].Location = new Point(-Center.X, -Center.Y);
}
#endregion constructors
#region public methods
/// <summary>Populates the Act object from the raw byte data</summary>
/// <param name="raw">Entire contents of an *.ACT file</param>
/// <exception cref="ArgumentException">Data validation failure</exception>
public void DecodeFile(byte[] raw)
{
ArrayFunctions.TrimArray(raw, 0, _header);
var offsetsJump = BitConverter.ToInt32(_header, 0x10); // usually same as_fileHeaderLength, but could technically be different
_center = new Point(BitConverter.ToInt32(_header, 0x24), BitConverter.ToInt32(_header, 0x28));
// Global colors, not likely to be used
_useGlobalColors = _header[0x2c] == 0x18;
if (_useGlobalColors)
{
_colors = new Color[BitConverter.ToInt32(_header, 0x30)];
var colorsJump = BitConverter.ToInt32(_header, 0xc);
System.Diagnostics.Debug.WriteLine("Global Colors: " + NumberOfColors);
for (int c = 0, pos = colorsJump; c < _colors.Length; c++, pos += 4)
_colors[c] = Color.FromArgb(raw[pos], raw[pos + 1], raw[pos + 2]); // Red, Green, Blue, Alpha unused
}
// Frames
int numFrames = BitConverter.ToInt32(_header, 0x18);
int[] frameOffsets = new int[numFrames];
System.Diagnostics.Debug.WriteLine("Frames: " + numFrames);
ArrayFunctions.TrimArray(raw, offsetsJump, frameOffsets);
_frames = new FrameCollection(this);
byte[] rawFrame;
for (int f = 0; f < numFrames; f++)
{
// FrameHeader
rawFrame = new byte[BitConverter.ToInt32(raw, frameOffsets[f])];
ArrayFunctions.TrimArray(raw, frameOffsets[f], rawFrame);
_frames.Add(new Frame(this, rawFrame));
}
// EOF
}
/// <summary>Gets the image from the given information</summary>
/// <param name="raw">The encoded Rows data</param>
/// <param name="width">Width of the Frame</param>
/// <param name="height">Height of the Frame</param>
/// <param name="colors">Defined Color array to be used</param>
/// <param name="lengthBitCount">Shift value to decode with</param>
/// <exception cref="ArgumentException">Data validation failure</exception>
/// <returns>8bppIndexed image</returns>
public static Bitmap DecodeImage(byte[] raw, int width, int height, Color[] colors, int lengthBitCount)
{
Bitmap image = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
BitmapData bd = GraphicsFunctions.GetBitmapData(image);
byte[] pixelData = new byte[bd.Stride * bd.Height];
byte b, indexShift = 0;
int offset = 0;
// Rows
for (int y = (bd.Height - 1); y >= 0; y--)
{
for (int x = 0, pos = bd.Stride * y; x < bd.Width; )
{
// OpCodes
b = raw[offset++]; // OpCode
if (b == 0xFD) // Repeat code
{
byte numRepeats = raw[offset++];
byte colorIndex = raw[offset++];
for (int j = 0; j <= numRepeats; j++, x++) pixelData[pos + x] = colorIndex;
}
else if (b == 0xFC) // Blank code
{
byte numRepeats = raw[offset++];
x += numRepeats + 1;
}
else if (b == 0xFB) // Shift code
{
indexShift = raw[offset++];
var unused = raw[offset++]; // this appears to always be zero
if (unused != 0) System.Diagnostics.Debug.WriteLine("16-bit Shift value detected: index: " + unused * 0x100 + indexShift);
}
else // Short code
{
byte p = (byte)(b >> lengthBitCount);
byte n = (byte)(Math.Pow(2, lengthBitCount) - 1);
for (int j = 0; j <= (b & n); j++, x++) pixelData[pos + x] = (byte)(p + indexShift);
}
}
if (raw[offset++] != 0xFE) throw new ArgumentException(_validationErrorMessage, "raw"); // EndRow
}
if (raw[offset++] != 0xFF) throw new ArgumentException(_validationErrorMessage, "raw"); // EndFrame
// end Frame
GraphicsFunctions.CopyBytesToImage(pixelData, bd);
image.UnlockBits(bd);
image.RotateFlip(RotateFlipType.RotateNoneFlipX);
ColorPalette pal = image.Palette;
for (int c = 0; c < colors.Length; c++) pal.Entries[c] = colors[c];
for (int c = colors.Length; c < 256; c++) pal.Entries[c] = Color.Blue;
image.Palette = pal;
return image;
}
/// <summary>Gets the encoded byte array</summary>
/// <param name="image">Image to be encoded.</param>
/// <param name="colors">Defined Color array to be used.</param>
/// <param name="lengthBitCount">Shift value to encode with.</param>
/// <exception cref="ArgumentException"><paramref name="image"/> is not 8bppIndexed<br/><b>-or-</b><br/>Invalid <paramref name="lengthBitCount"/> value.</exception>
/// <remarks><paramref name="image"/> must be 8bppIndexed. <paramref name="lengthBitCount"/> restricted to 3-5.</remarks>
/// <returns>Encoded byte array of the image ready to be written to disk</returns>
public static byte[] EncodeImage(Bitmap image, Color[] colors, int lengthBitCount)
{
/* CURRENTLY DEPRECATED
* If this overload stays long-term, since this is a static function I'm wondering if this should act like Frame.Image.set and trim the colors at the same time.
* Never actually used the colors in the signature, anyway
* Don't want to do that just yet, though
*/
return EncodeImage(image, lengthBitCount);
}
/// <summary>Gets the encoded byte array</summary>
/// <param name="image">Image to be encoded.</param>
/// <param name="lengthBitCount">Shift value to encode with.</param>
/// <exception cref="ArgumentException"><paramref name="image"/> is not 8bppIndexed<br/><b>-or-</b><br/>Invalid <paramref name="lengthBitCount"/> value.</exception>
/// <remarks><paramref name="image"/> must be 8bppIndexed. <paramref name="lengthBitCount"/> restricted to 3-5.</remarks>
/// <returns>Encoded byte array of the image ready to be written to disk</returns>
public static byte[] EncodeImage(Bitmap image, int lengthBitCount)
{
if (image.PixelFormat != PixelFormat.Format8bppIndexed) throw new ArgumentException("image must be 8bppIndexed", "image");
if (lengthBitCount < 3 || lengthBitCount > 5) throw new ArgumentException("Bit count must be 3-5", "lengthBitCount");
byte[] raw = new byte[image.Width * image.Height * 2];
int offset = 0;
image.RotateFlip(RotateFlipType.RotateNoneFlipX);
// Rows
BitmapData bd = GraphicsFunctions.GetBitmapData(image);
byte[] pixels = new byte[bd.Stride * bd.Height];
GraphicsFunctions.CopyImageToBytes(bd, pixels);
image.UnlockBits(bd);
image.RotateFlip(RotateFlipType.RotateNoneFlipX);
for (int y = (bd.Height - 1); y >= 0; y--)
{
for (int x = 0, pos = bd.Stride * y, len = 1; x < bd.Width;)
{
try
{ // throws on last row
if ((x + len) != bd.Width && pixels[pos + x] == pixels[pos + x + len])
{
len++;
continue;
}
}
catch { /* do nothing */ }
if ((len <= Math.Pow(2, lengthBitCount) && pixels[pos + x] < (0xFF >> lengthBitCount)) || (len <= (lengthBitCount == 3 ? 3 : 10) && pixels[pos + x] == (0xFF >> lengthBitCount))) // allow 0xF8-0xFA
{ // Short code
byte b = (byte)(len - 1);
b |= (byte)(pixels[pos + x] << lengthBitCount);
raw[offset++] = b;
}
else if (pixels[pos + x] == 0)
{ // Blank code
raw[offset++] = 0xFC;
raw[offset++] = (byte)(len - 1);
}
else
{ // Repeat code
raw[offset++] = 0xFD;
raw[offset++] = (byte)(len - 1);
raw[offset++] = pixels[pos + x];
}
// not going to use Shift codes
x += len;
len = 1;
}
raw[offset++] = 0xFE; // EndRow
}
raw[offset++] = 0xFF; // EndFrame
byte[] trimmedRaw = new byte[offset];
ArrayFunctions.TrimArray(raw, 0, trimmedRaw);
return trimmedRaw;
}
/// <summary>Writes the Act object to its original location</summary>
/// <exception cref="SaveFileException">Error saving file. Original unchanged if applicable</exception>
/// <exception cref="InvalidOperationException">Attempted to save XACT resource without defining a new location</exception>
public void Save()
{
if (!_filePath.ToUpper().EndsWith(_extension))
throw new InvalidOperationException("Must define temporary location for LFD XACT resources");
FileStream fs = null;
string tempFile = _filePath + ".tmp";
//TODO: convert Global Colors to frame colors
try
{
if (File.Exists(_filePath)) File.Copy(_filePath, tempFile); // create backup
File.Delete(_filePath);
// FileHeader
int length = _fileHeaderLength;
int totalColorCount = 0;
for (int f = 0; f < NumberOfFrames; f++)
{
length += _frames[f]._length;
totalColorCount += _frames[f].NumberOfColors;
}
ArrayFunctions.WriteToArray(length, _header, 0);
ArrayFunctions.WriteToArray(totalColorCount, _header, 4);
ArrayFunctions.WriteToArray(NumberOfFrames, _header, 0x18);
ArrayFunctions.WriteToArray(_center.X, _header, 0x24);
ArrayFunctions.WriteToArray(_center.Y, _header, 0x28);
// Width, Height
// FrameOffsets
int[] frameOffsets = new int[NumberOfFrames];
byte[] frameOffsetsBytes = new byte[NumberOfFrames * 4];
frameOffsets[0] = _fileHeaderLength + NumberOfFrames * 4;
for (int f = 1; f < NumberOfFrames; f++) frameOffsets[f] = frameOffsets[f - 1] + _frames[f - 1]._length;
ArrayFunctions.TrimArray(frameOffsets, 0, frameOffsetsBytes);
fs = File.OpenWrite(_filePath);
BinaryWriter bw = new BinaryWriter(fs);
bw.Write(_header);
bw.Write(frameOffsetsBytes);
for (int f = 0; f < NumberOfFrames; f++)
{
bw.Write(_frames[f]._header);
for (int c = 0; c < _frames[f].NumberOfColors; c++)
{
fs.WriteByte(_frames[f]._colors[c].R);
fs.WriteByte(_frames[f]._colors[c].G);
fs.WriteByte(_frames[f]._colors[c].B);
fs.Position++;
}
//BUG: missing the frame extents
bw.Write(_frames[f]._rows);
}
fs.SetLength(length);
fs.Close();
File.Delete(tempFile); // delete backup if it exists
}
catch (Exception x)
{
fs?.Close();
if (File.Exists(tempFile)) File.Copy(tempFile, _filePath); // restore backup if it exists
File.Delete(tempFile); // delete backup if it exists
System.Diagnostics.Debug.WriteLine(x.StackTrace);
throw new SaveFileException(x);
}
}
/// <summary>Writes the Act object to a new location</summary>
/// <param name="file">Full path to the new file location</param>
/// <exception cref="ArgumentException">Invalid file extension</exception>
/// <exception cref="SaveFileException">Error saving file. Original unchanged if applicable</exception>
public void Save(string file)
{
if (!file.ToUpper().EndsWith(_extension))
throw new ArgumentException("New file extension must be \"" + _extension + "\". XACT objects from LFD resources must be saved as a separate " + _extension + ", temporary or otherwise. The LfdFile object will handle LFD resource saving as necessary.", "file");
_filePath = file;
Save();
}
#endregion public methods
#region public properties
/// <summary>Gets or sets the collection of <see cref="Frames">Frames</see> contained within the Act</summary>
public FrameCollection Frames
{
get => _frames;
set
{
_frames = value;
_frames._parent = this;
for (int f = 0; f < _frames.Count; f++) _frames[f]._parent = this;
recalculateSize();
}
}
/// <summary>Gets or sets the pixel location used to "pin" the object in-game</summary>
/// <exception cref="BoundaryException"><i>value</i> does not fall within <see cref="Size"/></exception>
/// <remarks><see cref="Frame.Location"/> values will update as necessary</remarks>
public Point Center
{
get => _center;
set
{
if (value.X < Width && value.X >= 0 && value.Y < Height && value.Y >= 0)
{
int offX = _center.X - value.X;
int offY = _center.Y - value.Y;
for (int f = 0; f < NumberOfFrames; f++)
{
_frames[f].X += offX;
_frames[f].Y += offY;
}
_center = value;
}
else throw new BoundaryException("value", "0,0 - " + Width + "," + Height);
}
}
/// <summary>Gets the file name of the Act object</summary>
public string FileName => StringFunctions.GetFileName(_filePath);
/// <summary>Gets the full path of the Act object</summary>
public string FilePath => _filePath;
/// <summary>Gets the overall height of the Act object</summary>
public int Height => BitConverter.ToInt32(_header, 0x20) + 1;
/// <summary>Gets the number of images contained within the Act object</summary>
public int NumberOfFrames => _frames.Count;
/// <summary>Gets the overall size of the Act object</summary>
public Size Size
{
get => new Size(Width, Height);
internal set
{
ArrayFunctions.WriteToArray(value.Width - 1, _header, 0x1C);
ArrayFunctions.WriteToArray(value.Height - 1, _header, 0x20);
}
}
/// <summary>Gets the overall width of the Act object</summary>
public int Width => BitConverter.ToInt32(_header, 0x1C) + 1;
/// <summary>Gets a copy of the global color array.</summary>
/// <exception cref="NullReferenceException">Global colors are not used.</exception>
public Color[] GlobalColors => (Color[])_colors.Clone();
/// <summary>Gets or sets if the global color array is used.</summary>
/// <remarks>If setting to <b>true</b>, initializes a 256 color array.</remarks>
public bool UseGlobalColors {
get => _useGlobalColors;
set
{
if (value) _colors = new Color[256];
else _colors = null;
_useGlobalColors = value;
}
}
/// <summary>Gets the number of global colors defined.</summary>
public int NumberOfColors {
get
{
if (_useGlobalColors) return _colors.Length;
else return 0;
}
}
#endregion
internal void recalculateSize()
{
int right = 0, bottom = 0;
int centX = _center.X;
int centY = _center.Y;
int left = centX + _frames[0].X;
int top = centY + _frames[0].Y;
for (int f = 0; f < NumberOfFrames; f++)
{
int fLeft = centX + _frames[f].X;
int fTop = centY + _frames[f].Y;
left = (fLeft < left ? fLeft : left);
top = (fTop < top ? fTop : top);
int fRight = fLeft + _frames[f].Width - 1;
int fBottom = fTop + _frames[f].Height - 1;
right = (fRight > right ? fRight : right);
bottom = (fBottom > bottom ? fBottom : bottom);
}
// may be non-zero after Frame.Location adjustment or Frames/Frames[].set
_center.X -= left;
_center.Y -= top;
Size = new Size(right - left + 1, bottom - top + 1);
}
}
}