Repository navigation
Expand file tree
/
Copy pathMask.cs
More file actions
388 lines (378 loc) · 19 KB
/
Copy pathMask.cs
File metadata and controls
388 lines (378 loc) · 19 KB
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
/*
* Idmr.LfdReader.dll, Library file to read and write LFD resource files
* Copyright (C) 2009-2026 Michael Gaisser (mjgaisser@gmail.com)
* Licensed under the MPL v2.0 or later
*
* Full notice in help/Idmr.LfdReader.chm
* Version: 3.0
*/
/* CHANGE LOG
* v3.0, 260821
* [ADD] Dispose
* v2.5.2, 260517
* [UPD] Updated the example comments
* v2.4, 250202
* [FIX] Height calculation
* [FIX] Fixed the 00 processing and corner cases
* v1.2, 160712
* [ADD] _isModified edits
* v1.1, 141215
* [UPD] changed license to MPL
* v1.0
*/
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using Idmr.Common;
namespace Idmr.LfdReader
{
/// <summary>Object for "MASK" cockpit transparency resources.</summary>
/// <remarks>The Mask resource controls the windows for the in-flight cockpits.
/// Sloppy application of the Mask can result in stars being visible through the solid parts of your cockpit, much like how the snowspeeder cockpits in Empire Strikes Back were transparent!<br/>
/// Images are monochrome, white is solid and black is transparent.
/// Maximum dimensions are controlled by <see cref="Panl"/>.</remarks>
/// <example><h4>Raw Data definition</h4>
/// <code>
/// RawData
/// {
/// /* 0x00 */ Row[] Rows;
/// }
///
/// Row
/// {
/// #if (first pixel is solid)
/// /* 0x00 */ byte FirstColor = 0xFF; // solid
/// #else
/// /* 0x00 */ byte FirstColor = 0x01; // transparent
/// #endif
/// /* 0x01 */ byte[] Lengths;
/// }</code>
/// <para>If you know the dimensions of the Mask before you start, that's great. If not, it can be determined iteratively, however there is some degree of error.
/// The platforms are fed the height and width when processing the mask data, since they're being applied to camera views where the dimensions are already known.</para>
/// <para>The iterative method currently used by <see cref="DecodeResource"/> when the dimensions are unknown assumes the first two rows start with the same state (solid/transparent) and the top row does not have any Length values that equal <i>FirstColor</i>.
/// It's not perfect, but it seems to work fine with the stock cockpits.
/// Results may vary with custom cockpits.<br/>
/// Quickly iterating through <i>Rows</i> using the width value until the end of RawData or until a Row starts with <b>0x00</b> will determine the height.
/// Again, preferably you'd already have the dimensions.</para>
/// <para>The <i>FirstColor</i> for the Row is a marker that defines the starting color.
/// After that point, it simply alternates solid/transparent <i>Lengths</i> until it reaches the end of the Row.
/// The <i>Lengths</i> values are one-indexed, so a value of <b>0x01</b> is one pixel.
/// For values larger than <b>0xFF</b>, <b>0x00</b> is used as 256 pixels and must be followed by a "closing" value, as it will not switch pixel states by itself.<br/>
/// For example, a row of <c>FF 00 0C 0A 00 6A</c> starts solid with 268 pixels (256 + 12), followed by 10 transparent pixels and 362 solid pixels (256 + 106).</para>
/// <para>If the screen width isn't 320 (i.e., 640 or 3D) then a length over 512 pixels is possible by using a second <b>0x00</b> value. The third value is then used for closing.</para>
/// <para>Lengths of exactly 256 or 512 are more complicated. Both are <c>00 00</c>, so the first one is 256, but the second may or may not be the closing value.
/// This again relies on the screen width as an input. For the sake of the decoding method used here, if a second 256 would overflow the width,
/// then it's treated as a closing pixel for a total of 256, and the pixel state switches. Both <c>FF 40 00 00</c> and <c>FF 11 00 00 2F</c> are 256 lengths.<br/>
/// In <c>FF 00 80 00 01</c> the second <b>0x00</b> requires an extra value to close it. The resulting width is 641, but will be trimmed.<br/>
/// For lines of exactly 512, it will always require a closing value and usually exist at the end of the row so the excess will be trimmed; <c>FF 80 00 00 01</c>.
/// One of the Gunbboat views does this and TIEEdit does not account for it properly and displays the view corrupted.
/// It should however be possible to have <c>00 00 00</c> to denote 512 in the middle of a row, similar to how 256 is encoded for 320px images.</para>
/// <para>This behavior with the closing pixel is because behind the scenes, the first <b>0x00</b> is really counted as 255 and a follow-up value is required and that next value is increased by 1,
/// getting us to 256 plus the second value. So for a 320-wide image, <c>00 00</c> is really (255 + 0 + 1).<br/>
/// For the 640 images, <c>00 00</c> is really 511px and the 3rd required value has 1 added to it to get (511 + value + 1).
/// This also means that lines of 256 are not possible in 640 images except at the end of the row with a bonus pixel, since as we've established <c>00 00</c> is for 512+, and <c>00 01</c> would result in 257px.</para></example>
public class Mask : Resource
{
Bitmap _image = null;
#region constructors
/// <summary>Blank constructor.</summary>
/// <remarks><see cref="Image"/> is <see langword="null"/>.</remarks>
public Mask()
{
_type = ResourceType.Mask;
}
/// <summary>Creates a new instance from an existing opened file.</summary>
/// <param name="stream">The opened LFD file.</param>
/// <param name="filePosition">The offset of the beginning of the resource.</param>
/// <exception cref="LoadFileException">Typically due to file corruption.</exception>
public Mask(FileStream stream, long filePosition)
{
read(stream, filePosition);
}
/// <summary>Creates a new instance from an exsiting file.</summary>
/// <param name="path">The full path to the unopened LFD file.</param>
/// <param name="filePosition">The offset of the beginning of the resource.</param>
/// <exception cref="LoadFileException">Typically due to file corruption.</exception>
public Mask(string path, long filePosition)
{
FileStream stream = File.OpenRead(path);
read(stream, filePosition);
stream.Close();
}
/// <summary>Creates a new instance from an existing opened file.</summary>
/// <param name="stream">The opened LFD file.</param>
/// <param name="filePosition">The offset of the beginning of the resource.</param>
/// <param name="width">Predefined width of the mask.</param>
/// <param name="height">Predefined height of the mask.</param>
/// <exception cref="BoundaryException"><paramref name="width"/> or <paramref name="height"/> exceed maximum allowable dimensions.</exception>
/// <exception cref="LoadFileException">Typically due to file corruption.</exception>
public Mask(FileStream stream, long filePosition, short width, short height)
{
if (width > Panl.MaximumWidth || height > Panl.MaximumHeight) throw new BoundaryException("dimensions", Panl.MaximumWidth + "x" + Panl.MaximumHeight);
Width = width;
Height = height;
read(stream, filePosition);
}
/// <summary>Creates a new instance from an exsiting file.</summary>
/// <param name="path">The full path to the unopened LFD file.</param>
/// <param name="filePosition">The offset of the beginning of the resource.</param>
/// <param name="width">Predefined width of the mask.</param>
/// <param name="height">Predefined height of the mask.</param>
/// <exception cref="BoundaryException"><paramref name="width"/> or <paramref name="height"/> exceed maximum allowable dimensions.</exception>
/// <exception cref="LoadFileException">Typically due to file corruption.</exception>
public Mask(string path, long filePosition, short width, short height)
{
if (width > Panl.MaximumWidth || height > Panl.MaximumHeight) throw new BoundaryException("dimensions", Panl.MaximumWidth + "x" + Panl.MaximumHeight);
Width = width;
Height = height;
FileStream stream = File.OpenRead(path);
read(stream, filePosition);
stream.Close();
}
/// <summary>Creates a new instance from an existing opened file.</summary>
/// <param name="stream">The opened LFD file.</param>
/// <param name="filePosition">The offset of the beginning of the resource.</param>
/// <param name="dimensions">Predefined size of the mask.</param>
/// <exception cref="BoundaryException"><paramref name="dimensions"/> exceeds maximum allowable dimensions.</exception>
/// <exception cref="LoadFileException">Typically due to file corruption.</exception>
public Mask(FileStream stream, long filePosition, Size dimensions)
{
if (dimensions.Width > Panl.MaximumWidth || dimensions.Height > Panl.MaximumHeight) throw new BoundaryException("dimensions", Panl.MaximumWidth + "x" + Panl.MaximumHeight);
Width = (short)dimensions.Width;
Height = (short)dimensions.Height;
read(stream, filePosition);
}
/// <summary>Creates a new instance from an exsiting file.</summary>
/// <param name="path">The full path to the unopened LFD file.</param>
/// <param name="filePosition">The offset of the beginning of the resource.</param>
/// <param name="dimensions">Predefined size of the mask.</param>
/// <exception cref="BoundaryException"><paramref name="dimensions"/> exceeds maximum allowable dimensions.</exception>
/// <exception cref="LoadFileException">Typically due to file corruption.</exception>
public Mask(string path, long filePosition, Size dimensions)
{
if (dimensions.Width > Panl.MaximumWidth || dimensions.Height > Panl.MaximumHeight) throw new BoundaryException("dimensions", Panl.MaximumWidth + "x" + Panl.MaximumHeight);
Width = (short)dimensions.Width;
Height = (short)dimensions.Height;
FileStream stream = File.OpenRead(path);
read(stream, filePosition);
stream.Close();
}
#endregion constructors
void read(FileStream stream, long filePosition)
{
try { _process(stream, filePosition); }
catch (Exception x) { throw new LoadFileException(x); }
}
/// <summary>Clean up any resources being used.</summary>
/// <param name="disposing"><see langword="true"/> if managed resources should be disposed; otherwise, <see langword="false"/>.</param>
protected override void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing) _image.Dispose();
_image = null;
base.Dispose(disposing);
}
#region public methods
/// <summary>Processes raw data to populate the resource.</summary>
/// <param name="raw">Raw byte data.</param>
/// <param name="containsHeader">Whether or not <paramref name="raw"/> contains the resource Header information.</param>
/// <exception cref="ArgumentException">Header-defined <see cref="Type"/> is not <see cref="Resource.ResourceType.Mask"/>.</exception>
public override void DecodeResource(byte[] raw, bool containsHeader)
{
_decodeResource(raw, containsHeader);
if (_type != ResourceType.Mask) throw new ArgumentException("Raw header is not for a Mask resource");
if (Width == 0)
for (int i = 1; ; i++) // get mask width
{
if (_rawData[i] == 0) Width += 0x100;
else if (_rawData[i] == _rawData[0] || _rawData[i] == 0xFF) break; // TODO: currently assumes there's no 1px or 255px lengths in the top row, try to account for it
else Width += _rawData[i];
}
int pos = 0, x, y;
if (Height == 0)
for (Height = 0; pos < _rawData.Length - 2; Height++) // get mask height
{
if (_rawData[pos++] == 0) break;
for (x = 0; x < Width; pos++)
{
if (_rawData[pos] == 0)
{
bool closed = false;
// this is annoying...
// first 00 counts as 256, next one is conditional
x += 0x100;
if (_rawData[pos + 1] == 0)
{
// for small Widths, 00 00 will be 256 and the 2nd 00 closes it
// for larger Widths, 00 00 can be 512 and remain open
if (x + 0x100 <= Width) x += 0x100;
else closed = true;
pos++;
if (x == Width && !closed) pos++; // this gets the ending 512 case, need to close it
}
else if (x == Width) pos++; // this gets the ending 256 case, close it
// rows ending with a line of 256 or 512 need a bonus value since it's open
// if it ends as 00 00, should be taken care of above where the second 00 is really a bonus pixel
// else if it ends as 00 01, skip the bonus pixel
}
else x += _rawData[pos];
}
}
// start ze image!
_image = new Bitmap(Width, Height, PixelFormat.Format1bppIndexed);
BitmapData bd = GraphicsFunctions.GetBitmapData(_image);
bool draw;
byte[] pixels = new byte[bd.Stride * bd.Height];
for (y = 0, pos = 0, draw = false; y < Height; y++, draw = false)
{
if (raw[pos] == 0xFF) draw = true;
pos++;
int len = 0;
int w = bd.Stride * y;
for (x = 0; x < Width; pos++)
{
if (_rawData[pos] == 0)
{
bool closed = false;
len += 0x100;
if (_rawData[pos + 1] == 0)
{
if (x + len + 0x100 <= Width) len += 0x100;
else closed = true;
pos++;
}
else if (x + len == Width) { pos++; closed = true; }
if (x + len < Width && !closed) continue;
else if (x + len == Width && !closed) pos++;
}
else len += _rawData[pos];
if (draw)
{
for (int x0 = x; x < x0 + len - 1; x++) pixels[w + x / 8] |= (byte)(0x80 >> (x & 7));
if (x != Width) pixels[w + x / 8] |= (byte)(0x80 >> (x & 7)); // will not fire for "throw away" pixel
x++;
}
else x += len;
draw = !draw;
len = 0;
}
}
GraphicsFunctions.CopyBytesToImage(pixels, bd);
_image.UnlockBits(bd);
_isModified = false;
}
/// <summary>Prepares the resource for writing and updates <see cref="Resource.RawData"/>.</summary>
public override void EncodeResource()
{
BitmapData bd = GraphicsFunctions.GetBitmapData(_image);
byte[] pixels = new byte[bd.Stride * bd.Height];
byte[] tempRaw = new byte[pixels.Length];
// assuming 640 width, pixels[] is 80 bytes wide. for MASK to consume 80 bytes a row, it requires 79 color switches. Not happening
// (although a pure alternating static mask would be 641 bytes wide, at that point wtf)
GraphicsFunctions.CopyImageToBytes(bd, pixels);
int len = 0;
for (int y = 0; y < _image.Height; y++)
{
for (int x = 0, numBlack = 0, numWhite = 0, pos = y * bd.Stride; x < _image.Width; x++, len++)
{
byte shift = (byte)(0x80 >> (x & 7));
if ((pixels[pos + x / 8] & shift) == shift)
{ // white
if (x == 0) tempRaw[len++] = 0xFF;
if (numBlack != 0)
{
if (numBlack >= 512) len++;
if (numBlack >= 256) len++;
if ((numBlack & 0xFF) != 0) tempRaw[len] = (byte)(numBlack & 0xFF);
else tempRaw[len] = 1; // SetMask ensures this is a "throw away" pixel, effectively x=Width
}
numWhite++;
numBlack = 0;
}
else
{ // black
if (x == 0) tempRaw[len++] = 1;
if (numWhite != 0)
{
if (numWhite >= 512) len++;
if (numWhite >= 256) len++;
if ((numWhite & 0xFF) != 0) tempRaw[len] = (byte)(numWhite & 0xFF);
else tempRaw[len] = 1; // SetMask ensures this is a "throw away" pixel, effectively x=Width
}
numWhite = 0;
numBlack++;
}
}
}
byte[] raw = new byte[len + 2];
ArrayFunctions.TrimArray(tempRaw, 0, raw);
_rawData = raw;
}
/// <summary>Sets the transparency mask.</summary>
/// <param name="image">New image mask. Converts to <see cref="PixelFormat.Format1bppIndexed"/>, must be <b>640x480</b> or smaller.</param>
/// <exception cref="ArgumentException"><paramref name="image"/> contains a consecutive length of 256 or 512 pixels not located at the end of the image.</exception>
/// <exception cref="BoundaryException"><paramref name="image"/> exceeds maximum allowable dimensions.</exception>
/// <remarks>The MASK format cannot handle the 256 or 512px lengths in the middle of the image. It can however do that at the end of the image, via "throw away" pixels.<br/>
/// Maximum size is defined by <see cref="Panl.MaximumWidth"/> and <see cref="Panl.MaximumHeight"/>.<br/>
/// Black is used as the transparecny color.</remarks>
public void SetMask(Bitmap image) => SetMask(image, Color.FromArgb(0, 0, 0));
/// <summary>Sets the transparency mask.</summary>
/// <param name="image">New image mask. Converts to <see cref="PixelFormat.Format1bppIndexed"/>, must be <b>640x480</b> or smaller.</param>
/// <param name="transparentColor">The Color to be used as transparent.</param>
/// <exception cref="ArgumentException"><paramref name="image"/> contains a consecutive length of 256 or 512 pixels not located at the end of the image.</exception>
/// <exception cref="BoundaryException"><paramref name="image"/> exceeds maximum allowable dimensions.</exception>
/// <remarks>The MASK format cannot handle the 256 or 512px lengths in the middle of the image. It can however do that at the end of the image, via "throw away" pixels.<br/>
/// Maximum size is defined by <see cref="Panl.MaximumWidth"/> and <see cref="Panl.MaximumHeight"/>.</remarks>
public void SetMask(Bitmap image, Color transparentColor)
{
string message = "Image contains line length of 256 or 512 pixels";
if (image.Width > Panl.MaximumWidth || image.Height > Panl.MaximumHeight) throw new BoundaryException("image.Size", Panl.MaximumWidth + "x" + Panl.MaximumHeight);
Bitmap temp = _image;
try
{
// due to special length checks, can't use Common.Functions.ConvertTo1bpp
image = new Bitmap(image); // force to 32bbpRGB
_image = new Bitmap(image.Width, image.Height, PixelFormat.Format1bppIndexed);
// import image data
BitmapData bd32 = GraphicsFunctions.GetBitmapData(image);
byte[] pix32 = new byte[bd32.Stride * bd32.Height];
GraphicsFunctions.CopyImageToBytes(bd32, pix32);
BitmapData bd1 = GraphicsFunctions.GetBitmapData(_image);
byte[] pix1 = new byte[bd1.Stride * bd1.Height];
for (int y = 0; y < image.Height; y++)
for (int x = 0, numBlack = 0, numWhite = 0, pos32 = y * bd32.Stride, pos1 = y * bd1.Stride; x < bd32.Width; x++)
if (pix32[pos32 + x * 4] != transparentColor.B || pix32[pos32 + x * 4 + 1] != transparentColor.G || pix32[pos32 + x * 4 + 2] != transparentColor.R)
{ // white
pix1[pos1 + x / 8] |= (byte)(0x80 >> (x & 7));
// throw if 256px detected, and not end of row (which format can handle)
if ((numBlack % 256) == 0 && x != image.Width - 1) throw new ArgumentException(message, "image");
numBlack = 0;
numWhite++;
}
else
{ // black
if ((numWhite % 256) == 0 && x != image.Width - 1) throw new ArgumentException(message, "image");
numBlack++;
numWhite = 0;
}
GraphicsFunctions.CopyBytesToImage(pix1, bd1);
image.UnlockBits(bd32);
_image.UnlockBits(bd1);
Width = (short)_image.Width;
Height = (short)_image.Height;
_isModified = true;
}
catch (Exception x) { _image = temp; throw x; }
}
#endregion public methods
#region public properties
/// <summary>Gets the monochrome mask image.</summary>
public Bitmap Image => _image;
/// <summary>Gets the width of the Mask image.</summary>
public short Width { get; internal set; }
/// <summary>Gets the height of the Mask image.</summary>
public short Height { get; internal set; }
#endregion public properties
}
}