// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading.Tasks; using SixLabors.Fonts; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Primitives; using SixLabors.ImageSharp.Processing.Drawing.Brushes; using SixLabors.ImageSharp.Processing.Drawing.Pens; using SixLabors.ImageSharp.Processing.Drawing.Processors; using SixLabors.ImageSharp.Processing.Processors; using SixLabors.Memory; using SixLabors.Primitives; using SixLabors.Shapes; namespace SixLabors.ImageSharp.Processing.Text.Processors { /// /// Using the brush as a source of pixels colors blends the brush color with source. /// /// The pixel format. internal class DrawTextProcessor : ImageProcessor where TPixel : struct, IPixel { private CachingGlyphRenderer textRenderer; /// /// Initializes a new instance of the class. /// /// The options /// The text we want to render /// The font we want to render with /// The brush to source pixel colors from. /// The pen to outline text with. /// The location on the image to start drawign the text from. public DrawTextProcessor(TextGraphicsOptions options, string text, Font font, IBrush brush, IPen pen, PointF location) { Guard.NotNull(text, nameof(text)); Guard.NotNull(font, nameof(font)); if (brush == null && pen == null) { throw new ArgumentNullException($"at least one of {nameof(brush)} or {nameof(pen)} must not be null"); } this.Options = options; this.Text = text; this.Font = font; this.Location = location; this.Brush = brush; this.Pen = pen; } /// /// Gets the brush. /// public IBrush Brush { get; } /// /// Gets the options /// public TextGraphicsOptions Options { get; } /// /// Gets the text /// public string Text { get; } /// /// Gets the pen used for outlining the text, if Null then we will not outline /// public IPen Pen { get; } /// /// Gets the font used to render the text. /// public Font Font { get; } /// /// Gets the location to draw the text at. /// public PointF Location { get; } protected override void BeforeImageApply(Image source, Rectangle sourceRectangle) { base.BeforeImageApply(source, sourceRectangle); // do everythign at the image level as we are deligating the processing down to other processors var style = new RendererOptions(this.Font, this.Options.DpiX, this.Options.DpiY, this.Location) { ApplyKerning = this.Options.ApplyKerning, TabWidth = this.Options.TabWidth, WrappingWidth = this.Options.WrapTextWidth, HorizontalAlignment = this.Options.HorizontalAlignment, VerticalAlignment = this.Options.VerticalAlignment }; this.textRenderer = new CachingGlyphRenderer(source.GetMemoryAllocator(), this.Text.Length, this.Pen, this.Brush != null); this.textRenderer.Options = (GraphicsOptions)this.Options; TextRenderer.RenderTextTo(this.textRenderer, this.Text, style); } protected override void AfterImageApply(Image source, Rectangle sourceRectangle) { base.AfterImageApply(source, sourceRectangle); this.textRenderer?.Dispose(); this.textRenderer = null; } /// protected override void OnFrameApply(ImageFrame source, Rectangle sourceRectangle, Configuration configuration) { // this is a no-op as we have processes all as an image, we should be able to pass out of before email apply a skip frames outcome Draw(this.textRenderer.FillOperations, this.Brush); Draw(this.textRenderer.OutlineOperations, this.Pen?.StrokeFill); void Draw(List operations, IBrush brush) { if (operations?.Count > 0) { using (BrushApplicator app = brush.CreateApplicator(source, sourceRectangle, this.textRenderer.Options)) { foreach (DrawingOperation operation in operations) { Buffer2D buffer = operation.Map; int startY = operation.Location.Y; int startX = operation.Location.X; int offSetSpan = 0; if (startX < 0) { offSetSpan = -startX; startX = 0; } int fistRow = 0; if (startY < 0) { fistRow = -startY; } int end = operation.Map.Height; int maxHeight = source.Height - startY; end = Math.Min(end, maxHeight); for (int row = fistRow; row < end; row++) { int y = startY + row; Span span = buffer.GetRowSpan(row).Slice(offSetSpan); app.Apply(span, startX, y); } } } } } } private struct DrawingOperation { public Buffer2D Map { get; set; } public Point Location { get; set; } } private class CachingGlyphRenderer : IGlyphRenderer, IDisposable { private PathBuilder builder; private Point currentRenderPosition = default(Point); private int currentRenderingGlyph = 0; private int offset = 0; private PointF currentPoint = default(PointF); private HashSet renderedGlyphs = new HashSet(); private Dictionary> glyphMap; private Dictionary> glyphMapPen; private bool renderOutline = false; private bool renderFill = false; private bool raterizationRequired = false; public CachingGlyphRenderer(MemoryAllocator memoryManager, int size, IPen pen, bool renderFill) { this.MemoryManager = memoryManager; this.Pen = pen; this.renderFill = renderFill; this.renderOutline = pen != null; this.offset = 2; if (this.renderFill) { this.FillOperations = new List(size); this.glyphMap = new Dictionary>(); } if (this.renderOutline) { this.offset = (int)MathF.Ceiling((pen.StrokeWidth * 2) + 2); this.OutlineOperations = new List(size); this.glyphMapPen = new Dictionary>(); } this.builder = new PathBuilder(); } public List FillOperations { get; } public List OutlineOperations { get; } public MemoryAllocator MemoryManager { get; internal set; } public IPen Pen { get; internal set; } public GraphicsOptions Options { get; internal set; } public void BeginFigure() { this.builder.StartFigure(); } public bool BeginGlyph(RectangleF bounds, int cacheKey) { this.currentRenderPosition = Point.Truncate(bounds.Location); // we have offset our rendering origion a little bit down to prevent edge cropping, move the draw origin up to compensate this.currentRenderPosition = new Point(this.currentRenderPosition.X - this.offset, this.currentRenderPosition.Y - this.offset); this.currentRenderingGlyph = cacheKey; if (this.renderedGlyphs.Contains(cacheKey)) { // we have already drawn the glyph vectors skip trying again this.raterizationRequired = false; return false; } // we check to see if we have a render cache and if we do then we render else this.builder.Clear(); // ensure all glyphs render around [zero, zero] so offset negative root positions so when we draw the glyph we can offet it back this.builder.SetOrigin(new PointF(-(int)bounds.X + this.offset, -(int)bounds.Y + this.offset)); this.raterizationRequired = true; return true; } public void BeginText(RectangleF bounds) { // not concerned about this one this.OutlineOperations?.Clear(); this.FillOperations?.Clear(); } public void CubicBezierTo(PointF secondControlPoint, PointF thirdControlPoint, PointF point) { this.builder.AddBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point); this.currentPoint = point; } public void Dispose() { if (this.renderFill) { foreach (KeyValuePair> m in this.glyphMap) { m.Value.Dispose(); } } if (this.renderOutline) { foreach (KeyValuePair> m in this.glyphMapPen) { m.Value.Dispose(); } } } public void EndFigure() { this.builder.CloseFigure(); } public void EndGlyph() { // has the glyoh been rendedered already???? if (this.raterizationRequired) { IPath path = this.builder.Build(); if (this.renderFill) { this.glyphMap[this.currentRenderingGlyph] = this.Render(path); } if (this.renderOutline) { if (this.Pen.StrokePattern.Length == 0) { path = path.GenerateOutline(this.Pen.StrokeWidth); } else { path = path.GenerateOutline(this.Pen.StrokeWidth, this.Pen.StrokePattern); } this.glyphMapPen[this.currentRenderingGlyph] = this.Render(path); } this.renderedGlyphs.Add(this.currentRenderingGlyph); } if (this.renderFill) { this.FillOperations.Add(new DrawingOperation { Location = this.currentRenderPosition, Map = this.glyphMap[this.currentRenderingGlyph] }); } if (this.renderOutline) { this.OutlineOperations.Add(new DrawingOperation { Location = this.currentRenderPosition, Map = this.glyphMapPen[this.currentRenderingGlyph] }); } } private Buffer2D Render(IPath path) { Size size = Rectangle.Ceiling(path.Bounds).Size; size = new Size(size.Width + (this.offset * 2), size.Height + (this.offset * 2)); float subpixelCount = 4; float offset = 0.5f; if (this.Options.Antialias) { offset = 0f; // we are antialising skip offsetting as real antalising should take care of offset. subpixelCount = this.Options.AntialiasSubpixelDepth; if (subpixelCount < 4) { subpixelCount = 4; } } // take the path inside the path builder, scan thing and generate a Buffer2d representing the glyph and cache it. Buffer2D fullBuffer = this.MemoryManager.Allocate2D(size.Width + 1, size.Height + 1, true); using (IBuffer bufferBacking = this.MemoryManager.Allocate(path.MaxIntersections)) using (IBuffer rowIntersectionBuffer = this.MemoryManager.Allocate(size.Width)) { float subpixelFraction = 1f / subpixelCount; float subpixelFractionPoint = subpixelFraction / subpixelCount; for (int y = 0; y <= size.Height; y++) { Span scanline = fullBuffer.GetRowSpan(y); bool scanlineDirty = false; float yPlusOne = y + 1; for (float subPixel = (float)y; subPixel < yPlusOne; subPixel += subpixelFraction) { var start = new PointF(path.Bounds.Left - 1, subPixel); var end = new PointF(path.Bounds.Right + 1, subPixel); Span intersectionSpan = rowIntersectionBuffer.GetSpan(); Span buffer = bufferBacking.GetSpan(); int pointsFound = path.FindIntersections(start, end, intersectionSpan); if (pointsFound == 0) { // nothing on this line skip continue; } for (int i = 0; i < pointsFound && i < intersectionSpan.Length; i++) { buffer[i] = intersectionSpan[i].X; } QuickSort(buffer.Slice(0, pointsFound)); for (int point = 0; point < pointsFound; point += 2) { // points will be paired up float scanStart = buffer[point]; float scanEnd = buffer[point + 1]; int startX = (int)MathF.Floor(scanStart + offset); int endX = (int)MathF.Floor(scanEnd + offset); if (startX >= 0 && startX < scanline.Length) { for (float x = scanStart; x < startX + 1; x += subpixelFraction) { scanline[startX] += subpixelFractionPoint; scanlineDirty = true; } } if (endX >= 0 && endX < scanline.Length) { for (float x = endX; x < scanEnd; x += subpixelFraction) { scanline[endX] += subpixelFractionPoint; scanlineDirty = true; } } int nextX = startX + 1; endX = Math.Min(endX, scanline.Length); // reduce to end to the right edge nextX = Math.Max(nextX, 0); for (int x = nextX; x < endX; x++) { scanline[x] += subpixelFraction; scanlineDirty = true; } } } if (scanlineDirty) { if (!this.Options.Antialias) { for (int x = 0; x < size.Width; x++) { if (scanline[x] >= 0.5) { scanline[x] = 1; } else { scanline[x] = 0; } } } } } } return fullBuffer; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void Swap(Span data, int left, int right) { float tmp = data[left]; data[left] = data[right]; data[right] = tmp; } private static void QuickSort(Span data) { QuickSort(data, 0, data.Length - 1); } private static void QuickSort(Span data, int lo, int hi) { if (lo < hi) { int p = Partition(data, lo, hi); QuickSort(data, lo, p); QuickSort(data, p + 1, hi); } } private static int Partition(Span data, int lo, int hi) { float pivot = data[lo]; int i = lo - 1; int j = hi + 1; while (true) { do { i = i + 1; } while (data[i] < pivot && i < hi); do { j = j - 1; } while (data[j] > pivot && j > lo); if (i >= j) { return j; } Swap(data, i, j); } } public void EndText() { } public void LineTo(PointF point) { this.builder.AddLine(this.currentPoint, point); this.currentPoint = point; } public void MoveTo(PointF point) { this.builder.StartFigure(); this.currentPoint = point; } public void QuadraticBezierTo(PointF secondControlPoint, PointF point) { this.builder.AddBezier(this.currentPoint, secondControlPoint, point); this.currentPoint = point; } } } }