/// <summary> /// Renders <paramref name="message"/> using this font. /// </summary> /// <param name="message">The text to render.</param> /// <param name="smushOverride">Optional override for the smush settings. Defaults to <c>null</c>, meaning the font's default setting is used.</param> /// <returns></returns> public string Render(string message, int?smushOverride = null) { var smush = smushOverride ?? _smushMode; var outputLines = Enumerable.Range(0, Height).Select(_ => new StringBuilder()).ToList(); FiggleCharacter lastCh = null; foreach (var c in message) { var ch = GetCharacter(c); if (ch == null) { continue; } var fitMove = CalculateFitMove(lastCh, ch); for (var row = 0; row < Height; row++) { var charLine = ch.Lines[row]; var outputLine = outputLines[row]; if (fitMove != 0) { var toMove = fitMove; if (lastCh != null) { var lineSpace = lastCh.Lines[row].SpaceAfter; if (lineSpace != 0) { var lineSpaceTrim = Math.Min(lineSpace, toMove); toMove -= lineSpaceTrim; outputLine.Length -= lineSpaceTrim; } } var smushCharIndex = outputLine.Length - 1; var cl = outputLine[smushCharIndex]; outputLine.Append(toMove == 0 ? charLine.Content : charLine.Content.Substring(toMove)); if (toMove != 0 && outputLine.Length != 0 && ch.Lines[row].Content.Length != 0) { var cr = ch.Lines[row].Content[toMove - 1]; var sc = TrySmush(cl, cr); if (sc != '\0' && smushCharIndex >= 0) { outputLine[smushCharIndex] = sc; } } } else { outputLine.Append(charLine.Content); } } lastCh = ch; } var res = new StringBuilder(); foreach (var outputLine in outputLines) { res.AppendLine(outputLine.Replace(_hardBlank, ' ').ToString()); } return(res.ToString()); int CalculateFitMove(FiggleCharacter l, FiggleCharacter r) { if (smush == SM_FULLWIDTH) { return(0); } if (l == null) { return(0); // TODO could still shift b if it had whitespace in the first column } var minMove = int.MaxValue; for (var row = 0; row < Height; row++) { var ll = l.Lines[row]; var rl = r.Lines[row]; var move = ll.SpaceAfter + rl.SpaceBefore; if (TrySmush(ll.BackChar, rl.FrontChar) != '\0') { move++; } if (move < minMove) { minMove = move; } } Debug.Assert(minMove >= 0, "minMove >= 0"); return(minMove); } // TODO disallow smushing if either char's line has a length < 2 char TrySmush(char l, char r) { if (l == ' ') { return(r); } if (r == ' ') { return(l); } // kerning if ((_smushMode & SM_SMUSH) == 0) { return('\0'); } // universal smushing if ((_smushMode & 0b00111111) == 0) { // prefer visible character in case of hard blanks if (l == _hardBlank) { return(r); } if (r == _hardBlank) { return(l); } // prefer overlapping character depending upon text direction return(Direction == FiggleTextDirection.LeftToRight ? r : l); } if ((_smushMode & SM_HARDBLANK) != 0 && l == _hardBlank && r == _hardBlank) { return(l); } if (l == _hardBlank && r == _hardBlank) { return('\0'); } if ((_smushMode & SM_EQUAL) != 0 && l == r) { return(l); } if ((_smushMode & SM_LOWLINE) != 0) { const string lowLineChars = @"|/\[]{}()<>"; if (l == '_' && lowLineChars.Contains(r)) { return(r); } if (r == '_' && lowLineChars.Contains(l)) { return(l); } } if ((_smushMode & SM_HIERARCHY) != 0) { if (l == '|' && @"/\[]{}()<>".Contains(r)) { return(r); } if (r == '|' && @"/\[]{}()<>".Contains(l)) { return(l); } if ("/\\".Contains(l) && "[]{}()<>".Contains(r)) { return(r); } if ("/\\".Contains(r) && "[]{}()<>".Contains(l)) { return(l); } if ("[]".Contains(l) && "{}()<>".Contains(r)) { return(r); } if ("[]".Contains(r) && "{}()<>".Contains(l)) { return(l); } if ("{}".Contains(l) && "()<>".Contains(r)) { return(r); } if ("{}".Contains(r) && "()<>".Contains(l)) { return(l); } if ("()".Contains(l) && "<>".Contains(r)) { return(r); } if ("()".Contains(r) && "<>".Contains(l)) { return(l); } } if ((_smushMode & SM_PAIR) != 0) { if (l == '[' && r == ']') { return('|'); } if (r == '[' && l == ']') { return('|'); } if (l == '{' && r == '}') { return('|'); } if (r == '{' && l == '}') { return('|'); } if (l == '(' && r == ')') { return('|'); } if (r == '(' && l == ')') { return('|'); } } if ((_smushMode & SM_BIGX) != 0) { if (l == '/' && r == '\\') { return('|'); } if (r == '/' && l == '\\') { return('Y'); } if (l == '>' && r == '<') { return('X'); } } return('\0'); } }
/// <summary> /// Parses a FIGlet font description stream, and returns a usable <see cref="FiggleFont"/>. /// </summary> /// <param name="stream">The stream to read from.</param> /// <param name="pool">An optional string pool for merging identical string references.</param> /// <returns>The font described by the stream.</returns> /// <exception cref="ArgumentNullException"><paramref name="stream"/> is <c>null</c>.</exception> /// <exception cref="FiggleException">The stream contained an error and could not be parsed.</exception> public static FiggleFont Parse(Stream stream, StringPool pool = null) { if (stream == null) { throw new ArgumentNullException(nameof(stream)); } // TODO allow specifying encoding var reader = new StreamReader(stream); var firstLine = reader.ReadLine(); if (firstLine == null) { throw new FiggleException("Font file is empty."); } var match = _firstLinePattern.Match(firstLine); if (!match.Success) { throw new FiggleException("Font file has invalid first line."); } var hardBlank = match.Groups["hardblank"].Value[0]; var height = int.Parse(match.Groups["height"].Value); var baseline = int.Parse(match.Groups["baseline"].Value); var layoutOld = int.Parse(match.Groups["layoutold"].Value); var commentLineCount = int.Parse(match.Groups["commentlinecount"].Value); var layoutNewMatch = match.Groups["layoutnew"]; var layoutNew = layoutNewMatch.Success ? int.Parse(layoutNewMatch.Value) : UpgradeLayout(); int UpgradeLayout() { if (layoutOld == 0) { return(SM_KERN); } if (layoutOld < 0) { return(SM_FULLWIDTH); } return((layoutOld & 0x1F) | SM_SMUSH); } var dirMatch = match.Groups["direction"]; var direction = dirMatch.Success ? (FiggleTextDirection)int.Parse(dirMatch.Value) : FiggleTextDirection.LeftToRight; // skip comment lines for (var i = 0; i < commentLineCount; i++) { reader.ReadLine(); } if (pool == null) { pool = new StringPool(); } /* * Characters 0-31 are control characters. * * Characters 32-126 appear in order: * * 32 (blank/space) 64 @ 96 ` * 33 ! 65 A 97 a * 34 " 66 B 98 b * 35 # 67 C 99 c * 36 $ 68 D 100 d * 37 % 69 E 101 e * 38 & 70 F 102 f * 39 ' 71 G 103 g * 40 ( 72 H 104 h * 41 ) 73 I 105 i * 42 * 74 J 106 j * 43 + 75 K 107 k * 44 , 76 L 108 l * 45 - 77 M 109 m * 46 . 78 N 110 n * 47 / 79 O 111 o * 48 0 80 P 112 p * 49 1 81 Q 113 q * 50 2 82 R 114 r * 51 3 83 S 115 s * 52 4 84 T 116 t * 53 5 85 U 117 u * 54 6 86 V 118 v * 55 7 87 W 119 w * 56 8 88 X 120 x * 57 9 89 Y 121 y * 58 : 90 Z 122 z * 59 ; 91 [ 123 { * 60 < 92 \ 124 | * 61 = 93 ] 125 } * 62 > 94 ^ 126 ~ * 63 ? 95 _ * * Then codes: * * 196 Ä * 214 Ö * 220 Ü * 228 ä * 246 ö * 252 ü * 223 ß * * If some of these characters are not desired, empty characters may be used, having endmarks flush with the margin. * * After the required characters come "code tagged characters" in range -2147483648 to +2147483647, excluding -1. The assumed mapping is to ASCII/Latin-1/Unicode. * * A zero character is treated as the character to be used whenever a required character is missing. If no zero character is available, nothing will be printed. */ FiggleCharacter ReadCharacter() { var lines = new Line[height]; for (var i = 0; i < height; i++) { var line = reader.ReadLine(); if (line == null) { throw new FiggleException("Unexpected EOF in Font file."); } // TODO validate single endmark on all lines but last, and double endmark on last // TODO validate all lines are the advertised width (without endmarks) // TODO pool computed space counts too var endmark = line[line.Length - 1]; line = line.TrimEnd(endmark); lines[i] = new Line(pool.Pool(line), CountSolSpaces(line), CountEolSpaces(line)); } return(new FiggleCharacter(lines)); byte CountSolSpaces(string s) { byte count = 0; for (; count < s.Length && s[count] == ' '; count++) { } return(count); } byte CountEolSpaces(string s) { byte count = 0; for (var i = s.Length - 1; i > 0 && s[i] == ' '; i--, count++) { } return(count); } } var requiredCharacters = new FiggleCharacter[256]; for (var i = 32; i < 127; i++) { requiredCharacters[i] = ReadCharacter(); } requiredCharacters[196] = ReadCharacter(); requiredCharacters[214] = ReadCharacter(); requiredCharacters[220] = ReadCharacter(); requiredCharacters[228] = ReadCharacter(); requiredCharacters[246] = ReadCharacter(); requiredCharacters[252] = ReadCharacter(); requiredCharacters[223] = ReadCharacter(); // support code-tagged characters var sparseCharacters = new Dictionary <int, FiggleCharacter>(); while (true) { readLine: var line = reader.ReadLine(); if (line == null) { break; } if (string.IsNullOrWhiteSpace(line)) { goto readLine; } if (!ParseUtil.TryParse(line, out int code)) { throw new FiggleException($"Unsupported code-tagged character code string \"{line}\"."); } if (code >= 0 && code < 256) { requiredCharacters[code] = ReadCharacter(); } else { sparseCharacters[code] = ReadCharacter(); } } return(new FiggleFont(requiredCharacters, sparseCharacters, hardBlank, height, baseline, direction, layoutNew)); }