diff --git a/README.md b/README.md index aca6006..309ada3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Paint.NET-Plugins Plugins for Paint .NET -##Spaced text +## Spaced text Renders grid-fit anti-aliased text with variable character and line spacing. Works with or without a selection area. Features: diff --git a/SpacedText/Constants.cs b/SpacedText/Data/Constants.cs similarity index 87% rename from SpacedText/Constants.cs rename to SpacedText/Data/Constants.cs index 8917c85..a023cc9 100644 --- a/SpacedText/Constants.cs +++ b/SpacedText/Data/Constants.cs @@ -1,13 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SpacedTextPlugin +namespace SpacedTextPlugin.Data { - using System.Diagnostics.PerformanceData; - public class Constants { public enum Properties diff --git a/SpacedText/Data/Extent.cs b/SpacedText/Data/Extent.cs new file mode 100644 index 0000000..10f47a6 --- /dev/null +++ b/SpacedText/Data/Extent.cs @@ -0,0 +1,34 @@ +namespace SpacedTextPlugin.Data +{ + using System.Drawing; + + public struct Extent + { + public int VerticalPosition { get; private set; } + public int Left { get; private set; } + public int Right { get; private set; } + public int Width { get; private set; } + + public Point Start => new Point(Left, VerticalPosition); + + public Point End => new Point(Right, VerticalPosition); + + public Extent(int left, int right, int y) + { + Left = left; + Right = right; + Width = right - left; + VerticalPosition = y; + } + + public Extent Multiply(int factor) + { + Left *= factor; + Right *= factor; + Width = Right - Left; + VerticalPosition *= factor; + + return this; + } + } +} diff --git a/SpacedText/Data/LineData.cs b/SpacedText/Data/LineData.cs new file mode 100644 index 0000000..18be7a8 --- /dev/null +++ b/SpacedText/Data/LineData.cs @@ -0,0 +1,11 @@ +namespace SpacedTextPlugin.Data +{ + using System.Drawing; + + public class LineData + { + public string Text { get; set; } + public Rectangle LineBounds { get; set; } + public Size TextSize { get; set; } + } +} diff --git a/SpacedText/Data/Settings.cs b/SpacedText/Data/Settings.cs new file mode 100644 index 0000000..752eefc --- /dev/null +++ b/SpacedText/Data/Settings.cs @@ -0,0 +1,26 @@ +namespace SpacedTextPlugin.Data +{ + using System.Drawing; + + class Settings + { + public string Text { get; set; } + public FontFamily FontFamily { get; set; } + public int FontSize { get; set; } + public double LetterSpacing { get; set; } + public double LineSpacing { get; set; } + public int AntiAliasLevel { get; set; } + public FontStyle FontStyle { get; set; } + public Constants.TextAlignmentOptions TextAlign { get; set; } + + public Font GetFont() + { + return new Font(FontFamily, FontSize, FontStyle, GraphicsUnit.Pixel); + } + + public Font GetAntiAliasSizeFont() + { + return new Font(FontFamily, FontSize * AntiAliasLevel, FontStyle, GraphicsUnit.Pixel); + } + } +} diff --git a/SpacedText/ExtensionMethods.cs b/SpacedText/ExtensionMethods.cs new file mode 100644 index 0000000..d1599c1 --- /dev/null +++ b/SpacedText/ExtensionMethods.cs @@ -0,0 +1,23 @@ +namespace SpacedTextPlugin +{ + using System.Drawing; + + public static class ExtensionMethods + { + public static Rectangle Multiply(this Rectangle r, int factor) + { + return new Rectangle( + r.X * factor, + r.Y * factor, + r.Width * factor, + r.Height * factor); + } + + public static Size Multiply(this Size s, int factor) + { + return new Size( + s.Width * factor, + s.Height * factor); + } + } +} diff --git a/SpacedText/Interop.cs b/SpacedText/Interop/Interop.cs similarity index 86% rename from SpacedText/Interop.cs rename to SpacedText/Interop/Interop.cs index 32fdb95..c1dd28d 100644 --- a/SpacedText/Interop.cs +++ b/SpacedText/Interop/Interop.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SpacedTextPlugin +namespace SpacedTextPlugin.Interop { + using System; using System.Drawing; using System.Runtime.InteropServices; - internal class Interop + internal static class Interop { [DllImport("gdi32.dll", CharSet = CharSet.Unicode)] public static extern int SetTextCharacterExtra(IntPtr hdc, int nCharExtra); diff --git a/SpacedText/PInvoked.cs b/SpacedText/Interop/PInvoked.cs similarity index 90% rename from SpacedText/PInvoked.cs rename to SpacedText/Interop/PInvoked.cs index f12f51f..a167a36 100644 --- a/SpacedText/PInvoked.cs +++ b/SpacedText/Interop/PInvoked.cs @@ -1,20 +1,19 @@ -namespace SpacedTextPlugin +namespace SpacedTextPlugin.Interop { using System; using System.Drawing; using System.Drawing.Drawing2D; - public class PInvoked + internal static class PInvoked { public static void TextOut(Graphics g, string text, int x, int y, Font font, double letterSpacing) { g.PixelOffsetMode = PixelOffsetMode.Half; - IntPtr hdc = default(IntPtr); IntPtr fontPtr = default(IntPtr); try { //Grab the Graphic object's handle - hdc = g.GetHdc(); + IntPtr hdc = g.GetHdc(); //Set the current GDI font fontPtr = Interop.SelectObject(hdc, font.ToHfont()); //Set the drawing surface background color @@ -36,13 +35,12 @@ public static void TextOut(Graphics g, string text, int x, int y, Font font, dou public static Size MeasureString(Graphics g, string text, Font font, double letterSpacing) { - IntPtr hdc = default(IntPtr); IntPtr fontPtr = default(IntPtr); Size size = new Size(); try { //Grab the Graphic object's handle - hdc = g.GetHdc(); + IntPtr hdc = g.GetHdc(); //Set the current GDI font fontPtr = Interop.SelectObject(hdc, font.ToHfont()); //Set the kerning diff --git a/SpacedText/Renderer.cs b/SpacedText/Renderer.cs new file mode 100644 index 0000000..4e3638f --- /dev/null +++ b/SpacedText/Renderer.cs @@ -0,0 +1,138 @@ +namespace SpacedTextPlugin +{ + using System; + using System.Collections.Generic; + using System.Drawing; + using System.Drawing.Drawing2D; + using System.Drawing.Imaging; + using System.Drawing.Text; + using PaintDotNet; + using SpacedTextPlugin.Data; + using SpacedTextPlugin.Interop; + + internal class Renderer : IDisposable + { + private readonly Rectangle selectionBounds; + private readonly Font font; + private readonly Bitmap image; + private readonly Graphics graphics; + private readonly ImageAttributes imageAttributes; + + private readonly Settings settings; + + public Renderer(Settings settings, PdnRegion selectionRegion) + { + //convert to AA space + selectionBounds = selectionRegion.GetBoundsInt(); + var scaledBounds = selectionBounds.Multiply(settings.AntiAliasLevel); + font = settings.GetAntiAliasSizeFont(); + image = new Bitmap(scaledBounds.Width, scaledBounds.Height); + graphics = Graphics.FromImage(image); + graphics.Clear(Color.Black); + graphics.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; + + this.settings = settings; + + //map color black to transparent + ColorMap[] colorMap = { + new ColorMap + { + OldColor = Color.Black, + NewColor = Color.Transparent + } + }; + imageAttributes = new ImageAttributes(); + imageAttributes.SetRemapTable(colorMap); + } + + public Bitmap Draw(IEnumerable lines) + { + foreach (var line in lines) + { + //create bitmap for line + using (var lineImage = new Bitmap(line.TextSize.Width, line.TextSize.Height)) + { + var lineGraphics = Graphics.FromImage(lineImage); + + if (settings.TextAlign != Constants.TextAlignmentOptions.Justify) + { + PInvoked.TextOut(lineGraphics, line.Text, 0, 0, font, settings.LetterSpacing); + } + else + { + Justify(line, lineGraphics); + } + + //draw line bitmap to image + graphics.DrawImage(lineImage, + new Rectangle( + line.LineBounds.Location, + lineImage.Size + ), /* destination rect */ + 0, 0, /* source coordinates */ + lineImage.Width, + lineImage.Height, + GraphicsUnit.Pixel, + imageAttributes + ); + +#if DEBUG + //draw rectangles + graphics.DrawRectangle(Pens.White, line.LineBounds); + graphics.DrawLine(Pens.Gray, + line.LineBounds.X, + line.LineBounds.Y + line.TextSize.Height / 2, + line.LineBounds.X + line.TextSize.Width, + line.LineBounds.Y + line.TextSize.Height / 2 + ); +#endif + lineGraphics.Dispose(); + } + } + + //create selection-sized bitmap + var resultImage = new Bitmap(selectionBounds.Width, selectionBounds.Height); + var resultGraphics = Graphics.FromImage(resultImage); + resultGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + resultGraphics.DrawImage(image, 0, 0, selectionBounds.Width, selectionBounds.Height); + + return resultImage; + } + + private void Justify(LineData line, Graphics lineGraphics) + { + var lineTextWithoutSpaces = line.Text.Replace(Constants.Space, string.Empty); + var lineSizeWithoutSpaces = PInvoked.MeasureString(lineGraphics, lineTextWithoutSpaces, font, + settings.LetterSpacing); + var spaceWidth = (line.TextSize.Width - lineSizeWithoutSpaces.Width) / + Math.Max((line.Text.Length - lineTextWithoutSpaces.Length), 1); + if (spaceWidth > font.Size * 3) + { + PInvoked.TextOut(lineGraphics, line.Text, 0, 0, font, settings.LetterSpacing); + } + else + { + var x = 0; + + foreach (string word in line.Text.Split(Constants.SpaceChar)) + { + var wordSize = PInvoked.MeasureString(lineGraphics, word, font, settings.LetterSpacing); + PInvoked.TextOut(lineGraphics, word, x, 0, font, settings.LetterSpacing); + x += wordSize.Width + spaceWidth; + } + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + graphics.Dispose(); + image.Dispose(); + } + } +} diff --git a/SpacedText/SpacedText.cs b/SpacedText/SpacedText.cs index 1ade63f..b1af9db 100644 --- a/SpacedText/SpacedText.cs +++ b/SpacedText/SpacedText.cs @@ -1,232 +1,56 @@ namespace SpacedTextPlugin { using System; - using System.Collections.Generic; using System.Drawing; - using System.Drawing.Drawing2D; using System.Drawing.Imaging; - using System.Drawing.Text; - using System.Linq; using PaintDotNet; - using C = Constants; + using SpacedTextPlugin.Data; internal class SpacedText : IDisposable { - //configuration options - public string Text { get; set; } - public FontFamily FontFamily { get; set; } - public int FontSize { get; set; } - public double LetterSpacing { get; set; } - public double LineSpacing { get; set; } - public int AntiAliasLevel { get; set; } - public FontStyle FontStyle { get; set; } - public C.TextAlignmentOptions TextAlign { get; set; } - //flow control public bool IsCancelRequested { get; set; } //public result public Rectangle Bounds { get; private set; } public Surface BufferSurface { get; private set; } - - //private - private readonly ImageAttributes imgAttr; - - public SpacedText() + + public void RenderText(PdnRegion selection, Settings settings) { - AntiAliasLevel = C.DefaultAntiAliasingLevel; - - ColorMap[] colorMap = { - new ColorMap - { - OldColor = Color.Black, - NewColor = Color.Transparent - } - }; - imgAttr = new ImageAttributes(); - imgAttr.SetRemapTable(colorMap); - } + var selectionRegion = selection; + Bounds = selectionRegion.GetBoundsInt(); - public void RenderText(Rectangle bounds) - { try { - Bounds = bounds; - Font font = new Font(FontFamily, FontSize*AntiAliasLevel, FontStyle, GraphicsUnit.Pixel); - - //render text on larger bitmap so it can be anti-aliased while scaling down - Bitmap bm = new Bitmap(Bounds.Size.Width*AntiAliasLevel, Bounds.Size.Height*AntiAliasLevel); - Graphics gr = Graphics.FromImage(bm); - gr.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; - gr.SmoothingMode = SmoothingMode.HighQuality; - gr.CompositingMode = CompositingMode.SourceOver; - gr.Clear(Color.Black); + TypeSetter setter = new TypeSetter(settings, selectionRegion); + setter.SetText(); + setter.AlignText(); - //letterspacing may be changed during execution - double letterSpacing = LetterSpacing; + Renderer renderer = new Renderer(settings, selectionRegion); + Bitmap resultImage = renderer.Draw(setter.Lines); +#if DEBUG + resultImage.Save("C:\\dev\\redo_bm.png", ImageFormat.Png); +#endif - if (!IsCancelRequested) - { - //split in lines - List lines = LineWrap(gr, font, letterSpacing, bm); - - if (!IsCancelRequested) - { - //draw lines - DrawLines(lines, gr, font, letterSpacing, bm); - } - } - - //scale bitmap down onto result-size bitmap and apply anti-aliasing - Bitmap resultBm = new Bitmap(Bounds.Width, Bounds.Height); - Graphics resultGr = Graphics.FromImage(resultBm); - resultGr.InterpolationMode = InterpolationMode.HighQualityBicubic; - resultGr.DrawImage(bm, 0f, 0f, Bounds.Width, Bounds.Height); - BufferSurface = Surface.CopyFromBitmap(resultBm); - - //cleanup - gr.Dispose(); - bm.Dispose(); - resultGr.Dispose(); - resultBm.Dispose(); + BufferSurface = Surface.CopyFromBitmap(resultImage); } catch (OutOfMemoryException) { //scale back anti-aliasing - if (AntiAliasLevel > 1) + if (settings.AntiAliasLevel > 1) { - AntiAliasLevel--; + settings.AntiAliasLevel--; } } } - private void DrawLines(List lines, Graphics gr, Font font, double letterSpacing, Bitmap bm) - { - int lineStart = 0; - foreach (string line in lines) - { - if (IsCancelRequested || lineStart > Bounds.Bottom * AntiAliasLevel) - { - break; - } - - if (!string.IsNullOrWhiteSpace(line)) - { - int left = FontSize / 2; - - if (TextAlign != C.TextAlignmentOptions.Justify) - { - //measure text - Size textBounds = PInvoked.MeasureString(gr, line, font, letterSpacing); - if (TextAlign == C.TextAlignmentOptions.Center) - { - left = bm.Width / 2 - textBounds.Width / 2; - } - else if (TextAlign == C.TextAlignmentOptions.Right) - { - left = bm.Width - (textBounds.Width + FontSize); - } - - if (textBounds.Width > 0 && textBounds.Height > 0 && - textBounds.Width * AntiAliasLevel < C.MaxBitmapSize && - textBounds.Height * AntiAliasLevel < C.MaxBitmapSize) - { - //create new bitmap for line - Bitmap lineBm = new Bitmap(textBounds.Width * AntiAliasLevel, - textBounds.Height * AntiAliasLevel); - Graphics lineGr = Graphics.FromImage(lineBm); - //draw text - PInvoked.TextOut(lineGr, line, 0, 0, font, letterSpacing); - - //draw lineBm to bm leaving out black - gr.DrawImage(lineBm, new Rectangle(new Point(left, lineStart), lineBm.Size), 0, 0, - lineBm.Width, - lineBm.Height, GraphicsUnit.Pixel, imgAttr); - lineGr.Dispose(); - lineBm.Dispose(); - } - } - else - { - //measure text without spaces - string lineWithoutSpaces = line.Replace(" ", string.Empty); - Size textBounds = PInvoked.MeasureString(gr, lineWithoutSpaces, font, letterSpacing); - - //calculate width of spaces - int spaceWidth = FontSize; - if (textBounds.Width > bm.Width / 2) - { - spaceWidth = (bm.Width - textBounds.Width - FontSize) / - Math.Max(line.Length - lineWithoutSpaces.Length, 1); - } - - //create new bitmap for line - Bitmap lineBm = new Bitmap(bm.Width, bm.Height); - Graphics lineGr = Graphics.FromImage(lineBm); - - //draw word by word with correct space in between.7 - foreach (string word in line.Split(' ')) - { - //draw text - PInvoked.TextOut(lineGr, word, left, 0, font, letterSpacing); - - Size wordBounds = PInvoked.MeasureString(lineGr, word, font, letterSpacing); - left += wordBounds.Width + spaceWidth; - } - - //draw lineBm to bm leaving out black - gr.DrawImage(lineBm, new Rectangle(new Point(0, lineStart), lineBm.Size), 0, 0, - lineBm.Width, - lineBm.Height, GraphicsUnit.Pixel, imgAttr); - lineGr.Dispose(); - lineBm.Dispose(); - } - } - - lineStart += font.Height + (int)Math.Round(font.Height * LineSpacing); - } - } - - private List LineWrap(Graphics gr, Font font, double letterSpacing, Bitmap bm) + public void Dispose() { - string[] words = Text.Replace(Environment.NewLine, " " + Environment.NewLine + C.Space) - .Split(new[] {C.SpaceChar}, StringSplitOptions.RemoveEmptyEntries); - List lines = new List(); - - string currentLine = words.Any() ? words.First() + C.Space : string.Empty; - - foreach (string word in words.Skip(1)) - { - //if manual line break: end current line and start new line - if (word.Equals(Environment.NewLine)) - { - lines.Add(currentLine.Trim()); - currentLine = string.Empty; - continue; - } - - //measure currentline + word - //else add word to currentline - if (PInvoked.MeasureString(gr, currentLine + word, font, letterSpacing).Width > bm.Width - FontSize) - { - //if outside bounds, then add line - lines.Add(currentLine.Trim()); - currentLine = word + C.Space; - } - else - { - currentLine += word + C.Space; - } - } - //add currentline - if (!string.IsNullOrEmpty(currentLine)) - { - lines.Add(currentLine.Trim()); - } - return lines; + Dispose(true); + GC.SuppressFinalize(this); } - public void Dispose() + protected virtual void Dispose(bool disposing) { BufferSurface.Dispose(); } diff --git a/SpacedText/SpacedText.csproj b/SpacedText/SpacedText.csproj index 4b11761..b7c1f12 100644 --- a/SpacedText/SpacedText.csproj +++ b/SpacedText/SpacedText.csproj @@ -65,12 +65,18 @@ - - - + + + + + + + + +