mirror of https://github.com/SixLabors/ImageSharp
16 changed files with 1088 additions and 1026 deletions
@ -0,0 +1,111 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
|
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.ParallelUtils; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Drawing |
|||
{ |
|||
/// <summary>
|
|||
/// Combines two images together by blending the pixels.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixelBg">The pixel format of destination image.</typeparam>
|
|||
/// <typeparam name="TPixelFg">The pixel format of source image.</typeparam>
|
|||
internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg> |
|||
where TPixelBg : struct, IPixel<TPixelBg> |
|||
where TPixelFg : struct, IPixel<TPixelFg> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelDst, TPixelSrc}"/> class.
|
|||
/// </summary>
|
|||
/// <param name="image">The image to blend with the currently processing image.</param>
|
|||
/// <param name="location">The location to draw the blended image.</param>
|
|||
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
|
|||
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
|
|||
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
|
|||
public DrawImageProcessor( |
|||
Image<TPixelFg> image, |
|||
Point location, |
|||
PixelColorBlendingMode colorBlendingMode, |
|||
PixelAlphaCompositionMode alphaCompositionMode, |
|||
float opacity) |
|||
{ |
|||
Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity)); |
|||
|
|||
this.Image = image; |
|||
this.Opacity = opacity; |
|||
this.Blender = PixelOperations<TPixelBg>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode); |
|||
this.Location = location; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the image to blend
|
|||
/// </summary>
|
|||
public Image<TPixelFg> Image { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the opacity of the image to blend
|
|||
/// </summary>
|
|||
public float Opacity { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the pixel blender
|
|||
/// </summary>
|
|||
public PixelBlender<TPixelBg> Blender { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the location to draw the blended image
|
|||
/// </summary>
|
|||
public Point Location { get; } |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply( |
|||
ImageFrame<TPixelBg> source, |
|||
Rectangle sourceRectangle, |
|||
Configuration configuration) |
|||
{ |
|||
Image<TPixelFg> targetImage = this.Image; |
|||
PixelBlender<TPixelBg> blender = this.Blender; |
|||
int locationY = this.Location.Y; |
|||
|
|||
// Align start/end positions.
|
|||
Rectangle bounds = targetImage.Bounds(); |
|||
|
|||
int minX = Math.Max(this.Location.X, sourceRectangle.X); |
|||
int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Right); |
|||
int targetX = minX - this.Location.X; |
|||
|
|||
int minY = Math.Max(this.Location.Y, sourceRectangle.Y); |
|||
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom); |
|||
|
|||
int width = maxX - minX; |
|||
|
|||
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); |
|||
|
|||
// not a valid operation because rectangle does not overlap with this image.
|
|||
if (workingRect.Width <= 0 || workingRect.Height <= 0) |
|||
{ |
|||
throw new ImageProcessingException( |
|||
"Cannot draw image because the source image does not overlap the target image."); |
|||
} |
|||
|
|||
ParallelHelper.IterateRows( |
|||
workingRect, |
|||
configuration, |
|||
rows => |
|||
{ |
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
Span<TPixelBg> background = source.GetPixelRowSpan(y).Slice(minX, width); |
|||
Span<TPixelFg> foreground = |
|||
targetImage.GetPixelRowSpan(y - locationY).Slice(targetX, width); |
|||
blender.Blend<TPixelFg>(configuration, background, background, foreground, this.Opacity); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
|
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.ParallelUtils; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Drawing |
|||
{ |
|||
/// <summary>
|
|||
/// Using the brush as a source of pixels colors blends the brush color with source.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal class FillProcessor<TPixel> : ImageProcessor<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
private readonly FillProcessor definition; |
|||
|
|||
public FillProcessor(FillProcessor definition) |
|||
{ |
|||
this.definition = definition; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) |
|||
{ |
|||
int startX = sourceRectangle.X; |
|||
int endX = sourceRectangle.Right; |
|||
int startY = sourceRectangle.Y; |
|||
int endY = sourceRectangle.Bottom; |
|||
|
|||
// Align start/end positions.
|
|||
int minX = Math.Max(0, startX); |
|||
int maxX = Math.Min(source.Width, endX); |
|||
int minY = Math.Max(0, startY); |
|||
int maxY = Math.Min(source.Height, endY); |
|||
|
|||
int width = maxX - minX; |
|||
|
|||
var workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY); |
|||
|
|||
IBrush brush = this.definition.Brush; |
|||
GraphicsOptions options = this.definition.Options; |
|||
|
|||
// If there's no reason for blending, then avoid it.
|
|||
if (this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush)) |
|||
{ |
|||
ParallelExecutionSettings parallelSettings = configuration.GetParallelSettings().MultiplyMinimumPixelsPerTask(4); |
|||
|
|||
TPixel colorPixel = solidBrush.Color.ToPixel<TPixel>(); |
|||
|
|||
ParallelHelper.IterateRows( |
|||
workingRect, |
|||
parallelSettings, |
|||
rows => |
|||
{ |
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
source.GetPixelRowSpan(y).Slice(minX, width).Fill(colorPixel); |
|||
} |
|||
}); |
|||
} |
|||
else |
|||
{ |
|||
// Reset offset if necessary.
|
|||
if (minX > 0) |
|||
{ |
|||
startX = 0; |
|||
} |
|||
|
|||
if (minY > 0) |
|||
{ |
|||
startY = 0; |
|||
} |
|||
|
|||
using (IMemoryOwner<float> amount = source.MemoryAllocator.Allocate<float>(width)) |
|||
using (BrushApplicator<TPixel> applicator = brush.CreateApplicator( |
|||
source, |
|||
sourceRectangle, |
|||
options)) |
|||
{ |
|||
amount.GetSpan().Fill(1f); |
|||
|
|||
ParallelHelper.IterateRows( |
|||
workingRect, |
|||
configuration, |
|||
rows => |
|||
{ |
|||
for (int y = rows.Min; y < rows.Max; y++) |
|||
{ |
|||
int offsetY = y - startY; |
|||
int offsetX = minX - startX; |
|||
|
|||
applicator.Apply(amount.GetSpan(), offsetX, offsetY); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private bool IsSolidBrushWithoutBlending(out SolidBrush solidBrush) |
|||
{ |
|||
solidBrush = this.definition.Brush as SolidBrush; |
|||
|
|||
if (solidBrush == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return this.definition.Options.IsOpaqueColorWithoutBlending(solidBrush.Color); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,195 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
|
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Primitives; |
|||
using SixLabors.ImageSharp.Utils; |
|||
using SixLabors.Primitives; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Drawing |
|||
{ |
|||
/// <summary>
|
|||
/// Using a brush and a shape fills shape with contents of brush the
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The type of the color.</typeparam>
|
|||
/// <seealso cref="ImageProcessor{TPixel}" />
|
|||
internal class FillRegionProcessor<TPixel> : ImageProcessor<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
private readonly FillRegionProcessor definition; |
|||
|
|||
public FillRegionProcessor(FillRegionProcessor definition) |
|||
{ |
|||
this.definition = definition; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) |
|||
{ |
|||
GraphicsOptions options = this.definition.Options; |
|||
IBrush brush = this.definition.Brush; |
|||
Region region = this.definition.Region; |
|||
Rectangle rect = region.Bounds; |
|||
|
|||
// Align start/end positions.
|
|||
int minX = Math.Max(0, rect.Left); |
|||
int maxX = Math.Min(source.Width, rect.Right); |
|||
int minY = Math.Max(0, rect.Top); |
|||
int maxY = Math.Min(source.Height, rect.Bottom); |
|||
if (minX >= maxX) |
|||
{ |
|||
return; // no effect inside image;
|
|||
} |
|||
|
|||
if (minY >= maxY) |
|||
{ |
|||
return; // no effect inside image;
|
|||
} |
|||
|
|||
int maxIntersections = region.MaxIntersections; |
|||
float subpixelCount = 4; |
|||
|
|||
// we need to offset the pixel grid to account for when we outline a path.
|
|||
// basically if the line is [1,2] => [3,2] then when outlining at 1 we end up with a region of [0.5,1.5],[1.5, 1.5],[3.5,2.5],[2.5,2.5]
|
|||
// and this can cause missed fills when not using antialiasing.so we offset the pixel grid by 0.5 in the x & y direction thus causing the#
|
|||
// region to align with the pixel grid.
|
|||
float offset = 0.5f; |
|||
if (options.Antialias) |
|||
{ |
|||
offset = 0f; // we are antialiasing skip offsetting as real antialiasing should take care of offset.
|
|||
subpixelCount = options.AntialiasSubpixelDepth; |
|||
if (subpixelCount < 4) |
|||
{ |
|||
subpixelCount = 4; |
|||
} |
|||
} |
|||
|
|||
using (BrushApplicator<TPixel> applicator = brush.CreateApplicator(source, rect, options)) |
|||
{ |
|||
int scanlineWidth = maxX - minX; |
|||
using (IMemoryOwner<float> bBuffer = source.MemoryAllocator.Allocate<float>(maxIntersections)) |
|||
using (IMemoryOwner<float> bScanline = source.MemoryAllocator.Allocate<float>(scanlineWidth)) |
|||
{ |
|||
bool scanlineDirty = true; |
|||
float subpixelFraction = 1f / subpixelCount; |
|||
float subpixelFractionPoint = subpixelFraction / subpixelCount; |
|||
|
|||
Span<float> buffer = bBuffer.GetSpan(); |
|||
Span<float> scanline = bScanline.GetSpan(); |
|||
|
|||
bool isSolidBrushWithoutBlending = this.IsSolidBrushWithoutBlending(out SolidBrush solidBrush); |
|||
TPixel solidBrushColor = isSolidBrushWithoutBlending ? solidBrush.Color.ToPixel<TPixel>() : default; |
|||
|
|||
for (int y = minY; y < maxY; y++) |
|||
{ |
|||
if (scanlineDirty) |
|||
{ |
|||
scanline.Clear(); |
|||
scanlineDirty = false; |
|||
} |
|||
|
|||
float yPlusOne = y + 1; |
|||
for (float subPixel = (float)y; subPixel < yPlusOne; subPixel += subpixelFraction) |
|||
{ |
|||
int pointsFound = region.Scan(subPixel + offset, buffer, configuration); |
|||
if (pointsFound == 0) |
|||
{ |
|||
// nothing on this line, skip
|
|||
continue; |
|||
} |
|||
|
|||
QuickSort.Sort(buffer.Slice(0, pointsFound)); |
|||
|
|||
for (int point = 0; point < pointsFound; point += 2) |
|||
{ |
|||
// points will be paired up
|
|||
float scanStart = buffer[point] - minX; |
|||
float scanEnd = buffer[point + 1] - minX; |
|||
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 (!options.Antialias) |
|||
{ |
|||
bool hasOnes = false; |
|||
bool hasZeros = false; |
|||
for (int x = 0; x < scanlineWidth; x++) |
|||
{ |
|||
if (scanline[x] >= 0.5) |
|||
{ |
|||
scanline[x] = 1; |
|||
hasOnes = true; |
|||
} |
|||
else |
|||
{ |
|||
scanline[x] = 0; |
|||
hasZeros = true; |
|||
} |
|||
} |
|||
|
|||
if (isSolidBrushWithoutBlending && hasOnes != hasZeros) |
|||
{ |
|||
if (hasOnes) |
|||
{ |
|||
source.GetPixelRowSpan(y).Slice(minX, scanlineWidth).Fill(solidBrushColor); |
|||
} |
|||
|
|||
continue; |
|||
} |
|||
} |
|||
|
|||
applicator.Apply(scanline, minX, y); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private bool IsSolidBrushWithoutBlending(out SolidBrush solidBrush) |
|||
{ |
|||
solidBrush = this.definition.Brush as SolidBrush; |
|||
|
|||
if (solidBrush == null) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
return this.definition.Options.IsOpaqueColorWithoutBlending(solidBrush.Color); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,446 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System; |
|||
using System.Buffers; |
|||
using System.Collections.Generic; |
|||
|
|||
using SixLabors.Fonts; |
|||
using SixLabors.ImageSharp.Advanced; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Utils; |
|||
using SixLabors.Memory; |
|||
using SixLabors.Primitives; |
|||
using SixLabors.Shapes; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing.Processors.Text |
|||
{ |
|||
/// <summary>
|
|||
/// Using the brush as a source of pixels colors blends the brush color with source.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
internal class DrawTextProcessor<TPixel> : ImageProcessor<TPixel> |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
private CachingGlyphRenderer textRenderer; |
|||
|
|||
private readonly DrawTextProcessor definition; |
|||
|
|||
public DrawTextProcessor(DrawTextProcessor definition) |
|||
{ |
|||
this.definition = definition; |
|||
} |
|||
|
|||
private TextGraphicsOptions Options => this.definition.Options; |
|||
|
|||
private Font Font => this.definition.Font; |
|||
|
|||
private PointF Location => this.definition.Location; |
|||
|
|||
private string Text => this.definition.Text; |
|||
|
|||
private IPen Pen => this.definition.Pen; |
|||
|
|||
private IBrush Brush => this.definition.Brush; |
|||
|
|||
protected override void BeforeImageApply(Image<TPixel> source, Rectangle sourceRectangle) |
|||
{ |
|||
base.BeforeImageApply(source, sourceRectangle); |
|||
|
|||
// do everything at the image level as we are delegating 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; |
|||
var renderer = new TextRenderer(this.textRenderer); |
|||
renderer.RenderText(this.Text, style); |
|||
} |
|||
|
|||
protected override void AfterImageApply(Image<TPixel> source, Rectangle sourceRectangle) |
|||
{ |
|||
base.AfterImageApply(source, sourceRectangle); |
|||
this.textRenderer?.Dispose(); |
|||
this.textRenderer = null; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void OnFrameApply(ImageFrame<TPixel> 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<DrawingOperation> operations, IBrush brush) |
|||
{ |
|||
if (operations?.Count > 0) |
|||
{ |
|||
using (BrushApplicator<TPixel> app = brush.CreateApplicator(source, sourceRectangle, this.textRenderer.Options)) |
|||
{ |
|||
foreach (DrawingOperation operation in operations) |
|||
{ |
|||
Buffer2D<float> 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 maxHeight = source.Height - startY; |
|||
int end = Math.Min(operation.Map.Height, maxHeight); |
|||
|
|||
for (int row = fistRow; row < end; row++) |
|||
{ |
|||
int y = startY + row; |
|||
Span<float> span = buffer.GetRowSpan(row).Slice(offSetSpan); |
|||
app.Apply(span, startX, y); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private struct DrawingOperation |
|||
{ |
|||
public Buffer2D<float> Map { get; set; } |
|||
|
|||
public Point Location { get; set; } |
|||
} |
|||
|
|||
private class CachingGlyphRenderer : IGlyphRenderer, IDisposable |
|||
{ |
|||
// just enough accuracy to allow for 1/8 pixel differences which
|
|||
// later are accumulated while rendering, but do not grow into full pixel offsets
|
|||
// The value 8 is benchmarked to:
|
|||
// - Provide a good accuracy (smaller than 0.2% image difference compared to the non-caching variant)
|
|||
// - Cache hit ratio above 60%
|
|||
private const float AccuracyMultiple = 8; |
|||
|
|||
private readonly PathBuilder builder; |
|||
|
|||
private Point currentRenderPosition = default; |
|||
private (GlyphRendererParameters glyph, PointF subPixelOffset) currentGlyphRenderParams = default; |
|||
private readonly int offset = 0; |
|||
private PointF currentPoint = default(PointF); |
|||
|
|||
private readonly Dictionary<(GlyphRendererParameters glyph, PointF subPixelOffset), GlyphRenderData> |
|||
glyphData = new Dictionary<(GlyphRendererParameters glyph, PointF subPixelOffset), GlyphRenderData>(); |
|||
|
|||
private readonly bool renderOutline = false; |
|||
private readonly bool renderFill = false; |
|||
private bool rasterizationRequired = false; |
|||
|
|||
public CachingGlyphRenderer(MemoryAllocator memoryAllocator, int size, IPen pen, bool renderFill) |
|||
{ |
|||
this.MemoryAllocator = memoryAllocator; |
|||
this.Pen = pen; |
|||
this.renderFill = renderFill; |
|||
this.renderOutline = pen != null; |
|||
this.offset = 2; |
|||
if (this.renderFill) |
|||
{ |
|||
this.FillOperations = new List<DrawingOperation>(size); |
|||
} |
|||
|
|||
if (this.renderOutline) |
|||
{ |
|||
this.offset = (int)MathF.Ceiling((pen.StrokeWidth * 2) + 2); |
|||
this.OutlineOperations = new List<DrawingOperation>(size); |
|||
} |
|||
|
|||
this.builder = new PathBuilder(); |
|||
} |
|||
|
|||
public List<DrawingOperation> FillOperations { get; } |
|||
|
|||
public List<DrawingOperation> OutlineOperations { get; } |
|||
|
|||
public MemoryAllocator MemoryAllocator { 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, GlyphRendererParameters parameters) |
|||
{ |
|||
this.currentRenderPosition = Point.Truncate(bounds.Location); |
|||
PointF subPixelOffset = bounds.Location - this.currentRenderPosition; |
|||
|
|||
subPixelOffset.X = MathF.Round(subPixelOffset.X * AccuracyMultiple) / AccuracyMultiple; |
|||
subPixelOffset.Y = MathF.Round(subPixelOffset.Y * AccuracyMultiple) / AccuracyMultiple; |
|||
|
|||
// 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.currentGlyphRenderParams = (parameters, subPixelOffset); |
|||
|
|||
if (this.glyphData.ContainsKey(this.currentGlyphRenderParams)) |
|||
{ |
|||
// we have already drawn the glyph vectors skip trying again
|
|||
this.rasterizationRequired = 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.rasterizationRequired = 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() |
|||
{ |
|||
foreach (KeyValuePair<(GlyphRendererParameters glyph, PointF subPixelOffset), GlyphRenderData> kv in this.glyphData) |
|||
{ |
|||
kv.Value.Dispose(); |
|||
} |
|||
|
|||
this.glyphData.Clear(); |
|||
} |
|||
|
|||
public void EndFigure() |
|||
{ |
|||
this.builder.CloseFigure(); |
|||
} |
|||
|
|||
public void EndGlyph() |
|||
{ |
|||
GlyphRenderData renderData = default; |
|||
|
|||
// has the glyoh been rendedered already????
|
|||
if (this.rasterizationRequired) |
|||
{ |
|||
IPath path = this.builder.Build(); |
|||
|
|||
if (this.renderFill) |
|||
{ |
|||
renderData.FillMap = 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); |
|||
} |
|||
|
|||
renderData.OutlineMap = this.Render(path); |
|||
} |
|||
|
|||
this.glyphData[this.currentGlyphRenderParams] = renderData; |
|||
} |
|||
else |
|||
{ |
|||
renderData = this.glyphData[this.currentGlyphRenderParams]; |
|||
} |
|||
|
|||
if (this.renderFill) |
|||
{ |
|||
this.FillOperations.Add(new DrawingOperation |
|||
{ |
|||
Location = this.currentRenderPosition, |
|||
Map = renderData.FillMap |
|||
}); |
|||
} |
|||
|
|||
if (this.renderOutline) |
|||
{ |
|||
this.OutlineOperations.Add(new DrawingOperation |
|||
{ |
|||
Location = this.currentRenderPosition, |
|||
Map = renderData.OutlineMap |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private Buffer2D<float> 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<float> fullBuffer = this.MemoryAllocator.Allocate2D<float>(size.Width + 1, size.Height + 1, AllocationOptions.Clean); |
|||
|
|||
using (IMemoryOwner<float> bufferBacking = this.MemoryAllocator.Allocate<float>(path.MaxIntersections)) |
|||
using (IMemoryOwner<PointF> rowIntersectionBuffer = this.MemoryAllocator.Allocate<PointF>(size.Width)) |
|||
{ |
|||
float subpixelFraction = 1f / subpixelCount; |
|||
float subpixelFractionPoint = subpixelFraction / subpixelCount; |
|||
|
|||
for (int y = 0; y <= size.Height; y++) |
|||
{ |
|||
Span<float> 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<PointF> intersectionSpan = rowIntersectionBuffer.GetSpan(); |
|||
Span<float> 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.Sort(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; |
|||
} |
|||
|
|||
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; |
|||
} |
|||
|
|||
private struct GlyphRenderData : IDisposable |
|||
{ |
|||
public Buffer2D<float> FillMap; |
|||
|
|||
public Buffer2D<float> OutlineMap; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
this.FillMap?.Dispose(); |
|||
this.OutlineMap?.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,169 +1,163 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.Fonts; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Options for influencing the drawing functions.
|
|||
/// </summary>
|
|||
public struct TextGraphicsOptions |
|||
{ |
|||
private const int DefaultTextDpi = 72; |
|||
|
|||
/// <summary>
|
|||
/// Represents the default <see cref="TextGraphicsOptions"/>.
|
|||
/// </summary>
|
|||
public static readonly TextGraphicsOptions Default = new TextGraphicsOptions(true); |
|||
|
|||
private float? blendPercentage; |
|||
|
|||
private int? antialiasSubpixelDepth; |
|||
|
|||
private bool? antialias; |
|||
|
|||
private bool? applyKerning; |
|||
|
|||
private float? tabWidth; |
|||
|
|||
private float? dpiX; |
|||
|
|||
private float? dpiY; |
|||
|
|||
private PixelColorBlendingMode colorBlendingMode; |
|||
|
|||
private PixelAlphaCompositionMode alphaCompositionMode; |
|||
|
|||
private float wrapTextWidth; |
|||
|
|||
private HorizontalAlignment? horizontalAlignment; |
|||
|
|||
private VerticalAlignment? verticalAlignment; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TextGraphicsOptions" /> struct.
|
|||
/// </summary>
|
|||
/// <param name="enableAntialiasing">If set to <c>true</c> [enable antialiasing].</param>
|
|||
public TextGraphicsOptions(bool enableAntialiasing) |
|||
{ |
|||
this.applyKerning = true; |
|||
this.tabWidth = 4; |
|||
this.wrapTextWidth = 0; |
|||
this.horizontalAlignment = HorizontalAlignment.Left; |
|||
this.verticalAlignment = VerticalAlignment.Top; |
|||
|
|||
this.antialiasSubpixelDepth = 16; |
|||
this.colorBlendingMode = PixelColorBlendingMode.Normal; |
|||
this.alphaCompositionMode = PixelAlphaCompositionMode.SrcOver; |
|||
this.blendPercentage = 1; |
|||
this.antialias = enableAntialiasing; |
|||
this.dpiX = DefaultTextDpi; |
|||
this.dpiY = DefaultTextDpi; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether antialiasing should be applied.
|
|||
/// </summary>
|
|||
public bool Antialias { get => this.antialias ?? true; set => this.antialias = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled.
|
|||
/// </summary>
|
|||
public int AntialiasSubpixelDepth { get => this.antialiasSubpixelDepth ?? 16; set => this.antialiasSubpixelDepth = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the blending percentage to apply to the drawing operation
|
|||
/// </summary>
|
|||
public float BlendPercentage { get => (this.blendPercentage ?? 1).Clamp(0, 1); set => this.blendPercentage = value; } |
|||
|
|||
// In the future we could expose a PixelBlender<TPixel> directly on here
|
|||
// or some forms of PixelBlender factory for each pixel type. Will need
|
|||
// some API thought post V1.
|
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
|
|||
/// </summary>
|
|||
public PixelColorBlendingMode ColorBlendingMode { get => this.colorBlendingMode; set => this.colorBlendingMode = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
|
|||
/// </summary>
|
|||
public PixelAlphaCompositionMode AlphaCompositionMode { get => this.alphaCompositionMode; set => this.alphaCompositionMode = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the text should be drawing with kerning enabled.
|
|||
/// </summary>
|
|||
public bool ApplyKerning { get => this.applyKerning ?? true; set => this.applyKerning = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the number of space widths a tab should lock to.
|
|||
/// </summary>
|
|||
public float TabWidth { get => this.tabWidth ?? 4; set => this.tabWidth = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating if greater than zero determine the width at which text should wrap.
|
|||
/// </summary>
|
|||
public float WrapTextWidth { get => this.wrapTextWidth; set => this.wrapTextWidth = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the DPI to render text along the X axis.
|
|||
/// </summary>
|
|||
public float DpiX { get => this.dpiX ?? DefaultTextDpi; set => this.dpiX = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the DPI to render text along the Y axis.
|
|||
/// </summary>
|
|||
public float DpiY { get => this.dpiY ?? DefaultTextDpi; set => this.dpiY = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating how to align the text relative to the rendering space.
|
|||
/// If <see cref="WrapTextWidth"/> is greater than zero it will align relative to the space
|
|||
/// defined by the location and width, if <see cref="WrapTextWidth"/> equals zero, and thus
|
|||
/// wrapping disabled, then the alignment is relative to the drawing location.
|
|||
/// </summary>
|
|||
public HorizontalAlignment HorizontalAlignment { get => this.horizontalAlignment ?? HorizontalAlignment.Left; set => this.horizontalAlignment = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating how to align the text relative to the rendering space.
|
|||
/// </summary>
|
|||
public VerticalAlignment VerticalAlignment { get => this.verticalAlignment ?? VerticalAlignment.Top; set => this.verticalAlignment = value; } |
|||
|
|||
/// <summary>
|
|||
/// Performs an implicit conversion from <see cref="GraphicsOptions"/> to <see cref="TextGraphicsOptions"/>.
|
|||
/// </summary>
|
|||
/// <param name="options">The options.</param>
|
|||
/// <returns>
|
|||
/// The result of the conversion.
|
|||
/// </returns>
|
|||
public static implicit operator TextGraphicsOptions(GraphicsOptions options) |
|||
{ |
|||
return new TextGraphicsOptions(options.Antialias) |
|||
{ |
|||
AntialiasSubpixelDepth = options.AntialiasSubpixelDepth, |
|||
blendPercentage = options.BlendPercentage, |
|||
colorBlendingMode = options.ColorBlendingMode, |
|||
alphaCompositionMode = options.AlphaCompositionMode |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Performs an explicit conversion from <see cref="TextGraphicsOptions"/> to <see cref="GraphicsOptions"/>.
|
|||
/// </summary>
|
|||
/// <param name="options">The options.</param>
|
|||
/// <returns>
|
|||
/// The result of the conversion.
|
|||
/// </returns>
|
|||
public static explicit operator GraphicsOptions(TextGraphicsOptions options) |
|||
{ |
|||
return new GraphicsOptions(options.Antialias) |
|||
{ |
|||
AntialiasSubpixelDepth = options.AntialiasSubpixelDepth, |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using SixLabors.Fonts; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Processing |
|||
{ |
|||
/// <summary>
|
|||
/// Options for influencing the drawing functions.
|
|||
/// </summary>
|
|||
public struct TextGraphicsOptions |
|||
{ |
|||
private const int DefaultTextDpi = 72; |
|||
|
|||
/// <summary>
|
|||
/// Represents the default <see cref="TextGraphicsOptions"/>.
|
|||
/// </summary>
|
|||
public static readonly TextGraphicsOptions Default = new TextGraphicsOptions(true); |
|||
|
|||
private float? blendPercentage; |
|||
|
|||
private int? antialiasSubpixelDepth; |
|||
|
|||
private bool? antialias; |
|||
|
|||
private bool? applyKerning; |
|||
|
|||
private float? tabWidth; |
|||
|
|||
private float? dpiX; |
|||
|
|||
private float? dpiY; |
|||
|
|||
private HorizontalAlignment? horizontalAlignment; |
|||
|
|||
private VerticalAlignment? verticalAlignment; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="TextGraphicsOptions" /> struct.
|
|||
/// </summary>
|
|||
/// <param name="enableAntialiasing">If set to <c>true</c> [enable antialiasing].</param>
|
|||
public TextGraphicsOptions(bool enableAntialiasing) |
|||
{ |
|||
this.applyKerning = true; |
|||
this.tabWidth = 4; |
|||
this.WrapTextWidth = 0; |
|||
this.horizontalAlignment = HorizontalAlignment.Left; |
|||
this.verticalAlignment = VerticalAlignment.Top; |
|||
|
|||
this.antialiasSubpixelDepth = 16; |
|||
this.ColorBlendingMode = PixelColorBlendingMode.Normal; |
|||
this.AlphaCompositionMode = PixelAlphaCompositionMode.SrcOver; |
|||
this.blendPercentage = 1; |
|||
this.antialias = enableAntialiasing; |
|||
this.dpiX = DefaultTextDpi; |
|||
this.dpiY = DefaultTextDpi; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether antialiasing should be applied.
|
|||
/// </summary>
|
|||
public bool Antialias { get => this.antialias ?? true; set => this.antialias = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled.
|
|||
/// </summary>
|
|||
public int AntialiasSubpixelDepth { get => this.antialiasSubpixelDepth ?? 16; set => this.antialiasSubpixelDepth = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the blending percentage to apply to the drawing operation
|
|||
/// </summary>
|
|||
public float BlendPercentage { get => (this.blendPercentage ?? 1).Clamp(0, 1); set => this.blendPercentage = value; } |
|||
|
|||
// In the future we could expose a PixelBlender<TPixel> directly on here
|
|||
// or some forms of PixelBlender factory for each pixel type. Will need
|
|||
// some API thought post V1.
|
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
|
|||
/// </summary>
|
|||
public PixelColorBlendingMode ColorBlendingMode { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the color blending percentage to apply to the drawing operation
|
|||
/// </summary>
|
|||
public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating whether the text should be drawing with kerning enabled.
|
|||
/// </summary>
|
|||
public bool ApplyKerning { get => this.applyKerning ?? true; set => this.applyKerning = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the number of space widths a tab should lock to.
|
|||
/// </summary>
|
|||
public float TabWidth { get => this.tabWidth ?? 4; set => this.tabWidth = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating if greater than zero determine the width at which text should wrap.
|
|||
/// </summary>
|
|||
public float WrapTextWidth { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the DPI to render text along the X axis.
|
|||
/// </summary>
|
|||
public float DpiX { get => this.dpiX ?? DefaultTextDpi; set => this.dpiX = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating the DPI to render text along the Y axis.
|
|||
/// </summary>
|
|||
public float DpiY { get => this.dpiY ?? DefaultTextDpi; set => this.dpiY = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating how to align the text relative to the rendering space.
|
|||
/// If <see cref="WrapTextWidth"/> is greater than zero it will align relative to the space
|
|||
/// defined by the location and width, if <see cref="WrapTextWidth"/> equals zero, and thus
|
|||
/// wrapping disabled, then the alignment is relative to the drawing location.
|
|||
/// </summary>
|
|||
public HorizontalAlignment HorizontalAlignment { get => this.horizontalAlignment ?? HorizontalAlignment.Left; set => this.horizontalAlignment = value; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating how to align the text relative to the rendering space.
|
|||
/// </summary>
|
|||
public VerticalAlignment VerticalAlignment { get => this.verticalAlignment ?? VerticalAlignment.Top; set => this.verticalAlignment = value; } |
|||
|
|||
/// <summary>
|
|||
/// Performs an implicit conversion from <see cref="GraphicsOptions"/> to <see cref="TextGraphicsOptions"/>.
|
|||
/// </summary>
|
|||
/// <param name="options">The options.</param>
|
|||
/// <returns>
|
|||
/// The result of the conversion.
|
|||
/// </returns>
|
|||
public static implicit operator TextGraphicsOptions(GraphicsOptions options) |
|||
{ |
|||
return new TextGraphicsOptions(options.Antialias) |
|||
{ |
|||
AntialiasSubpixelDepth = options.AntialiasSubpixelDepth, |
|||
blendPercentage = options.BlendPercentage, |
|||
ColorBlendingMode = options.ColorBlendingMode, |
|||
AlphaCompositionMode = options.AlphaCompositionMode |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Performs an explicit conversion from <see cref="TextGraphicsOptions"/> to <see cref="GraphicsOptions"/>.
|
|||
/// </summary>
|
|||
/// <param name="options">The options.</param>
|
|||
/// <returns>
|
|||
/// The result of the conversion.
|
|||
/// </returns>
|
|||
public static explicit operator GraphicsOptions(TextGraphicsOptions options) |
|||
{ |
|||
return new GraphicsOptions(options.Antialias) |
|||
{ |
|||
AntialiasSubpixelDepth = options.AntialiasSubpixelDepth, |
|||
ColorBlendingMode = options.ColorBlendingMode, |
|||
AlphaCompositionMode = options.AlphaCompositionMode, |
|||
BlendPercentage = options.BlendPercentage |
|||
}; |
|||
} |
|||
} |
|||
AlphaCompositionMode = options.AlphaCompositionMode, |
|||
BlendPercentage = options.BlendPercentage |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue