diff --git a/WpfTex/LatexEditor/Fonts/GlyphInfo.cs b/WpfTex/LatexEditor/Fonts/GlyphInfo.cs index d5bb0d4..b755ac4 100644 --- a/WpfTex/LatexEditor/Fonts/GlyphInfo.cs +++ b/WpfTex/LatexEditor/Fonts/GlyphInfo.cs @@ -1,30 +1,36 @@ using System.Collections.Generic; using System.Windows; using System.Windows.Media; +using JetBrains.Annotations; namespace LatexEditor.Fonts { public class GlyphInfo { public CmFont Font; - public Point Offset; - public double Size; - private int Char; + public double RelativeSize; + public int Char; + public Point RelativeOffset; + public Point BaselineOrigin; public GlyphTypeface Gtf => Font.GlyphTypeface(); - /// - /// Index of this glyph within the GlyphTypeface. - /// - /// Thrown if does not have a glyph for char public ushort Index => Gtf.CharacterToGlyphMap[Char]; - public GlyphInfo(CmFont font, int character, double size, Point offset) + public double RelAdvWidth => Gtf.AdvanceWidths[Index]; + public double RelAdvHeight => Gtf.AdvanceHeights[Index]; + + public Point Offset => new Point( + BaselineOrigin.X + RelativeOffset.X * RelativeSize, + BaselineOrigin.Y + RelativeOffset.Y * RelativeSize); + + public GlyphInfo(CmFont font, int c, Point relativeOffset, Point baselineOrigin, double relativeSize = 1) { Font = font; - Char = character; - Size = size; - Offset = offset; + RelativeSize = relativeSize; + RelativeOffset = relativeOffset; + BaselineOrigin = baselineOrigin; + Char = c; } } } \ No newline at end of file diff --git a/WpfTex/LatexEditor/LatexEditor.csproj b/WpfTex/LatexEditor/LatexEditor.csproj index 6881c74..ab3dc79 100644 --- a/WpfTex/LatexEditor/LatexEditor.csproj +++ b/WpfTex/LatexEditor/LatexEditor.csproj @@ -76,7 +76,13 @@ + + + + + + diff --git a/WpfTex/LatexEditor/LatexViewer.cs b/WpfTex/LatexEditor/LatexViewer.cs index 073fa6f..b6dce6a 100644 --- a/WpfTex/LatexEditor/LatexViewer.cs +++ b/WpfTex/LatexEditor/LatexViewer.cs @@ -41,83 +41,6 @@ namespace LatexEditor set => SetValue(FontSizeProperty, value); } - protected List Parse(string latex) - { - var x = 0d; - var y = 0d; - var font = CmFont.SerifItalic; - var size = FontSize; - - var glyphs = new List(latex.Length); - - var state = new Stack(); - state.Push("-"); - - while (latex.Length > 0) - { - var s = state.Peek(); - - if (s == "cmd") - { - state.Pop(); - var m = Regex.Match(latex, @"([a-z]+)(?=[^a-z]|$)", RegexOptions.IgnoreCase); - if (m.Index == 0) - { - var cmd = latex.Substring(0, m.Groups[1].Length); - switch (cmd) - { - case "it": - font = CmFont.SerifItalic; - break; - case "sym": - font = CmFont.Symbols; - break; - case "rm": - font = CmFont.Serif; - break; - case "sup": - size = FontSize * 0.4; - y = FontSize * (1 - 0.4); - break; - case "sub": - size = FontSize * 0.5; - y = -FontSize * 0.5 * 0.5; - break; - default: - continue; - } - latex = latex.Substring(m.Length).TrimStart(' '); - } - else - { - latex = latex.Substring(1); - } - } - else - { - if (latex.StartsWith("\\")) - { - latex = latex.Substring(1); - - state.Push("cmd"); - } - else - { - var chr = latex[0]; - latex = latex.Substring(1); - - var gtf = font.GlyphTypeface(); - var point = new Point(x, y); - var gi = new GlyphInfo(font, chr, size, point); - x += gtf.AdvanceWidths[gi.Index] * size; - glyphs.Add(gi); - } - } - } - - return glyphs; - } - protected override void OnRender(DrawingContext dc) { Debug.WriteLine(dc, nameof(LatexViewer)); @@ -125,22 +48,23 @@ namespace LatexEditor if (string.IsNullOrEmpty(Content)) return; - var glyphInfoList = LatexParser.Parse(Content); + var glyphInfoList = LatexParser.ToGlyphInfos(Content); var gtfGroups = glyphInfoList.GroupBy(gi => gi.Gtf); foreach (var gtfGroup in gtfGroups) { var gtf = gtfGroup.Key; - var sizeGroups = gtfGroup.GroupBy(gi => gi.Size); + var sizeGroups = gtfGroup.GroupBy(gi => gi.RelativeSize); foreach (var sizeGroup in sizeGroups) { var size = sizeGroup.Key; var glyphs = sizeGroup.Select(gi => gi.Index).ToList(); - var advanceWidths = new double[glyphs.Count]; - var offsets = sizeGroup.Select(gi => gi.Offset).ToList(); - var gr = new GlyphRun(gtf, 0, false, size, + var advanceWidths = new double[glyphs.Count]; // all zero - position based only on offset + var offsets = sizeGroup.Select(gi => new Point(gi.Offset.X * FontSize, gi.Offset.Y * FontSize)) + .ToList(); + var gr = new GlyphRun(gtf, 0, false, size * FontSize, glyphs, new Point(0, FontSize), advanceWidths, offsets, null, null, null, null, null); diff --git a/WpfTex/LatexEditor/MainWindow.xaml b/WpfTex/LatexEditor/MainWindow.xaml index 31004be..f570ad9 100644 --- a/WpfTex/LatexEditor/MainWindow.xaml +++ b/WpfTex/LatexEditor/MainWindow.xaml @@ -8,20 +8,20 @@ Title="MainWindow" Height="350" Width="525"> - - A \ \alpha \quad N \ \nu \\ - B \ \beta \quad \Xi \ \xi \\ - \Gamma \ \gamma \quad O \ \omicron \\ - \Delta \ \delta \quad \Pi \ \pi \\ - E \ \epsilon \quad P \ \rho \\ - Z \ \zeta \quad \Sigma \ \sigma \\ - H \ \eta \quad T \ \tau \\ - \Theta \ \theta \quad Y \ \upsilon \\ - I \ \iota \quad \Phi \ \phi \\ - K \ \kappa \quad X \ \chi \\ - \Lambda \ \lambda \quad \Psi \ \psi \\ - M \ \mu \quad \Omega \ \omega - + + {2}{123} + + + + + + + + + + + + diff --git a/WpfTex/LatexEditor/Parser/LatexNull.cs b/WpfTex/LatexEditor/Parser/LatexNull.cs new file mode 100644 index 0000000..45b88c8 --- /dev/null +++ b/WpfTex/LatexEditor/Parser/LatexNull.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using System.Linq; +using LatexEditor.Fonts; + +namespace LatexEditor.Parser +{ + public class LatexNull : LatexSegment + { + public override IEnumerable Glyphs => Enumerable.Empty(); + public override double RelAdvWidth => 0; + public override double RelAdvHeight => 0; + } +} \ No newline at end of file diff --git a/WpfTex/LatexEditor/Parser/LatexParser.cs b/WpfTex/LatexEditor/Parser/LatexParser.cs index 8121c64..ab0a044 100644 --- a/WpfTex/LatexEditor/Parser/LatexParser.cs +++ b/WpfTex/LatexEditor/Parser/LatexParser.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; -using System.Windows; +using System.Runtime.CompilerServices; +using System.Windows.Navigation; using LatexEditor.Fonts; namespace LatexEditor.Parser @@ -18,7 +19,7 @@ namespace LatexEditor.Parser new TokenDescriptor("letter", @"[\S]"), // nearly catch-all for uncaptured symbols }; - private static readonly Dictionary greekLetters = new Dictionary + internal static readonly Dictionary GreekLetters = new Dictionary { ["alpha"] = 0x03b1, ["beta"] = 0x03b2, @@ -45,19 +46,22 @@ namespace LatexEditor.Parser ["psi"] = 0x03c8, ["omega"] = 0x03c9, - ["Gamma"] = 0x0393, - ["Delta"] = 0x0394, - ["Theta"] = 0x0398, - ["Lambda"] = 0x039b, - ["Xi"] = 0x039e, - ["Pi"] = 0x03a0, - ["Sigma"] = 0x03a3, - ["Phi"] = 0x03a6, - ["Psi"] = 0x03a8, - ["Omega"] = 0x03a9, + // Alpha Beta Epsilon Zeta Eta Iota Kappa Mu Nu + // Omicron Rho Tau and Upsilon all denoted by + // Latin symbols A B E Z H I K M N O P T and Y + ["Gamma"] = 0x0393, + ["Delta"] = 0x0394, + ["Theta"] = 0x0398, + ["Lambda"] = 0x039b, + ["Xi"] = 0x039e, + ["Pi"] = 0x03a0, + ["Sigma"] = 0x03a3, + ["Phi"] = 0x03a6, + ["Psi"] = 0x03a8, + ["Omega"] = 0x03a9, }; - private static readonly Dictionary spaces = new Dictionary + internal static readonly Dictionary Spaces = new Dictionary { [","] = 0.16666, ["!"] = -0.16666, @@ -69,57 +73,12 @@ namespace LatexEditor.Parser ["qquad"] = 4, }; - public static IEnumerable Parse(string latex) => Parse(LatexLexer.Tokenize(latex)); + public static IEnumerable ToGlyphInfos(string latex) => ToGlyphInfos(LatexLexer.Tokenize(latex)); - private static IEnumerable Parse(IEnumerable tokens) + private static IEnumerable ToGlyphInfos(IEnumerable tokens) { - var tokenQ = new Queue(tokens); - var glyphs = new List(); - - const double size = 20; - var x = 0d; - var y = 0d; - - void AddGlyph(CmFont font, int c) - { - glyphs.Add(new GlyphInfo(font, c, size, new Point(x * size, y * size))); - var gtf = font.GlyphTypeface(); - var idx = gtf.CharacterToGlyphMap[c]; - x += gtf.AdvanceWidths[idx]; - } - - while (tokenQ.Count > 0) - { - var token = tokenQ.Dequeue(); - var val = token.Value; - switch (token.TokenName) - { - case "letter": - AddGlyph(CmFont.SerifItalic, val[0]); - break; - case "number": - AddGlyph(CmFont.Serif, val[0]); - break; - case "command": - if (greekLetters.ContainsKey(val)) - AddGlyph(CmFont.SerifItalic, greekLetters[val]); - else if (spaces.ContainsKey(val)) - x += spaces[val]; - else - foreach (var c in token.CapturedString) - AddGlyph(CmFont.SerifItalic, c); - break; - case "escape": - if (val == "\\") - { - x = 0; - y -= 1; - } - break; - } - } - - return glyphs; + var lss = LatexSegment.ToLatexSegment(tokens); + return lss.Glyphs; } } } \ No newline at end of file diff --git a/WpfTex/LatexEditor/Parser/LatexReturn.cs b/WpfTex/LatexEditor/Parser/LatexReturn.cs new file mode 100644 index 0000000..bfa39ce --- /dev/null +++ b/WpfTex/LatexEditor/Parser/LatexReturn.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using LatexEditor.Fonts; + +namespace LatexEditor.Parser +{ + public class LatexReturn : LatexSegment + { + public override IEnumerable Glyphs => Enumerable.Empty(); + public override double RelAdvWidth => 0; + public override double RelAdvHeight { get; } + + public LatexReturn(double relAdvHeight) + { + RelAdvHeight = relAdvHeight; + } + } +} \ No newline at end of file diff --git a/WpfTex/LatexEditor/Parser/LatexRun.cs b/WpfTex/LatexEditor/Parser/LatexRun.cs new file mode 100644 index 0000000..06b56ee --- /dev/null +++ b/WpfTex/LatexEditor/Parser/LatexRun.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using LatexEditor.Fonts; + +namespace LatexEditor.Parser +{ + public class LatexRun : LatexSegment + { + public override IEnumerable Glyphs => Segments.SelectMany(ls => ls.Glyphs); + + public override double RelAdvWidth { get; } + public override double RelAdvHeight { get; } + + public IEnumerable Segments { get; set; } + + public LatexRun(IEnumerable segments) + { + Segments = segments; + + // to shift characters into position + var x = 0d; + var y = 0d; + foreach (var segment in Segments) + { + if (segment is LatexReturn) + { + x = 0d; + y -= segment.RelAdvHeight; + continue; + } + foreach (var gi in segment.Glyphs) + { + gi.BaselineOrigin.X = x; + gi.BaselineOrigin.Y = y; + } + x += segment.RelAdvWidth; + } + RelAdvWidth = x; + } + } +} \ No newline at end of file diff --git a/WpfTex/LatexEditor/Parser/LatexSegment.cs b/WpfTex/LatexEditor/Parser/LatexSegment.cs new file mode 100644 index 0000000..6a8fd3e --- /dev/null +++ b/WpfTex/LatexEditor/Parser/LatexSegment.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Diagnostics; +using JetBrains.Annotations; +using LatexEditor.Fonts; + +namespace LatexEditor.Parser +{ + public abstract class LatexSegment + { + [NotNull] + public abstract IEnumerable Glyphs { get; } + + public static LatexSegment ToLatexSegment(IEnumerable tokens) + { + var tokenQ = new Queue(tokens); + var segments = new List(); + while (tokenQ.Count > 0) + if (PopLatexSegment(tokenQ, out var ls)) + segments.Add(ls); + + if (segments.Count == 0) + return new LatexNull(); + if (segments.Count == 1) + return segments[0]; + return new LatexRun(segments); + } + + public abstract double RelAdvWidth { get; } + public abstract double RelAdvHeight { get; } + + private static bool PopLatexSegment(Queue tokens, out LatexSegment val) + { + val = null; + + if (tokens.Count == 0) + return false; + + // todo: tabulate this + + var head = tokens.Dequeue(); + if (head.TokenName == "letter") + val = new LatexText(CmFont.SerifItalic, head.Value); + if (head.TokenName == "command") + { + if (LatexParser.GreekLetters.ContainsKey(head.Value)) + val = new LatexText(CmFont.SerifItalic, LatexParser.GreekLetters[head.Value]); + if (LatexParser.Spaces.ContainsKey(head.Value)) + val = new LatexSpace(LatexParser.Spaces[head.Value]); + if (head.Value == "^") + if (PopLatexSegment(tokens, out var content)) + { + val = new LatexSuper(content); + } + else // to prevent possible nullref, but still incorrect. + { + val = null; + return false; + } + if (val == null) // if no command matches + val = new LatexText(CmFont.SerifItalic, head.CapturedString); + } + if (head.TokenName == "number") + val = new LatexText(CmFont.Serif, head.Value); + if (head.TokenName == "escape") + if (head.Value == "\\") + val = new LatexReturn(1); + if (head.TokenName == "open") + { + var segments = new List(); + while (tokens.Peek().TokenName != "close") + if (PopLatexSegment(tokens, out var content)) + segments.Add(content); + tokens.Dequeue(); // toss the close + val = new LatexRun(segments); + } + + return val != null || PopLatexSegment(tokens, out val); + } + } + + public class LatexSuper : LatexSegment + { + public override IEnumerable Glyphs => Content.Glyphs; + + public override double RelAdvWidth => Content.RelAdvWidth; + public override double RelAdvHeight => Content.RelAdvHeight; + + public LatexSegment Content { get; set; } + + public LatexSuper(LatexSegment content) + { + Content = content; + + foreach (var gi in Content.Glyphs) + { + gi.RelativeOffset.Y += 1; + gi.RelativeSize *= 0.5; + } + } + } +} \ No newline at end of file diff --git a/WpfTex/LatexEditor/Parser/LatexSpace.cs b/WpfTex/LatexEditor/Parser/LatexSpace.cs new file mode 100644 index 0000000..03275bf --- /dev/null +++ b/WpfTex/LatexEditor/Parser/LatexSpace.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using LatexEditor.Fonts; + +namespace LatexEditor.Parser +{ + public class LatexSpace : LatexSegment + { + public override IEnumerable Glyphs => Enumerable.Empty(); + public override double RelAdvWidth { get; } + public override double RelAdvHeight { get; } + + public LatexSpace(double relAdvWidth) + { + RelAdvWidth = relAdvWidth; + RelAdvHeight = 0; + } + } +} \ No newline at end of file diff --git a/WpfTex/LatexEditor/Parser/LatexText.cs b/WpfTex/LatexEditor/Parser/LatexText.cs new file mode 100644 index 0000000..cf4f883 --- /dev/null +++ b/WpfTex/LatexEditor/Parser/LatexText.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Windows; +using LatexEditor.Fonts; + +namespace LatexEditor.Parser +{ + public class LatexText : LatexSegment + { + private readonly List _glyphs = new List(); + + public override IEnumerable Glyphs => _glyphs; + public override double RelAdvWidth { get; } + public override double RelAdvHeight { get; } + + public LatexText(CmFont cmFont, int c) + { + var gi = new GlyphInfo(cmFont, c, new Point(0, 0), new Point(0, 0)); + _glyphs.Add(gi); + RelAdvWidth = gi.RelAdvWidth; + RelAdvHeight = gi.RelAdvHeight; + } + + public LatexText(CmFont cmFont, string text) + { + var x = 0d; + foreach (var c in text) + { + var gi = new GlyphInfo(cmFont, c, new Point(x, 0), new Point(0, 0)); + _glyphs.Add(gi); + x += gi.RelAdvWidth; + } + RelAdvWidth = x; + } + } +} \ No newline at end of file