Skip to content

Commit a67e459

Browse files
committed
Add managed image processing to Ecng.Drawing: size, resize, PNG conversion, text watermark
ImageHelper is the public surface: GetImageSize, ResizeImage, ConvertToPng, AddTextWatermark, GetGifFrames / GetAnimationFrames and GetGifLoopCount / GetAnimationLoopCount, all over a byte array holding an encoded picture. Everything underneath is internal and written in managed code, with no imaging package and no OS graphics API behind it: - PngCodec: every colour type and bit depth, the five scanline filters, Adam7, tRNS, CRC checks; writes RGBA8, its scanlines packed filtered or unfiltered, whichever comes out shorter. - ApngDecoder / ApngEncoder: frame rectangles, SOURCE/OVER blending, NONE/BACKGROUND/PREVIOUS disposal, delays and the loop count. - JpegCodec / JpegProgressiveCodec: 8-bit baseline and progressive Huffman JPEG, grayscale, YCbCr, Adobe CMYK and YCCK, restart markers, the Exif orientation. JpegEncoder writes baseline 4:4:4 at a quality from 1 to 100 on the scale of libjpeg, 90 by default, with Huffman tables built for the picture. - BmpCodec: OS/2 core and Windows V3-V5 headers, indexed 1/4/8 bit, 16/24/32 bit, bitfields, RLE4 and RLE8; writes 32-bit BGRA with a V4 header. - GifCodec: GIF87a/GIF89a with global and local palettes, LZW, interlacing, transparency, disposal 0-3 and the NETSCAPE loop count; every frame is composited onto the logical screen. - TrueTypeFont / TextMask: glyf outlines with cmap formats 4 and 12, rendered once into a mask for the watermark text. The font is an installed family, a .ttf path, the bytes of a font file, or the subset of DejaVu Sans the library carries for a machine that has no font; its license is in Drawing/Fonts and in the package. - RasterImage: the RGBA8 buffer the codecs share, reduced by averaging the source area of every pixel with colors weighted by alpha. A picture keeps its format through resizing and watermarking and is never enlarged; a GIF or an APNG stays animated through resizing, watermarking and conversion to PNG. A JPEG is handled the way its Exif orientation shows it. A picture that needs no change - it fits already, or nothing would be drawn on it - is returned as it came, byte for byte. The ICC profile of a JPEG or a PNG stays with the picture if it is one for RGB; Exif does not. A watermark is one line in the bottom right corner, made smaller to fit between the margins, down to 6 pixels to the em. A picture of more than 25 megapixels is not decoded, nor an animation of more than 512 frames or 100 megapixels of canvas. The 243 tests exercise the codecs through ImageHelper and directly, with hand-built fixtures and ones encoded by Pillow. Squashed from feature/drawing-image-utilities at 2879c7d and the twelve commits of fixes, watermark and encoder work that followed it.
1 parent 2cdcfe5 commit a67e459

55 files changed

Lines changed: 13437 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Drawing/Animation.cs‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
namespace Ecng.Drawing;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
8+
/// <summary>
9+
/// An animation: its canvas, how many times it plays and its frames. The frames are decoded one at a
10+
/// time, as they are asked for, so that a long animation never has all its canvases in memory at once.
11+
/// </summary>
12+
internal sealed class Animation
13+
{
14+
/// <summary>
15+
/// The most frames an animation may have to be decoded.
16+
/// </summary>
17+
public const int MaxFrames = 512;
18+
19+
/// <summary>
20+
/// The most pixels an animation may take to decode, all frames together: every frame costs its
21+
/// canvas, whatever part of it the frame itself covers.
22+
/// </summary>
23+
public const long MaxPixels = 100_000_000;
24+
25+
public Animation(int width, int height, int plays, int frameCount, IEnumerable<AnimationFrame> frames)
26+
{
27+
Width = width;
28+
Height = height;
29+
Plays = plays;
30+
FrameCount = frameCount;
31+
Frames = frames ?? throw new ArgumentNullException(nameof(frames));
32+
}
33+
34+
public int Width { get; }
35+
public int Height { get; }
36+
37+
/// <summary>
38+
/// How many times the animation plays; 0 - endlessly.
39+
/// </summary>
40+
public int Plays { get; }
41+
42+
public int FrameCount { get; }
43+
44+
/// <summary>
45+
/// The frames in order. Enumerating decodes them; each comes as a picture of its own that the caller may change.
46+
/// </summary>
47+
public IEnumerable<AnimationFrame> Frames { get; }
48+
49+
/// <summary>
50+
/// The same animation with every frame replaced by what <paramref name="change"/> makes of it.
51+
/// </summary>
52+
public Animation Select(int width, int height, Func<RasterImage, RasterImage> change)
53+
=> new(width, height, Plays, FrameCount, Frames.Select(frame => frame.WithImage(change(frame.Image))));
54+
55+
/// <summary>
56+
/// Refuses an animation that takes more work than the decoders do for one file.
57+
/// </summary>
58+
/// <param name="frames">Number of frames.</param>
59+
/// <param name="pixels">Pixels to decode and composite, all frames together.</param>
60+
/// <exception cref="InvalidDataException">The animation is beyond <see cref="MaxFrames"/> or <see cref="MaxPixels"/>.</exception>
61+
public static void EnsureWithinBudget(int frames, long pixels)
62+
{
63+
if (frames > MaxFrames)
64+
throw new InvalidDataException($"The animation has {frames} frames: more than the {MaxFrames} that are decoded.");
65+
66+
if (pixels > MaxPixels)
67+
throw new InvalidDataException($"The animation takes {pixels} pixels to decode: more than the {MaxPixels} that are decoded.");
68+
}
69+
}

‎Drawing/AnimationFrame.cs‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace Ecng.Drawing;
2+
3+
using System;
4+
5+
/// <summary>
6+
/// One frame of an animation: the whole canvas as it looks while the frame is shown, and for how long.
7+
/// </summary>
8+
internal sealed class AnimationFrame
9+
{
10+
/// <param name="image">The composited canvas.</param>
11+
/// <param name="delayNumerator">Numerator of the time the frame is shown for, in seconds.</param>
12+
/// <param name="delayDenominator">Denominator of that time; greater than zero.</param>
13+
public AnimationFrame(RasterImage image, int delayNumerator, int delayDenominator)
14+
{
15+
Image = image ?? throw new ArgumentNullException(nameof(image));
16+
17+
if (delayNumerator < 0)
18+
throw new ArgumentOutOfRangeException(nameof(delayNumerator));
19+
20+
if (delayDenominator <= 0)
21+
throw new ArgumentOutOfRangeException(nameof(delayDenominator));
22+
23+
DelayNumerator = delayNumerator;
24+
DelayDenominator = delayDenominator;
25+
}
26+
27+
public RasterImage Image { get; }
28+
29+
/// <summary>
30+
/// The frame is shown for <see cref="DelayNumerator"/> / <see cref="DelayDenominator"/> of a second:
31+
/// kept as the fraction the file states, so that writing it back changes nothing.
32+
/// </summary>
33+
public int DelayNumerator { get; }
34+
35+
/// <inheritdoc cref="DelayNumerator"/>
36+
public int DelayDenominator { get; }
37+
38+
public int DelayMilliseconds
39+
=> (int)Math.Min(int.MaxValue, Math.Round(DelayNumerator * 1000.0 / DelayDenominator, MidpointRounding.AwayFromZero));
40+
41+
/// <summary>
42+
/// The same timing over another picture.
43+
/// </summary>
44+
public AnimationFrame WithImage(RasterImage image)
45+
=> new(image, DelayNumerator, DelayDenominator);
46+
}

0 commit comments

Comments
 (0)