-
-
Notifications
You must be signed in to change notification settings - Fork 893
Expand file tree
/
Copy pathGifEncoderCore.cs
More file actions
475 lines (416 loc) · 19.7 KB
/
Copy pathGifEncoderCore.cs
File metadata and controls
475 lines (416 loc) · 19.7 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
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
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif
{
/// <summary>
/// Implements the GIF encoding protocol.
/// </summary>
internal sealed class GifEncoderCore
{
/// <summary>
/// Used for allocating memory during processing operations.
/// </summary>
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// Configuration bound to the encoding operation.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// A reusable buffer used to reduce allocations.
/// </summary>
private readonly byte[] buffer = new byte[20];
/// <summary>
/// The quantizer used to generate the color palette.
/// </summary>
private readonly IQuantizer quantizer;
/// <summary>
/// The color table mode: Global or local.
/// </summary>
private GifColorTableMode? colorTableMode;
/// <summary>
/// The number of bits requires to store the color palette.
/// </summary>
private int bitDepth;
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="options">The options for the encoder.</param>
public GifEncoderCore(Configuration configuration, IGifEncoderOptions options)
{
this.configuration = configuration;
this.memoryAllocator = configuration.MemoryAllocator;
this.quantizer = options.Quantizer;
this.colorTableMode = options.ColorTableMode;
}
/// <summary>
/// Encodes the image to the specified stream from the <see cref="Image{TPixel}"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="Image{TPixel}"/> to encode from.</param>
/// <param name="stream">The <see cref="Stream"/> to encode the image data to.</param>
public void Encode<TPixel>(Image<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
ImageMetadata metadata = image.Metadata;
GifMetadata gifMetadata = metadata.GetGifMetadata();
this.colorTableMode ??= gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette.
IQuantizedFrame<TPixel> quantized;
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{
quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds());
}
// Get the number of bits.
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
// Write the header.
this.WriteHeader(stream);
// Write the LSD.
int index = this.GetTransparentIndex(quantized);
this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, index, useGlobalTable, stream);
if (useGlobalTable)
{
this.WriteColorTable(quantized, stream);
}
// Write the comments.
this.WriteComments(gifMetadata, stream);
// Write application extension to allow additional frames.
if (image.Frames.Count > 1)
{
this.WriteApplicationExtension(stream, gifMetadata.RepeatCount);
}
if (useGlobalTable)
{
this.EncodeGlobal(image, quantized, index, stream);
}
else
{
this.EncodeLocal(image, quantized, stream);
}
// Clean up.
quantized?.Dispose();
// TODO: Write extension etc
stream.WriteByte(GifConstants.EndIntroducer);
}
private void EncodeGlobal<TPixel>(Image<TPixel> image, IQuantizedFrame<TPixel> quantized, int transparencyIndex, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<TPixel> frame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
this.WriteGraphicalControlExtension(frameMetadata, transparencyIndex, stream);
this.WriteImageDescriptor(frame, false, stream);
if (i == 0)
{
this.WriteImageData(quantized, stream);
}
else
{
using (IFrameQuantizer<TPixel> paletteFrameQuantizer = new PaletteFrameQuantizer<TPixel>(this.configuration, this.quantizer.Options, quantized.Palette))
using (IQuantizedFrame<TPixel> paletteQuantized = paletteFrameQuantizer.QuantizeFrame(frame, frame.Bounds()))
{
this.WriteImageData(paletteQuantized, stream);
}
}
}
}
private void EncodeLocal<TPixel>(Image<TPixel> image, IQuantizedFrame<TPixel> quantized, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
ImageFrame<TPixel> previousFrame = null;
GifFrameMetadata previousMeta = null;
foreach (ImageFrame<TPixel> frame in image.Frames)
{
ImageFrameMetadata metadata = frame.Metadata;
GifFrameMetadata frameMetadata = metadata.GetGifMetadata();
if (quantized is null)
{
// Allow each frame to be encoded at whatever color depth the frame designates if set.
if (previousFrame != null && previousMeta.ColorTableLength != frameMetadata.ColorTableLength
&& frameMetadata.ColorTableLength > 0)
{
var options = new QuantizerOptions
{
Dither = this.quantizer.Options.Dither,
DitherScale = this.quantizer.Options.DitherScale,
MaxColors = frameMetadata.ColorTableLength
};
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration, options))
{
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
}
else
{
using (IFrameQuantizer<TPixel> frameQuantizer = this.quantizer.CreateFrameQuantizer<TPixel>(this.configuration))
{
quantized = frameQuantizer.QuantizeFrame(frame, frame.Bounds());
}
}
}
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(quantized.Palette.Length).Clamp(1, 8);
this.WriteGraphicalControlExtension(frameMetadata, this.GetTransparentIndex(quantized), stream);
this.WriteImageDescriptor(frame, true, stream);
this.WriteColorTable(quantized, stream);
this.WriteImageData(quantized, stream);
quantized?.Dispose();
quantized = null; // So next frame can regenerate it
previousFrame = frame;
previousMeta = frameMetadata;
}
}
/// <summary>
/// Returns the index of the most transparent color in the palette.
/// </summary>
/// <param name="quantized">The quantized frame.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>
/// The <see cref="int"/>.
/// </returns>
private int GetTransparentIndex<TPixel>(IQuantizedFrame<TPixel> quantized)
where TPixel : struct, IPixel<TPixel>
{
// Transparent pixels are much more likely to be found at the end of a palette
int index = -1;
int length = quantized.Palette.Length;
using (IMemoryOwner<Rgba32> rgbaBuffer = this.memoryAllocator.Allocate<Rgba32>(length))
{
Span<Rgba32> rgbaSpan = rgbaBuffer.GetSpan();
ref Rgba32 paletteRef = ref MemoryMarshal.GetReference(rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba32(this.configuration, quantized.Palette.Span, rgbaSpan);
for (int i = quantized.Palette.Length - 1; i >= 0; i--)
{
if (Unsafe.Add(ref paletteRef, i).Equals(default))
{
index = i;
}
}
}
return index;
}
/// <summary>
/// Writes the file header signature and version to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteHeader(Stream stream) => stream.Write(GifConstants.MagicNumber, 0, GifConstants.MagicNumber.Length);
/// <summary>
/// Writes the logical screen descriptor to the stream.
/// </summary>
/// <param name="metadata">The image metadata.</param>
/// <param name="width">The image width.</param>
/// <param name="height">The image height.</param>
/// <param name="transparencyIndex">The transparency index to set the default background index to.</param>
/// <param name="useGlobalTable">Whether to use a global or local color table.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteLogicalScreenDescriptor(
ImageMetadata metadata,
int width,
int height,
int transparencyIndex,
bool useGlobalTable,
Stream stream)
{
byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1);
// The Pixel Aspect Ratio is defined to be the quotient of the pixel's
// width over its height. The value range in this field allows
// specification of the widest pixel of 4:1 to the tallest pixel of
// 1:4 in increments of 1/64th.
//
// Values : 0 - No aspect ratio information is given.
// 1..255 - Value used in the computation.
//
// Aspect Ratio = (Pixel Aspect Ratio + 15) / 64
byte ratio = 0;
if (metadata.ResolutionUnits == PixelResolutionUnit.AspectRatio)
{
double hr = metadata.HorizontalResolution;
double vr = metadata.VerticalResolution;
if (hr != vr)
{
if (hr > vr)
{
ratio = (byte)((hr * 64) - 15);
}
else
{
ratio = (byte)(((1 / vr) * 64) - 15);
}
}
}
var descriptor = new GifLogicalScreenDescriptor(
width: (ushort)width,
height: (ushort)height,
packed: packedValue,
backgroundColorIndex: unchecked((byte)transparencyIndex),
ratio);
descriptor.WriteTo(this.buffer);
stream.Write(this.buffer, 0, GifLogicalScreenDescriptor.Size);
}
/// <summary>
/// Writes the application extension to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="repeatCount">The animated image repeat count.</param>
private void WriteApplicationExtension(Stream stream, ushort repeatCount)
{
// Application Extension Header
if (repeatCount != 1)
{
var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount);
this.WriteExtension(loopingExtension, stream);
}
}
/// <summary>
/// Writes the image comments to the stream.
/// </summary>
/// <param name="metadata">The metadata to be extract the comment data.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteComments(GifMetadata metadata, Stream stream)
{
if (metadata.Comments.Count == 0)
{
return;
}
foreach (string comment in metadata.Comments)
{
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.CommentLabel;
stream.Write(this.buffer, 0, 2);
// Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan<char> commentSpan = comment.AsSpan();
int idx = 0;
for (; idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; idx += GifConstants.MaxCommentSubBlockLength)
{
WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength);
}
// Write the length bytes, if any, to another sub block.
if (idx < comment.Length)
{
int remaining = comment.Length - idx;
WriteCommentSubBlock(stream, commentSpan, idx, remaining);
}
stream.WriteByte(GifConstants.Terminator);
}
}
/// <summary>
/// Writes a comment sub-block to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="commentSpan">Comment as a Span.</param>
/// <param name="idx">Current start index.</param>
/// <param name="length">The length of the string to write. Should not exceed 255 bytes.</param>
private static void WriteCommentSubBlock(Stream stream, ReadOnlySpan<char> commentSpan, int idx, int length)
{
string subComment = commentSpan.Slice(idx, length).ToString();
byte[] subCommentBytes = GifConstants.Encoding.GetBytes(subComment);
stream.WriteByte((byte)length);
stream.Write(subCommentBytes, 0, length);
}
/// <summary>
/// Writes the graphics control extension to the stream.
/// </summary>
/// <param name="metadata">The metadata of the image or frame.</param>
/// <param name="transparencyIndex">The index of the color in the color palette to make transparent.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteGraphicalControlExtension(GifFrameMetadata metadata, int transparencyIndex, Stream stream)
{
byte packedValue = GifGraphicControlExtension.GetPackedValue(
disposalMethod: metadata.DisposalMethod,
transparencyFlag: transparencyIndex > -1);
var extension = new GifGraphicControlExtension(
packed: packedValue,
delayTime: (ushort)metadata.FrameDelay,
transparencyIndex: unchecked((byte)transparencyIndex));
this.WriteExtension(extension, stream);
}
/// <summary>
/// Writes the provided extension to the stream.
/// </summary>
/// <param name="extension">The extension to write to the stream.</param>
/// <param name="stream">The stream to write to.</param>
public void WriteExtension(IGifExtension extension, Stream stream)
{
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = extension.Label;
int extensionSize = extension.WriteTo(this.buffer.AsSpan(2));
this.buffer[extensionSize + 2] = GifConstants.Terminator;
stream.Write(this.buffer, 0, extensionSize + 3);
}
/// <summary>
/// Writes the image descriptor to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to be encoded.</param>
/// <param name="hasColorTable">Whether to use the global color table.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageDescriptor<TPixel>(ImageFrame<TPixel> image, bool hasColorTable, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
byte packedValue = GifImageDescriptor.GetPackedValue(
localColorTableFlag: hasColorTable,
interfaceFlag: false,
sortFlag: false,
localColorTableSize: this.bitDepth - 1);
var descriptor = new GifImageDescriptor(
left: 0,
top: 0,
width: (ushort)image.Width,
height: (ushort)image.Height,
packed: packedValue);
descriptor.WriteTo(this.buffer);
stream.Write(this.buffer, 0, GifImageDescriptor.Size);
}
/// <summary>
/// Writes the color table to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="ImageFrame{TPixel}"/> to encode.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteColorTable<TPixel>(IQuantizedFrame<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
// The maximum number of colors for the bit depth
int colorTableLength = ImageMaths.GetColorCountForBitDepth(this.bitDepth) * 3;
int pixelCount = image.Palette.Length;
using (IManagedByteBuffer colorTable = this.memoryAllocator.AllocateManagedByteBuffer(colorTableLength))
{
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration,
image.Palette.Span,
colorTable.GetSpan(),
pixelCount);
stream.Write(colorTable.Array, 0, colorTableLength);
}
}
/// <summary>
/// Writes the image pixel data to the stream.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="image">The <see cref="IQuantizedFrame{TPixel}"/> containing indexed pixels.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteImageData<TPixel>(IQuantizedFrame<TPixel> image, Stream stream)
where TPixel : struct, IPixel<TPixel>
{
using (var encoder = new LzwEncoder(this.memoryAllocator, (byte)this.bitDepth))
{
encoder.Encode(image.GetPixelSpan(), stream);
}
}
}
}